master
go 279 lines 7.72 KB
Raw
1 package dagcmd
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "time"
10
11 "github.com/cheggaaa/pb/v3"
12 blockstore "github.com/ipfs/boxo/blockstore"
13 "github.com/ipfs/boxo/dag/walker"
14 cid "github.com/ipfs/go-cid"
15 cmds "github.com/ipfs/go-ipfs-cmds"
16 ipld "github.com/ipfs/go-ipld-format"
17 "github.com/ipfs/kubo/core/commands/cmdenv"
18 "github.com/ipfs/kubo/core/commands/cmdutils"
19 iface "github.com/ipfs/kubo/core/coreiface"
20 "github.com/ipfs/kubo/core/coreiface/options"
21 gocar "github.com/ipld/go-car/v2"
22 carstorage "github.com/ipld/go-car/v2/storage"
23 cidlink "github.com/ipld/go-ipld-prime/linking/cid"
24 selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
25 )
26
27 // pb/v3 template for `ipfs dag export`: byte counter, speed, and
28 // elapsed time. No bar/percent/ETA because the total size of the
29 // CAR stream is not known up front. The explicit "%s/s" speed
30 // format overrides pb's default "p/s" suffix so the rate renders
31 // as "MiB/s".
32 const progressBarTemplate = `{{counters . }} {{speed . "%s/s" "?/s"}} {{etime . }}`
33
34 func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
35 // Accept CID or a content path
36 p, err := cmdutils.PathOrCidPath(req.Arguments[0])
37 if err != nil {
38 return err
39 }
40
41 localOnly, _ := req.Options[localOnlyOptionName].(bool)
42 if localOnly {
43 // --local-only and --offline=false contradict each other.
44 if offline, set := req.Options["offline"].(bool); set && !offline {
45 return fmt.Errorf("--%s implies --offline and cannot be combined with --offline=false; please drop one of them", localOnlyOptionName)
46 }
47 }
48
49 api, err := cmdenv.GetApi(env, req)
50 if err != nil {
51 return err
52 }
53 if localOnly {
54 // --local-only implies --offline so api.Block().Stat below cannot
55 // reach out for path resolution. The DAG walk itself uses the raw
56 // blockstore via walker (see exportPartialCAR) and is local by
57 // construction regardless of this setting.
58 api, err = api.WithOptions(options.Api.Offline(true))
59 if err != nil {
60 return err
61 }
62 }
63
64 // Resolve path and confirm the root block is available, fail fast if not
65 b, err := api.Block().Stat(req.Context, p)
66 if err != nil {
67 return err
68 }
69 c := b.Path().RootCid()
70
71 var bs blockstore.Blockstore
72 if localOnly {
73 node, err := cmdenv.GetNode(env)
74 if err != nil {
75 return err
76 }
77 bs = node.Blockstore
78 }
79
80 pipeR, pipeW := io.Pipe()
81
82 errCh := make(chan error, 2) // we only report the 1st error
83 go func() {
84 defer func() {
85 if err := pipeW.Close(); err != nil {
86 errCh <- fmt.Errorf("stream flush failed: %s", err)
87 }
88 close(errCh)
89 }()
90
91 if localOnly {
92 if err := exportPartialCAR(req.Context, bs, c, pipeW); err != nil {
93 errCh <- err
94 }
95 return
96 }
97
98 lsys := cidlink.DefaultLinkSystem()
99 lsys.SetReadStorage(&dagStore{dag: api.Dag(), ctx: req.Context})
100
101 // Uncomment the following to support CARv2 output.
102 /*
103 car, err := gocar.NewSelectiveWriter(req.Context, &lsys, c, selectorparse.CommonSelector_ExploreAllRecursively, gocar.AllowDuplicatePuts(false))
104 if err != nil {
105 errCh <- err
106 return
107 }
108 if _, err = car.WriteTo(pipeW); err != nil {
109 errCh <- err
110 return
111 }
112 */
113 _, err := gocar.TraverseV1(req.Context, &lsys, c, selectorparse.CommonSelector_ExploreAllRecursively, pipeW, gocar.AllowDuplicatePuts(false))
114 if err != nil {
115 errCh <- err
116 return
117 }
118
119 }()
120
121 res.SetEncodingType(cmds.OctetStream)
122 res.SetContentType("application/vnd.ipld.car")
123 if err := res.Emit(pipeR); err != nil {
124 pipeR.Close() // ignore the error if any
125 return err
126 }
127
128 err = <-errCh
129
130 // minimal user friendliness
131 if errors.Is(err, ipld.ErrNotFound{}) {
132 explicitOffline, _ := req.Options["offline"].(bool)
133 if explicitOffline {
134 err = fmt.Errorf("%s (currently offline, perhaps retry without the offline flag)", err)
135 } else {
136 node, envErr := cmdenv.GetNode(env)
137 if envErr == nil && !node.IsOnline {
138 err = fmt.Errorf("%s (currently offline, perhaps retry after attaching to the network)", err)
139 }
140 }
141 }
142
143 return err
144 }
145
146 // exportPartialCAR is the best-effort engine behind `dag export --local-only`.
147 // It walks the DAG rooted at root and writes the visited blocks to w as a
148 // CARv1 stream.
149 //
150 // The walker reads from the raw blockstore directly (not via the kubo
151 // CoreAPI or DAGService), so it is structurally incapable of triggering a
152 // network fetch. Any block missing or unreadable locally, plus its entire
153 // subtree, is silently skipped: the resulting CAR is partial by design.
154 //
155 // Errors writing the CAR itself (emit failures) are surfaced: those are
156 // output problems, not local-availability problems.
157 //
158 // This mirrors the MFS+unique provider in core/node/provider.go.
159 func exportPartialCAR(ctx context.Context, bs blockstore.Blockstore, root cid.Cid, w io.Writer) error {
160 writable, err := carstorage.NewWritable(w, []cid.Cid{root}, gocar.WriteAsCarV1(true))
161 if err != nil {
162 return err
163 }
164
165 // Capture the first emit (write-side) error so the walk stops cleanly.
166 var emitErr error
167 emit := func(k cid.Cid) bool {
168 blk, err := bs.Get(ctx, k)
169 if err != nil {
170 // Any read error after locality passed (e.g. GC race or
171 // corruption) is treated as "not available locally": skip
172 // the block and keep streaming the rest of the partial CAR.
173 return true
174 }
175 if err := writable.Put(ctx, k.KeyString(), blk.RawData()); err != nil {
176 emitErr = err
177 return false
178 }
179 return true
180 }
181
182 // Both the locality check (bs.Has) and the link fetcher read straight
183 // from the blockstore, so the walk cannot reach the network. Errors
184 // inside walker (locality, fetch) are skip-and-log, matching the
185 // best-effort semantics here.
186 if err := walker.WalkDAG(ctx, root,
187 walker.LinksFetcherFromBlockstore(bs),
188 emit,
189 walker.WithLocality(func(ctx context.Context, k cid.Cid) (bool, error) { return bs.Has(ctx, k) }),
190 ); err != nil {
191 return err
192 }
193 return emitErr
194 }
195
196 func finishCLIExport(res cmds.Response, re cmds.ResponseEmitter) error {
197 if !cmdenv.ShouldShowProgress(res.Request(), progressOptionName) {
198 return cmds.Copy(re, res)
199 }
200
201 bar := pb.New64(0).Set(pb.Bytes, true).SetWriter(os.Stderr).SetRefreshRate(500 * time.Millisecond)
202 bar.SetTemplateString(progressBarTemplate)
203 bar.Start()
204
205 var processedOneResponse bool
206 for {
207 v, err := res.Next()
208 if err != nil {
209 if errors.Is(err, io.EOF) {
210 // We only write the final bar update on success
211 // On error it looks too weird
212 bar.Finish()
213 return re.Close()
214 }
215 return re.CloseWithError(err)
216 }
217
218 if processedOneResponse {
219 return re.CloseWithError(errors.New("unexpected multipart response during emit, please file a bugreport"))
220 }
221
222 r, ok := v.(io.Reader)
223 if !ok {
224 // some sort of encoded response, this should not be happening
225 return errors.New("unexpected non-stream passed to PostRun: please file a bugreport")
226 }
227
228 processedOneResponse = true
229
230 if err = re.Emit(bar.NewProxyReader(r)); err != nil {
231 return err
232 }
233 }
234 }
235
236 type dagStore struct {
237 dag iface.APIDagService
238 ctx context.Context
239 }
240
241 func (ds *dagStore) Get(ctx context.Context, key string) ([]byte, error) {
242 if ctx.Err() != nil {
243 return nil, ctx.Err()
244 }
245
246 c, err := cidFromBinString(key)
247 if err != nil {
248 return nil, err
249 }
250
251 block, err := ds.dag.Get(ds.ctx, c)
252 if err != nil {
253 return nil, err
254 }
255
256 return block.RawData(), nil
257 }
258
259 func (ds *dagStore) Has(ctx context.Context, key string) (bool, error) {
260 _, err := ds.Get(ctx, key)
261 if err != nil {
262 if errors.Is(err, ipld.ErrNotFound{}) {
263 return false, nil
264 }
265 return false, err
266 }
267 return true, nil
268 }
269
270 func cidFromBinString(key string) (cid.Cid, error) {
271 l, k, err := cid.CidFromBytes([]byte(key))
272 if err != nil {
273 return cid.Undef, fmt.Errorf("dagStore: key was not a cid: %w", err)
274 }
275 if l != len(key) {
276 return cid.Undef, fmt.Errorf("dagStore: key was not a cid: had %d bytes leftover", len(key)-l)
277 }
278 return k, nil
279 }