Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9ad0c3b
feat(dag): add --local-only to dag export and import
ChayanDass Mar 7, 2026
fd7ee67
Merge branch 'master' into feat/dag-export-import-local-only
ChayanDass Mar 7, 2026
b402796
chore(deps): update go-car/v2 to latest master
lidel Mar 8, 2026
7b99db6
fix(dag): avoid CID round-trip in export and fix ci failure
ChayanDass Mar 10, 2026
3b27eed
dag: add validation and tests for --local-only flag
ChayanDass Mar 11, 2026
6a726f2
Merge branch 'master' into feat/dag-export-import-local-only
ChayanDass Mar 11, 2026
d24cde4
Merge branch 'master' into feat/dag-export-import-local-only
ChayanDass Mar 14, 2026
56a284a
Merge branch 'master' into feat/dag-export-import-local-only
ChayanDass Mar 16, 2026
6adbadf
Merge branch 'master' into feat/dag-export-import-local-only
ChayanDass Mar 18, 2026
76bc4e8
Merge branch 'master' into feat/dag-export-import-local-only
ChayanDass Mar 25, 2026
2e088cb
Merge remote-tracking branch 'origin/master' into feat/dag-export-imp…
lidel May 25, 2026
51689a1
chore(deps): bump go-car/v2 to latest master
lidel May 25, 2026
8871b6b
feat(dag): --local-only auto-sets companion flags
lidel May 25, 2026
bcc4457
refactor(dag): use boxo/walker for --local-only export
lidel May 25, 2026
16bb699
test(dag): tighten --local-only tests, add subtree-skip case
lidel May 25, 2026
6fcf6ca
docs: changelog entry for --local-only dag export/import
lidel May 25, 2026
1f39245
refactor(dag): wrap API explicitly for --local-only
lidel May 25, 2026
d4fce1d
fix(provider): quiet context.Canceled on shutdown
lidel May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion core/commands/dag/dag.go
Comment thread
ChayanDass marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
fastProvideRootOptionName = "fast-provide-root"
fastProvideDAGOptionName = "fast-provide-dag"
fastProvideWaitOptionName = "fast-provide-wait"
localOnlyOptionName = "local-only"
)

// DagCmd provides a subset of commands for interacting with ipld dag objects
Expand Down Expand Up @@ -193,6 +194,10 @@ Note:
currently present in the blockstore does not represent a complete DAG,
pinning of that individual root will fail.

Use --local-only to import a partial CAR (e.g. from 'dag export
--local-only'). --local-only implies --pin-roots=false because a partial
CAR has no full DAG to pin.

FAST PROVIDE OPTIMIZATION:

Root CIDs from CAR headers are immediately provided to the DHT in addition
Expand All @@ -213,7 +218,8 @@ Specification of CAR formats: https://ipld.io/specs/transport/car/
cmds.FileArg("path", true, true, "The path of a .car file.").EnableStdin(),
},
Options: []cmds.Option{
cmds.BoolOption(pinRootsOptionName, "Pin optional roots listed in the .car headers after importing.").WithDefault(true),
cmds.BoolOption(pinRootsOptionName, "Pin optional roots listed in the .car headers after importing. Default: true."),
cmds.BoolOption(localOnlyOptionName, "Import a partial CAR (e.g. from 'dag export --local-only'). Implies --pin-roots=false."),
cmds.BoolOption(silentOptionName, "No output."),
cmds.BoolOption(statsOptionName, "Output stats."),
cmds.BoolOption(fastProvideRootOptionName, "Immediately provide root CIDs to DHT in addition to regular queue, for faster discovery. Default: Import.FastProvideRoot"),
Expand Down Expand Up @@ -277,6 +283,10 @@ var DagExportCmd = &cmds.Command{
Note that at present only single root selections / .car files are supported.
The output of blocks happens in strict DAG-traversal, first-seen, order.
CAR file follows the CARv1 format: https://ipld.io/specs/transport/car/carv1/

Use --local-only for a best-effort export from the local blockstore: blocks
that are missing or unreadable locally (and their subtrees) are skipped, so
the resulting CAR is partial. --local-only implies --offline.
`,
HTTP: &cmds.HTTPHelpText{
ResponseContentType: "application/vnd.ipld.car",
Expand All @@ -287,6 +297,7 @@ CAR file follows the CARv1 format: https://ipld.io/specs/transport/car/carv1/
},
Options: []cmds.Option{
cmds.BoolOption(progressOptionName, "p", "Stream progress data. Defaults to true when stderr is a terminal."),
cmds.BoolOption(localOnlyOptionName, "Best-effort export of locally-available blocks; missing or unreadable blocks (and their subtrees) are skipped. Implies --offline."),
},
Run: dagExport,
PostRun: cmds.PostRunMap{
Expand Down
90 changes: 89 additions & 1 deletion core/commands/dag/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import (
"time"

"github.com/cheggaaa/pb/v3"
blockstore "github.com/ipfs/boxo/blockstore"
"github.com/ipfs/boxo/dag/walker"
cid "github.com/ipfs/go-cid"
cmds "github.com/ipfs/go-ipfs-cmds"
ipld "github.com/ipfs/go-ipld-format"
"github.com/ipfs/kubo/core/commands/cmdenv"
"github.com/ipfs/kubo/core/commands/cmdutils"
iface "github.com/ipfs/kubo/core/coreiface"
"github.com/ipfs/kubo/core/coreiface/options"
gocar "github.com/ipld/go-car/v2"
carstorage "github.com/ipld/go-car/v2/storage"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
)
Expand All @@ -34,10 +38,28 @@ func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
return err
}

localOnly, _ := req.Options[localOnlyOptionName].(bool)
if localOnly {
// --local-only and --offline=false contradict each other.
if offline, set := req.Options["offline"].(bool); set && !offline {
return fmt.Errorf("--%s implies --offline and cannot be combined with --offline=false; please drop one of them", localOnlyOptionName)
}
}

api, err := cmdenv.GetApi(env, req)
if err != nil {
return err
}
if localOnly {
// --local-only implies --offline so api.Block().Stat below cannot
// reach out for path resolution. The DAG walk itself uses the raw
// blockstore via walker (see exportPartialCAR) and is local by
// construction regardless of this setting.
api, err = api.WithOptions(options.Api.Offline(true))
if err != nil {
return err
}
}

// Resolve path and confirm the root block is available, fail fast if not
b, err := api.Block().Stat(req.Context, p)
Expand All @@ -46,6 +68,15 @@ func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
}
c := b.Path().RootCid()

var bs blockstore.Blockstore
if localOnly {
node, err := cmdenv.GetNode(env)
if err != nil {
return err
}
bs = node.Blockstore
}

pipeR, pipeW := io.Pipe()

errCh := make(chan error, 2) // we only report the 1st error
Expand All @@ -57,6 +88,13 @@ func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
close(errCh)
}()

if localOnly {
if err := exportPartialCAR(req.Context, bs, c, pipeW); err != nil {
errCh <- err
}
return
}

lsys := cidlink.DefaultLinkSystem()
lsys.SetReadStorage(&dagStore{dag: api.Dag(), ctx: req.Context})

Expand Down Expand Up @@ -105,6 +143,56 @@ func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
return err
}

// exportPartialCAR is the best-effort engine behind `dag export --local-only`.
// It walks the DAG rooted at root and writes the visited blocks to w as a
// CARv1 stream.
//
// The walker reads from the raw blockstore directly (not via the kubo
// CoreAPI or DAGService), so it is structurally incapable of triggering a
// network fetch. Any block missing or unreadable locally, plus its entire
// subtree, is silently skipped: the resulting CAR is partial by design.
//
// Errors writing the CAR itself (emit failures) are surfaced: those are
// output problems, not local-availability problems.
//
// This mirrors the MFS+unique provider in core/node/provider.go.
func exportPartialCAR(ctx context.Context, bs blockstore.Blockstore, root cid.Cid, w io.Writer) error {
writable, err := carstorage.NewWritable(w, []cid.Cid{root}, gocar.WriteAsCarV1(true))
if err != nil {
return err
}

// Capture the first emit (write-side) error so the walk stops cleanly.
var emitErr error
emit := func(k cid.Cid) bool {
blk, err := bs.Get(ctx, k)
if err != nil {
// Any read error after locality passed (e.g. GC race or
// corruption) is treated as "not available locally": skip
// the block and keep streaming the rest of the partial CAR.
return true
}
if err := writable.Put(ctx, k.KeyString(), blk.RawData()); err != nil {
emitErr = err
return false
}
return true
}

// Both the locality check (bs.Has) and the link fetcher read straight
// from the blockstore, so the walk cannot reach the network. Errors
// inside walker (locality, fetch) are skip-and-log, matching the
// best-effort semantics here.
if err := walker.WalkDAG(ctx, root,
walker.LinksFetcherFromBlockstore(bs),
emit,
walker.WithLocality(func(ctx context.Context, k cid.Cid) (bool, error) { return bs.Has(ctx, k) }),
); err != nil {
return err
}
return emitErr
}

func finishCLIExport(res cmds.Response, re cmds.ResponseEmitter) error {
if !cmdenv.ShouldShowProgress(res.Request(), progressOptionName) {
return cmds.Copy(re, res)
Expand Down Expand Up @@ -185,7 +273,7 @@ func cidFromBinString(key string) (cid.Cid, error) {
return cid.Undef, fmt.Errorf("dagStore: key was not a cid: %w", err)
}
if l != len(key) {
return cid.Undef, fmt.Errorf("dagSore: key was not a cid: had %d bytes leftover", len(key)-l)
return cid.Undef, fmt.Errorf("dagStore: key was not a cid: had %d bytes leftover", len(key)-l)
}
return k, nil
}
18 changes: 17 additions & 1 deletion core/commands/dag/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,24 @@ func dagImport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment
return err
}

doPinRoots, _ := req.Options[pinRootsOptionName].(bool)
pinRootsVal, pinRootsSet := req.Options[pinRootsOptionName].(bool)
localOnly, _ := req.Options[localOnlyOptionName].(bool)

// --pin-roots defaults to true; the default is applied here (not via
// .WithDefault) so we can tell apart "user explicitly passed true" from
// "no value provided".
doPinRoots := true
if pinRootsSet {
doPinRoots = pinRootsVal
}

if localOnly {
if pinRootsSet && pinRootsVal {
return fmt.Errorf("--%s implies --%s=false and cannot be combined with --%s=true; please drop one of them", localOnlyOptionName, pinRootsOptionName, pinRootsOptionName)
}
// --local-only implies --pin-roots=false: a partial CAR has no full DAG to pin.
doPinRoots = false
}
fastProvideRoot, fastProvideRootSet := req.Options[fastProvideRootOptionName].(bool)
fastProvideDAG, fastProvideDAGSet := req.Options[fastProvideDAGOptionName].(bool)
fastProvideWait, fastProvideWaitSet := req.Options[fastProvideWaitOptionName].(bool)
Expand Down
18 changes: 12 additions & 6 deletions core/node/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -880,11 +880,13 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option {
strategy := cfg.Provide.Strategy.WithDefault(config.DefaultProvideStrategy)
providerLog.Infow("provider keystore sync started", "strategy", strategy)
if err := syncKeystore(ctx); err != nil {
// ErrClosed means the keystore was closed by the shutdown
// hook while this goroutine was still in flight: the
// OnStart ctx is not cancelled yet, so we classify the
// failure as shutdown explicitly.
if ctx.Err() != nil || errors.Is(err, keystore.ErrClosed) {
// Shutdown can race ahead of ctx.Err() becoming
// visible here: ResetCids returns ctx.Err()
// straight from its own ctx-done select, and the
// keystore can also close mid-sync (ErrClosed)
// before the OnStart ctx is cancelled. Classify
// both as shutdown.
if ctx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, keystore.ErrClosed) {
providerLog.Debugw("provider keystore sync interrupted by shutdown", "err", err, "strategy", strategy)
} else {
providerLog.Errorw("provider keystore sync failed", "err", err, "strategy", strategy)
Expand All @@ -908,7 +910,11 @@ func SweepingProviderOpt(cfg *config.Config) fx.Option {
return
case <-ticker.C:
if err := syncKeystore(gcCtx); err != nil {
if gcCtx.Err() != nil || errors.Is(err, keystore.ErrClosed) {
// See classifier note on the startup-sync
// branch above: context.Canceled can
// arrive ahead of gcCtx.Err() becoming
// visible to this goroutine.
if gcCtx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, keystore.ErrClosed) {
providerLog.Debugw("provider keystore sync interrupted by shutdown", "err", err)
} else {
providerLog.Errorw("provider keystore sync failed", "err", err)
Expand Down
12 changes: 12 additions & 0 deletions docs/changelogs/v0.42.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
- [Overview](#overview)
- [🔦 Highlights](#-highlights)
- [🎯 Announce CIDs on demand with `ipfs provide once`](#-announce-cids-on-demand-with-ipfs-provide-once)
- [🧩 Export and import partial CARs with `--local-only`](#-export-and-import-partial-cars-with---local-only)
- [⚙️ `Provide.DHT.Interval=0` no longer disables providing](#%EF%B8%8F-providedhtinterval0-no-longer-disables-providing)
- [🐛 Fixed pin operations hanging under pinned reprovide strategies](#-fixed-pin-operations-hanging-under-pinned-reprovide-strategies)
- [🐛 Smoother first-run upgrades from very old repos](#-smoother-first-run-upgrades-from-very-old-repos)
Expand Down Expand Up @@ -46,6 +47,17 @@ In a terminal, the command shows a running count of queued CIDs. With `--enc=jso

`ipfs routing provide` keeps working but is deprecated. See `ipfs provide once --help` for usage and migration notes.

#### 🧩 Export and import partial CARs with `--local-only`

`ipfs dag export --local-only` writes a CAR with only the blocks you have locally; any missing blocks (and their subtrees) are skipped instead of failing the export. `ipfs dag import --local-only` reads such a partial CAR without trying to pin its roots.

This is useful when:

- you want to share part of a DAG (for example an MFS tree) that is only partly cached locally
- you fetched a partial CAR from a gateway that supports [IPIP-0402](https://specs.ipfs.tech/ipips/ipip-0402/) and want to add what you got to your local store

`--local-only` sets the matching companion flag automatically: on export it implies `--offline`; on import it implies `--pin-roots=false`. See `ipfs dag export --help` and `ipfs dag import --help` for details.

#### ⚙️ `Provide.DHT.Interval=0` no longer disables providing

`Provide.DHT.Interval=0` now disables only the periodic reprovide schedule. New CIDs still announce via fast-provide-root and `ipfs provide once`. To fully disable providing, set [`Provide.Enabled=false`](https://github.com/ipfs/kubo/blob/master/docs/config.md#provideenabled).
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/kubo-as-a-library/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ require (
github.com/ipfs/go-peertaskqueue v0.8.3 // indirect
github.com/ipfs/go-test v0.3.0 // indirect
github.com/ipfs/go-unixfsnode v1.10.4 // indirect
github.com/ipld/go-car/v2 v2.16.0 // indirect
github.com/ipld/go-car/v2 v2.16.1-0.20260428045700-c4b9f366f20c // indirect
github.com/ipld/go-codec-dagpb v1.7.0 // indirect
github.com/ipld/go-ipld-prime v0.23.0 // indirect
github.com/ipshipyard/p2p-forge v0.8.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/kubo-as-a-library/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,8 @@ github.com/ipfs/go-test v0.3.0 h1:0Y4Uve3tp9HI+2lIJjfOliOrOgv/YpXg/l1y3P4DEYE=
github.com/ipfs/go-test v0.3.0/go.mod h1:JK+U8pRpATZb7lsYNSJlCj3WYB3cFfWIbI6nWRM/GFk=
github.com/ipfs/go-unixfsnode v1.10.4 h1:cMmMyOrSjQkPVQbQvt8trErIn6jhayNf9pBA9oOwfxY=
github.com/ipfs/go-unixfsnode v1.10.4/go.mod h1:Vu1e/s7ToALBBRo38sJ8DwUVWmSeQMTdxk5/rcHl7d0=
github.com/ipld/go-car/v2 v2.16.0 h1:LWe0vmN/QcQmUU4tr34W5Nv5mNraW+G6jfN2s+ndBco=
github.com/ipld/go-car/v2 v2.16.0/go.mod h1:RqFGWN9ifcXVmCrTAVnfnxiWZk1+jIx67SYhenlmL34=
github.com/ipld/go-car/v2 v2.16.1-0.20260428045700-c4b9f366f20c h1:ZFONxHSj6bzzB9eKIu+yS2AazTJe7j9FPesfy4sZSE0=
github.com/ipld/go-car/v2 v2.16.1-0.20260428045700-c4b9f366f20c/go.mod h1:/4HY8tFZ1q42Mw54ILLPQfjkUqMJxFKqY1yMDKHlYko=
github.com/ipld/go-codec-dagpb v1.7.0 h1:hpuvQjCSVSLnTnHXn+QAMR0mLmb1gA6wl10LExo2Ts0=
github.com/ipld/go-codec-dagpb v1.7.0/go.mod h1:rD3Zg+zub9ZnxcLwfol/OTQRVjaLzXypgy4UqHQvilM=
github.com/ipld/go-ipld-prime v0.11.0/go.mod h1:+WIAkokurHmZ/KwzDOMUuoeJgaRQktHtEaLglS3ZeV8=
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ require (
github.com/ipfs/go-metrics-prometheus v0.1.0
github.com/ipfs/go-test v0.3.0
github.com/ipfs/go-unixfsnode v1.10.4
github.com/ipld/go-car/v2 v2.16.0
github.com/ipld/go-car/v2 v2.16.1-0.20260428045700-c4b9f366f20c
github.com/ipld/go-codec-dagpb v1.7.0
github.com/ipld/go-ipld-prime v0.23.0
github.com/ipshipyard/p2p-forge v0.8.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,8 @@ github.com/ipfs/go-test v0.3.0 h1:0Y4Uve3tp9HI+2lIJjfOliOrOgv/YpXg/l1y3P4DEYE=
github.com/ipfs/go-test v0.3.0/go.mod h1:JK+U8pRpATZb7lsYNSJlCj3WYB3cFfWIbI6nWRM/GFk=
github.com/ipfs/go-unixfsnode v1.10.4 h1:cMmMyOrSjQkPVQbQvt8trErIn6jhayNf9pBA9oOwfxY=
github.com/ipfs/go-unixfsnode v1.10.4/go.mod h1:Vu1e/s7ToALBBRo38sJ8DwUVWmSeQMTdxk5/rcHl7d0=
github.com/ipld/go-car/v2 v2.16.0 h1:LWe0vmN/QcQmUU4tr34W5Nv5mNraW+G6jfN2s+ndBco=
github.com/ipld/go-car/v2 v2.16.0/go.mod h1:RqFGWN9ifcXVmCrTAVnfnxiWZk1+jIx67SYhenlmL34=
github.com/ipld/go-car/v2 v2.16.1-0.20260428045700-c4b9f366f20c h1:ZFONxHSj6bzzB9eKIu+yS2AazTJe7j9FPesfy4sZSE0=
github.com/ipld/go-car/v2 v2.16.1-0.20260428045700-c4b9f366f20c/go.mod h1:/4HY8tFZ1q42Mw54ILLPQfjkUqMJxFKqY1yMDKHlYko=
github.com/ipld/go-codec-dagpb v1.7.0 h1:hpuvQjCSVSLnTnHXn+QAMR0mLmb1gA6wl10LExo2Ts0=
github.com/ipld/go-codec-dagpb v1.7.0/go.mod h1:rD3Zg+zub9ZnxcLwfol/OTQRVjaLzXypgy4UqHQvilM=
github.com/ipld/go-ipld-prime v0.11.0/go.mod h1:+WIAkokurHmZ/KwzDOMUuoeJgaRQktHtEaLglS3ZeV8=
Expand Down
Loading
Loading