From 9ad0c3b535e3f855f589699b12b2f4579b62d290 Mon Sep 17 00:00:00 2001 From: Chayan Das <01chayandas@gmail.com> Date: Sat, 7 Mar 2026 13:14:56 +0530 Subject: [PATCH 1/4] feat(dag): add --local-only to dag export and import - Export: only export blocks present locally; skip missing (partial CAR). --local-only with --offline. Support both binary and base58 link keys. - Import: support partial CARs; --local-only with -- pin-roots=false (error if both --pin-roots and --local-only set). - Fix cidFromBinString to accept base58 key format from link implementations. Signed-off-by: Chayan Das <01chayandas@gmail.com> --- core/commands/dag/dag.go | 6 ++++++ core/commands/dag/export.go | 36 +++++++++++++++++++++++++++++------- core/commands/dag/import.go | 4 ++++ go.mod | 2 ++ go.sum | 2 -- 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/core/commands/dag/dag.go b/core/commands/dag/dag.go index a256213ecd0..f5548bddcce 100644 --- a/core/commands/dag/dag.go +++ b/core/commands/dag/dag.go @@ -23,6 +23,7 @@ const ( statsOptionName = "stats" fastProvideRootOptionName = "fast-provide-root" fastProvideWaitOptionName = "fast-provide-wait" + localOnlyOptionName = "local-only" ) // DagCmd provides a subset of commands for interacting with ipld dag objects @@ -192,6 +193,9 @@ Note: currently present in the blockstore does not represent a complete DAG, pinning of that individual root will fail. + Use --local-only with --pin-roots=false when importing a partial CAR (e.g. from + 'dag export --local-only'). + FAST PROVIDE OPTIMIZATION: Root CIDs from CAR headers are immediately provided to the DHT in addition @@ -213,6 +217,7 @@ Specification of CAR formats: https://ipld.io/specs/transport/car/ }, Options: []cmds.Option{ cmds.BoolOption(pinRootsOptionName, "Pin optional roots listed in the .car headers after importing.").WithDefault(true), + cmds.BoolOption(localOnlyOptionName, "Import a partial CAR without pinning roots (for CARs from dag export --local-only)."), 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"), @@ -285,6 +290,7 @@ CAR file follows the CARv1 format: https://ipld.io/specs/transport/car/carv1/ }, Options: []cmds.Option{ cmds.BoolOption(progressOptionName, "p", "Display progress on CLI. Defaults to true when STDERR is a TTY."), + cmds.BoolOption(localOnlyOptionName, "If set, only blocks present locally are exported; missing blocks are skipped (partial CAR). Use with --offline for a local-only DAG walk."), }, Run: dagExport, PostRun: cmds.PostRunMap{ diff --git a/core/commands/dag/export.go b/core/commands/dag/export.go index 48223f86083..9104ae74c57 100644 --- a/core/commands/dag/export.go +++ b/core/commands/dag/export.go @@ -1,6 +1,7 @@ package dagcmd import ( + "bytes" "context" "errors" "fmt" @@ -16,7 +17,10 @@ import ( "github.com/ipfs/kubo/core/commands/cmdutils" iface "github.com/ipfs/kubo/core/coreiface" gocar "github.com/ipld/go-car/v2" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/linking" cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/traversal" selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" ) @@ -27,6 +31,7 @@ func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment return err } + localOnly, _ := req.Options[localOnlyOptionName].(bool) api, err := cmdenv.GetApi(env, req) if err != nil { return err @@ -51,7 +56,25 @@ func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment }() lsys := cidlink.DefaultLinkSystem() - lsys.SetReadStorage(&dagStore{dag: api.Dag(), ctx: req.Context}) + ds := &dagStore{dag: api.Dag(), ctx: req.Context} + if localOnly { + lsys.StorageReadOpener = func(lctx linking.LinkContext, lnk datamodel.Link) (io.Reader, error) { + cl, ok := lnk.(cidlink.Link) + if !ok { + return nil, fmt.Errorf("unsupported link type: %T", lnk) + } + data, err := ds.Get(lctx.Ctx, cl.Cid.String()) + if err != nil { + if ipld.IsNotFound(err) { + return nil, traversal.SkipMe{} + } + return nil, fmt.Errorf("local block read failed: %w", err) + } + return bytes.NewReader(data), nil + } + } else { + lsys.SetReadStorage(ds) + } // Uncomment the following to support CARv2 output. /* @@ -189,12 +212,11 @@ func (ds *dagStore) Has(ctx context.Context, key string) (bool, error) { } func cidFromBinString(key string) (cid.Cid, error) { - l, k, err := cid.CidFromBytes([]byte(key)) - if err != nil { - return cid.Undef, fmt.Errorf("dagStore: key was not a cid: %w", err) + if l, k, err := cid.CidFromBytes([]byte(key)); err == nil && l == len(key) { + return k, nil } - if l != len(key) { - return cid.Undef, fmt.Errorf("dagSore: key was not a cid: had %d bytes leftover", len(key)-l) + if c, decodeErr := cid.Decode(key); decodeErr == nil { + return c, nil } - return k, nil + return cid.Undef, fmt.Errorf("dagStore: key was not a cid (binary or base58)") } diff --git a/core/commands/dag/import.go b/core/commands/dag/import.go index 032b9e52a6c..fde3e9157f8 100644 --- a/core/commands/dag/import.go +++ b/core/commands/dag/import.go @@ -49,7 +49,11 @@ func dagImport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment } doPinRoots, _ := req.Options[pinRootsOptionName].(bool) + localOnly, _ := req.Options[localOnlyOptionName].(bool) + if doPinRoots && localOnly { + return fmt.Errorf("cannot pass both --%s and --%s", pinRootsOptionName, localOnlyOptionName) + } fastProvideRoot, fastProvideRootSet := req.Options[fastProvideRootOptionName].(bool) fastProvideWait, fastProvideWaitSet := req.Options[fastProvideWaitOptionName].(bool) diff --git a/go.mod b/go.mod index cfffd2b69b4..9c4a0877c68 100644 --- a/go.mod +++ b/go.mod @@ -280,3 +280,5 @@ exclude ( github.com/ipfs/go-ipfs-cmds v2.0.1+incompatible github.com/libp2p/go-libp2p v6.0.23+incompatible ) + +replace github.com/ipld/go-car/v2 => ../go-car/v2 diff --git a/go.sum b/go.sum index 819906d270e..4bdc7d0fadb 100644 --- a/go.sum +++ b/go.sum @@ -458,8 +458,6 @@ github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc= github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o= github.com/ipfs/go-unixfsnode v1.10.3 h1:c8sJjuGNkxXAQH75P+f5ngPda/9T+DrboVA0TcDGvGI= github.com/ipfs/go-unixfsnode v1.10.3/go.mod h1:2Jlc7DoEwr12W+7l8Hr6C7XF4NHST3gIkqSArLhGSxU= -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-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= From b4027962057b38c4c32fcc35be0ccca06dae1432 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 9 Mar 2026 00:00:05 +0100 Subject: [PATCH 2/4] chore(deps): update go-car/v2 to latest master - remove local replace directive for go-car/v2 - upgrade to v2.16.1-0.20260306172652-7d2f4aceb070 --- docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 ++-- go.mod | 4 +--- go.sum | 2 ++ test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index a0be7d15588..0e46deab170 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -99,7 +99,7 @@ require ( github.com/ipfs/go-peertaskqueue v0.8.3 // indirect github.com/ipfs/go-test v0.2.3 // indirect github.com/ipfs/go-unixfsnode v1.10.3 // indirect - github.com/ipld/go-car/v2 v2.16.0 // indirect + github.com/ipld/go-car/v2 v2.16.1-0.20260306172652-7d2f4aceb070 // indirect github.com/ipld/go-codec-dagpb v1.7.0 // indirect github.com/ipld/go-ipld-prime v0.22.0 // indirect github.com/ipshipyard/p2p-forge v0.7.0 // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 969cf1187db..5af55edb1ca 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -416,8 +416,8 @@ github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc= github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o= github.com/ipfs/go-unixfsnode v1.10.3 h1:c8sJjuGNkxXAQH75P+f5ngPda/9T+DrboVA0TcDGvGI= github.com/ipfs/go-unixfsnode v1.10.3/go.mod h1:2Jlc7DoEwr12W+7l8Hr6C7XF4NHST3gIkqSArLhGSxU= -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.20260306172652-7d2f4aceb070 h1:aUJhNVqQZV3PhxQX/BPAkurYnsxBmMLx2a7YPsgd5NU= +github.com/ipld/go-car/v2 v2.16.1-0.20260306172652-7d2f4aceb070/go.mod h1:eN3661vGFqfxzIfPLrCQIdKcY9iIOCgZON1CedVtvwk= 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= diff --git a/go.mod b/go.mod index 9c4a0877c68..92e528910f5 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/ipfs/go-metrics-prometheus v0.1.0 github.com/ipfs/go-test v0.2.3 github.com/ipfs/go-unixfsnode v1.10.3 - github.com/ipld/go-car/v2 v2.16.0 + github.com/ipld/go-car/v2 v2.16.1-0.20260306172652-7d2f4aceb070 github.com/ipld/go-codec-dagpb v1.7.0 github.com/ipld/go-ipld-prime v0.22.0 github.com/ipshipyard/p2p-forge v0.7.0 @@ -280,5 +280,3 @@ exclude ( github.com/ipfs/go-ipfs-cmds v2.0.1+incompatible github.com/libp2p/go-libp2p v6.0.23+incompatible ) - -replace github.com/ipld/go-car/v2 => ../go-car/v2 diff --git a/go.sum b/go.sum index 4bdc7d0fadb..bff2d977bf6 100644 --- a/go.sum +++ b/go.sum @@ -458,6 +458,8 @@ github.com/ipfs/go-test v0.2.3 h1:Z/jXNAReQFtCYyn7bsv/ZqUwS6E7iIcSpJ2CuzCvnrc= github.com/ipfs/go-test v0.2.3/go.mod h1:QW8vSKkwYvWFwIZQLGQXdkt9Ud76eQXRQ9Ao2H+cA1o= github.com/ipfs/go-unixfsnode v1.10.3 h1:c8sJjuGNkxXAQH75P+f5ngPda/9T+DrboVA0TcDGvGI= github.com/ipfs/go-unixfsnode v1.10.3/go.mod h1:2Jlc7DoEwr12W+7l8Hr6C7XF4NHST3gIkqSArLhGSxU= +github.com/ipld/go-car/v2 v2.16.1-0.20260306172652-7d2f4aceb070 h1:aUJhNVqQZV3PhxQX/BPAkurYnsxBmMLx2a7YPsgd5NU= +github.com/ipld/go-car/v2 v2.16.1-0.20260306172652-7d2f4aceb070/go.mod h1:eN3661vGFqfxzIfPLrCQIdKcY9iIOCgZON1CedVtvwk= 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= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index bb890a1c195..b562acf1222 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -149,7 +149,7 @@ require ( github.com/ipfs/go-metrics-interface v0.3.0 // indirect github.com/ipfs/go-unixfsnode v1.10.3 // indirect github.com/ipfs/kubo v0.31.0 // indirect - github.com/ipld/go-car/v2 v2.16.0 // indirect + github.com/ipld/go-car/v2 v2.16.1-0.20260306172652-7d2f4aceb070 // indirect github.com/ipld/go-codec-dagpb v1.7.0 // indirect github.com/ipld/go-ipld-prime v0.22.0 // indirect github.com/ipshipyard/p2p-forge v0.7.0 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 2c131ae4576..7c396f08660 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -496,8 +496,8 @@ github.com/ipfs/iptb v1.4.1 h1:faXd3TKGPswbHyZecqqg6UfbES7RDjTKQb+6VFPKDUo= github.com/ipfs/iptb v1.4.1/go.mod h1:nTsBMtVYFEu0FjC5DgrErnABm3OG9ruXkFXGJoTV5OA= github.com/ipfs/iptb-plugins v0.5.1 h1:11PNTNEt2+SFxjUcO5qpyCTXqDj6T8Tx9pU/G4ytCIQ= github.com/ipfs/iptb-plugins v0.5.1/go.mod h1:mscJAjRnu4g16QK6oUBn9RGpcp8ueJmLfmPxIG/At78= -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.20260306172652-7d2f4aceb070 h1:aUJhNVqQZV3PhxQX/BPAkurYnsxBmMLx2a7YPsgd5NU= +github.com/ipld/go-car/v2 v2.16.1-0.20260306172652-7d2f4aceb070/go.mod h1:eN3661vGFqfxzIfPLrCQIdKcY9iIOCgZON1CedVtvwk= 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.22.0 h1:YJhDhjEOvOYaqshd3b4atIWUoRg/rKrgmwCyUHwlbuY= From 7b99db60393fa8373d9b6ff3edde7416ee3f09af Mon Sep 17 00:00:00 2001 From: Chayan Das <01chayandas@gmail.com> Date: Tue, 10 Mar 2026 20:06:20 +0530 Subject: [PATCH 3/4] fix(dag): avoid CID round-trip in export and fix ci failure Signed-off-by: Chayan Das <01chayandas@gmail.com> --- core/commands/dag/dag.go | 4 ++-- core/commands/dag/export.go | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/core/commands/dag/dag.go b/core/commands/dag/dag.go index f5548bddcce..eab70aa3332 100644 --- a/core/commands/dag/dag.go +++ b/core/commands/dag/dag.go @@ -193,7 +193,7 @@ Note: currently present in the blockstore does not represent a complete DAG, pinning of that individual root will fail. - Use --local-only with --pin-roots=false when importing a partial CAR (e.g. from + Use --local-only and --pin-roots=false for partial CARs (e.g. from 'dag export --local-only'). FAST PROVIDE OPTIMIZATION: @@ -217,7 +217,7 @@ Specification of CAR formats: https://ipld.io/specs/transport/car/ }, Options: []cmds.Option{ cmds.BoolOption(pinRootsOptionName, "Pin optional roots listed in the .car headers after importing.").WithDefault(true), - cmds.BoolOption(localOnlyOptionName, "Import a partial CAR without pinning roots (for CARs from dag export --local-only)."), + cmds.BoolOption(localOnlyOptionName, "Import partial CAR without pinning roots (e.g. from dag export --local-only)."), 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"), diff --git a/core/commands/dag/export.go b/core/commands/dag/export.go index 9104ae74c57..4967b47ab41 100644 --- a/core/commands/dag/export.go +++ b/core/commands/dag/export.go @@ -63,14 +63,14 @@ func dagExport(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment if !ok { return nil, fmt.Errorf("unsupported link type: %T", lnk) } - data, err := ds.Get(lctx.Ctx, cl.Cid.String()) + block, err := ds.dag.Get(lctx.Ctx, cl.Cid) if err != nil { if ipld.IsNotFound(err) { return nil, traversal.SkipMe{} } return nil, fmt.Errorf("local block read failed: %w", err) } - return bytes.NewReader(data), nil + return bytes.NewReader(block.RawData()), nil } } else { lsys.SetReadStorage(ds) @@ -212,11 +212,12 @@ func (ds *dagStore) Has(ctx context.Context, key string) (bool, error) { } func cidFromBinString(key string) (cid.Cid, error) { - if l, k, err := cid.CidFromBytes([]byte(key)); err == nil && l == len(key) { - return k, nil + l, k, err := cid.CidFromBytes([]byte(key)) + if err != nil { + return cid.Undef, fmt.Errorf("dagStore: key was not a cid: %w", err) } - if c, decodeErr := cid.Decode(key); decodeErr == nil { - return c, nil + if l != len(key) { + return cid.Undef, fmt.Errorf("dagStore: key was not a cid: had %d bytes leftover", len(key)-l) } - return cid.Undef, fmt.Errorf("dagStore: key was not a cid (binary or base58)") + return k, nil } From 3b27eed8b0be97ed8e29781c0b56a165c9622bc3 Mon Sep 17 00:00:00 2001 From: Chayan Das <01chayandas@gmail.com> Date: Wed, 11 Mar 2026 12:08:14 +0530 Subject: [PATCH 4/4] dag: add validation and tests for --local-only flag Signed-off-by: Chayan Das <01chayandas@gmail.com> --- test/cli/dag_test.go | 155 +++++++++++++++++++++++++++++++++++++++ test/cli/harness/ipfs.go | 8 +- 2 files changed, 160 insertions(+), 3 deletions(-) diff --git a/test/cli/dag_test.go b/test/cli/dag_test.go index 38457318a0c..a834767ba20 100644 --- a/test/cli/dag_test.go +++ b/test/cli/dag_test.go @@ -1,9 +1,14 @@ package cli import ( + "context" "encoding/json" + "fmt" "io" "os" + "os/exec" + "path/filepath" + "strings" "testing" "time" @@ -305,3 +310,153 @@ func TestDagImportFastProvide(t *testing.T) { require.Contains(t, daemonLog, "fast-provide-root: skipped") }) } + +// dagRefs returns root plus recursive ref CIDs from "ipfs refs -r --unique root". +func dagRefs(node *harness.Node, root string) []string { + refsRes := node.IPFS("refs", "-r", "--unique", root) + refs := []string{root} + for _, line := range testutils.SplitLines(strings.TrimSpace(refsRes.Stdout.String())) { + if line != "" { + refs = append(refs, line) + } + } + return refs +} + +func parseImportedBlockCount(stdout string) int { + var n int + for _, line := range testutils.SplitLines(stdout) { + if _, err := fmt.Sscanf(line, "Imported %d blocks", &n); err == nil { + return n + } + } + return 0 +} + +func TestDagExportLocalOnly(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init().StartDaemon() + defer node.StopDaemon() + + root := node.IPFSAddDeterministic("300KiB", "dag-export-local-only", "--raw-leaves") + refs := dagRefs(node, root) + require.GreaterOrEqual(t, len(refs), 2, "need at least root and one child block") + + fullCarPath := filepath.Join(node.Dir, "full.car") + require.NoError(t, node.IPFSDagExport(root, fullCarPath)) + require.Equal(t, 0, node.RunIPFS("pin", "rm", root).ExitCode()) + require.Equal(t, 0, node.RunIPFS("block", "rm", refs[1]).ExitCode()) + + // Export --offline should fail; discard output (no file needed). + res := node.Runner.Run(harness.RunRequest{ + Path: node.IPFSBin, + Args: []string{"dag", "export", "--offline", root}, + CmdOpts: []harness.CmdOpt{harness.RunWithStdout(io.Discard)}, + }) + require.NotEqual(t, 0, res.ExitCode(), "export --offline without --local-only should fail when a block is missing") + require.Contains(t, res.Stderr.String(), "block was not found locally") + + partialCarPath := filepath.Join(node.Dir, "partial.car") + require.NoError(t, node.IPFSDagExport(root, partialCarPath, "--local-only", "--offline")) + + nodeFull := harness.NewT(t).NewNode().Init().StartDaemon() + defer nodeFull.StopDaemon() + fullCAR, err := os.Open(fullCarPath) + require.NoError(t, err) + defer fullCAR.Close() + fullRes := nodeFull.Runner.Run(harness.RunRequest{ + Path: nodeFull.IPFSBin, + Args: []string{"dag", "import", "--pin-roots=false", "--stats"}, + CmdOpts: []harness.CmdOpt{harness.RunWithStdin(fullCAR)}, + }) + require.Equal(t, 0, fullRes.ExitCode()) + fullCount := parseImportedBlockCount(fullRes.Stdout.String()) + require.Greater(t, fullCount, 0, "expected 'Imported N blocks' in output: %s", fullRes.Stdout.String()) + + nodePartial := harness.NewT(t).NewNode().Init().StartDaemon() + defer nodePartial.StopDaemon() + partialCAR, err := os.Open(partialCarPath) + require.NoError(t, err) + defer partialCAR.Close() + partialRes := nodePartial.Runner.Run(harness.RunRequest{ + Path: nodePartial.IPFSBin, + Args: []string{"dag", "import", "--pin-roots=false", "--stats"}, + CmdOpts: []harness.CmdOpt{harness.RunWithStdin(partialCAR)}, + }) + require.Equal(t, 0, partialRes.ExitCode()) + partialCount := parseImportedBlockCount(partialRes.Stdout.String()) + require.Greater(t, partialCount, 0, "expected 'Imported N blocks' in output: %s", partialRes.Stdout.String()) + + require.Less(t, partialCount, fullCount, "partial CAR should have fewer blocks than full DAG") +} + +func TestDagExportLocalOnlyRequiresOffline(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init().StartDaemon() + defer node.StopDaemon() + + root := node.IPFSAddDeterministic("300KiB", "dag-local-only-requires-offline", "--raw-leaves") + refs := dagRefs(node, root) + + require.GreaterOrEqual(t, len(refs), 2) + require.Equal(t, 0, node.RunIPFS("pin", "rm", root).ExitCode()) + require.Equal(t, 0, node.RunIPFS("block", "rm", refs[1]).ExitCode()) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, node.IPFSBin, "dag", "export", "--local-only", root) + cmd.Env = append(os.Environ(), "IPFS_PATH="+node.Dir) + cmd.Stdout = io.Discard + + err := cmd.Run() + + require.Error(t, err) // command should fail +} + +func TestDagImportPartialCAR(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init().StartDaemon() + defer node.StopDaemon() + + root := node.IPFSAddDeterministic("300KiB", "dag-import-partial", "--raw-leaves") + refs := dagRefs(node, root) + require.GreaterOrEqual(t, len(refs), 2) + + require.Equal(t, 0, node.RunIPFS("pin", "rm", root).ExitCode()) + require.Equal(t, 0, node.RunIPFS("block", "rm", refs[1]).ExitCode()) + + partialCarPath := filepath.Join(node.Dir, "partial.car") + require.NoError(t, node.IPFSDagExport(root, partialCarPath, "--local-only", "--offline")) + + imp := harness.NewT(t).NewNode().Init().StartDaemon() + defer imp.StopDaemon() + partialCAR, err := os.Open(partialCarPath) + require.NoError(t, err) + defer partialCAR.Close() + require.NoError(t, imp.IPFSDagImport(partialCAR, root)) +} +func TestDagImportLocalOnlyPinRootsConflict(t *testing.T) { + t.Parallel() + node := harness.NewT(t).NewNode().Init().StartDaemon() + defer node.StopDaemon() + + r, err := os.Open(fixtureFile) + require.NoError(t, err) + defer r.Close() + + res := node.Runner.Run(harness.RunRequest{ + Path: node.IPFSBin, + Args: []string{"dag", "import", "--local-only", "--pin-roots"}, + CmdOpts: []harness.CmdOpt{harness.RunWithStdin(r)}, + }) + + require.Equal(t, 1, res.ExitCode()) + require.Error(t, res.Err) + + errOutput := res.Stderr.String() + + require.Contains(t, errOutput, "cannot pass both") + require.Contains(t, errOutput, "pin-roots") + require.Contains(t, errOutput, "local-only") +} diff --git a/test/cli/harness/ipfs.go b/test/cli/harness/ipfs.go index d7470b4e764..637b316867b 100644 --- a/test/cli/harness/ipfs.go +++ b/test/cli/harness/ipfs.go @@ -162,17 +162,19 @@ func (n *Node) IPFSDagImport(content io.Reader, cid string, args ...string) erro } // IPFSDagExport exports a DAG rooted at cid to a CAR file at carPath. -func (n *Node) IPFSDagExport(cid string, carPath string) error { - log.Debugf("node %d dag export of %s to %q", n.ID, cid, carPath) +func (n *Node) IPFSDagExport(cid string, carPath string, args ...string) error { + log.Debugf("node %d dag export of %s to %q with args: %v", n.ID, cid, carPath, args) car, err := os.Create(carPath) if err != nil { return err } defer car.Close() + fullArgs := append([]string{"dag", "export"}, args...) + fullArgs = append(fullArgs, cid) res := n.Runner.MustRun(RunRequest{ Path: n.IPFSBin, - Args: []string{"dag", "export", cid}, + Args: fullArgs, CmdOpts: []CmdOpt{RunWithStdout(car)}, }) return res.Err