master
go 217 lines 6.02 KB
Raw
1 package coreapi
2
3 import (
4 "context"
5 "fmt"
6
7 dag "github.com/ipfs/boxo/ipld/merkledag"
8 "github.com/ipfs/boxo/ipld/merkledag/dagutils"
9 ft "github.com/ipfs/boxo/ipld/unixfs"
10 "github.com/ipfs/boxo/path"
11 coreiface "github.com/ipfs/kubo/core/coreiface"
12 caopts "github.com/ipfs/kubo/core/coreiface/options"
13 "go.opentelemetry.io/otel/attribute"
14 "go.opentelemetry.io/otel/trace"
15
16 "github.com/ipfs/kubo/tracing"
17 )
18
19 type ObjectAPI CoreAPI
20
21 type Link struct {
22 Name, Hash string
23 Size uint64
24 }
25
26 type Node struct {
27 Links []Link
28 Data string
29 }
30
31 func (api *ObjectAPI) AddLink(ctx context.Context, base path.Path, name string, child path.Path, opts ...caopts.ObjectAddLinkOption) (path.ImmutablePath, error) {
32 ctx, span := tracing.Span(ctx, "CoreAPI.ObjectAPI", "AddLink", trace.WithAttributes(
33 attribute.String("base", base.String()),
34 attribute.String("name", name),
35 attribute.String("child", child.String()),
36 ))
37 defer span.End()
38
39 options, err := caopts.ObjectAddLinkOptions(opts...)
40 if err != nil {
41 return path.ImmutablePath{}, err
42 }
43 span.SetAttributes(attribute.Bool("create", options.Create))
44
45 baseNd, err := api.core().ResolveNode(ctx, base)
46 if err != nil {
47 return path.ImmutablePath{}, err
48 }
49
50 childNd, err := api.core().ResolveNode(ctx, child)
51 if err != nil {
52 return path.ImmutablePath{}, err
53 }
54
55 basePb, ok := baseNd.(*dag.ProtoNode)
56 if !ok {
57 return path.ImmutablePath{}, dag.ErrNotProtobuf
58 }
59
60 // This command operates at the dag-pb level via dagutils.Editor, which
61 // only manipulates ProtoNode links without updating UnixFS metadata.
62 // Only plain UnixFS Directory nodes are safe to mutate this way.
63 // File nodes: adding links corrupts Blocksizes, content lost on read-back.
64 // HAMTShard nodes: bitfield not updated, shard trie becomes inconsistent.
65 // https://specs.ipfs.tech/unixfs/#pbnode-links-name
66 // https://github.com/ipfs/kubo/issues/7190
67 if !options.SkipUnixFSValidation {
68 fsNode, err := ft.FSNodeFromBytes(basePb.Data())
69 if err != nil {
70 return path.ImmutablePath{}, fmt.Errorf(
71 "cannot add named links to a non-UnixFS dag-pb node; " +
72 "pass --allow-non-unixfs to skip validation")
73 }
74 switch fsNode.Type() {
75 case ft.TDirectory:
76 // plain directories: safe, no link-count metadata to desync
77 case ft.THAMTShard:
78 return path.ImmutablePath{}, fmt.Errorf(
79 "cannot add links to a HAMTShard at the dag-pb level " +
80 "(would corrupt the HAMT bitfield); use 'ipfs files' " +
81 "commands instead, or pass --allow-non-unixfs to override")
82 default:
83 return path.ImmutablePath{}, fmt.Errorf(
84 "cannot add named links to a UnixFS %s node, "+
85 "only Directory nodes support link addition at the dag-pb level "+
86 "(see https://specs.ipfs.tech/unixfs/)",
87 fsNode.Type())
88 }
89 }
90
91 var createfunc func() *dag.ProtoNode
92 if options.Create {
93 createfunc = ft.EmptyDirNode
94 }
95
96 e := dagutils.NewDagEditor(basePb, api.dag)
97
98 err = e.InsertNodeAtPath(ctx, name, childNd, createfunc)
99 if err != nil {
100 return path.ImmutablePath{}, err
101 }
102
103 nnode, err := e.Finalize(ctx, api.dag)
104 if err != nil {
105 return path.ImmutablePath{}, err
106 }
107
108 return path.FromCid(nnode.Cid()), nil
109 }
110
111 func (api *ObjectAPI) RmLink(ctx context.Context, base path.Path, link string, opts ...caopts.ObjectRmLinkOption) (path.ImmutablePath, error) {
112 ctx, span := tracing.Span(ctx, "CoreAPI.ObjectAPI", "RmLink", trace.WithAttributes(
113 attribute.String("base", base.String()),
114 attribute.String("link", link)),
115 )
116 defer span.End()
117
118 options, err := caopts.ObjectRmLinkOptions(opts...)
119 if err != nil {
120 return path.ImmutablePath{}, err
121 }
122
123 baseNd, err := api.core().ResolveNode(ctx, base)
124 if err != nil {
125 return path.ImmutablePath{}, err
126 }
127
128 basePb, ok := baseNd.(*dag.ProtoNode)
129 if !ok {
130 return path.ImmutablePath{}, dag.ErrNotProtobuf
131 }
132
133 // Same validation as AddLink: dagutils.Editor operates at the dag-pb
134 // level and cannot update UnixFS metadata (HAMT bitfields, Blocksizes).
135 if !options.SkipUnixFSValidation {
136 fsNode, err := ft.FSNodeFromBytes(basePb.Data())
137 if err != nil {
138 return path.ImmutablePath{}, fmt.Errorf(
139 "cannot remove links from a non-UnixFS dag-pb node; " +
140 "pass --allow-non-unixfs to skip validation")
141 }
142 switch fsNode.Type() {
143 case ft.TDirectory:
144 // plain directories: safe, no link-count metadata to desync
145 case ft.THAMTShard:
146 return path.ImmutablePath{}, fmt.Errorf(
147 "cannot remove links from a HAMTShard at the dag-pb level " +
148 "(would corrupt the HAMT bitfield); use 'ipfs files rm' " +
149 "instead, or pass --allow-non-unixfs to override")
150 default:
151 return path.ImmutablePath{}, fmt.Errorf(
152 "cannot remove links from a UnixFS %s node, "+
153 "only Directory nodes support link removal at the dag-pb level "+
154 "(see https://specs.ipfs.tech/unixfs/)",
155 fsNode.Type())
156 }
157 }
158
159 e := dagutils.NewDagEditor(basePb, api.dag)
160
161 err = e.RmLink(ctx, link)
162 if err != nil {
163 return path.ImmutablePath{}, err
164 }
165
166 nnode, err := e.Finalize(ctx, api.dag)
167 if err != nil {
168 return path.ImmutablePath{}, err
169 }
170
171 return path.FromCid(nnode.Cid()), nil
172 }
173
174 func (api *ObjectAPI) Diff(ctx context.Context, before path.Path, after path.Path) ([]coreiface.ObjectChange, error) {
175 ctx, span := tracing.Span(ctx, "CoreAPI.ObjectAPI", "Diff", trace.WithAttributes(
176 attribute.String("before", before.String()),
177 attribute.String("after", after.String()),
178 ))
179 defer span.End()
180
181 beforeNd, err := api.core().ResolveNode(ctx, before)
182 if err != nil {
183 return nil, err
184 }
185
186 afterNd, err := api.core().ResolveNode(ctx, after)
187 if err != nil {
188 return nil, err
189 }
190
191 changes, err := dagutils.Diff(ctx, api.dag, beforeNd, afterNd)
192 if err != nil {
193 return nil, err
194 }
195
196 out := make([]coreiface.ObjectChange, len(changes))
197 for i, change := range changes {
198 out[i] = coreiface.ObjectChange{
199 Type: coreiface.ChangeType(change.Type),
200 Path: change.Path,
201 }
202
203 if change.Before.Defined() {
204 out[i].Before = path.FromCid(change.Before)
205 }
206
207 if change.After.Defined() {
208 out[i].After = path.FromCid(change.After)
209 }
210 }
211
212 return out, nil
213 }
214
215 func (api *ObjectAPI) core() coreiface.CoreAPI {
216 return (*CoreAPI)(api)
217 }