From 08ee9dfc16e7fcd6c7592bc1ef7e1ba9d098fcc1 Mon Sep 17 00:00:00 2001 From: Peter Rabbitson Date: Fri, 4 Jun 2021 11:20:34 +0000 Subject: [PATCH] Expose basic text-based datamodel selector on retrieval Syntaxt of selection is located at https://pkg.go.dev/github.com/ipld/go-ipld-selector-text-lite#SelectorSpecFromPath Example use, assuming that: - The root of the deal is a plain dag-pb unixfs directory - The directory is not sharded - The user wants to retrieve the first entry in that directory lotus client retrieve --miner f0XXXXX --datamodel-path-selector 'Links/0/Hash' bafyROOTCID ~/output For a much more elaborate example see the top of ./itests/deals_partial_retrieval_test.go --- .circleci/config.yml | 5 + api/api_full.go | 8 +- api/docgen/docgen.go | 3 + build/openrpc/full.json.gz | Bin 25416 -> 25460 bytes cli/client.go | 9 + documentation/en/api-v0-methods.md | 2 + documentation/en/api-v1-unstable-methods.md | 2 + documentation/en/cli-lotus.md | 15 +- go.mod | 2 + go.sum | 3 + itests/deals_partial_retrieval_test.go | 177 ++++++++++++++++++++ markets/utils/selectors.go | 91 ++++++++++ node/impl/client/client.go | 70 +++++++- 13 files changed, 374 insertions(+), 13 deletions(-) create mode 100644 itests/deals_partial_retrieval_test.go create mode 100644 markets/utils/selectors.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 200792130cd..760b3eeb6e9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -835,6 +835,11 @@ workflows: suite: itest-deals_padding target: "./itests/deals_padding_test.go" + - test: + name: test-itest-deals_partial_retrieval + suite: itest-deals_partial_retrieval + target: "./itests/deals_partial_retrieval_test.go" + - test: name: test-itest-deals_power suite: itest-deals_power diff --git a/api/api_full.go b/api/api_full.go index 0649ececf1f..4376729d1d5 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -7,6 +7,7 @@ import ( "time" "github.com/ipfs/go-cid" + textselector "github.com/ipld/go-ipld-selector-text-lite" "github.com/libp2p/go-libp2p-core/peer" "github.com/filecoin-project/go-address" @@ -931,9 +932,10 @@ type MarketDeal struct { type RetrievalOrder struct { // TODO: make this less unixfs specific - Root cid.Cid - Piece *cid.Cid - Size uint64 + Root cid.Cid + Piece *cid.Cid + DatamodelPathSelector *textselector.Expression + Size uint64 FromLocalCAR string // if specified, get data from a local CARv2 file. // TODO: support offset diff --git a/api/docgen/docgen.go b/api/docgen/docgen.go index ce22fefd19d..5fb90dbe7f1 100644 --- a/api/docgen/docgen.go +++ b/api/docgen/docgen.go @@ -27,6 +27,7 @@ import ( filestore2 "github.com/filecoin-project/go-fil-markets/filestore" "github.com/filecoin-project/go-fil-markets/retrievalmarket" "github.com/filecoin-project/go-jsonrpc/auth" + textselector "github.com/ipld/go-ipld-selector-text-lite" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/crypto" @@ -90,6 +91,7 @@ func init() { addExample(&pid) storeIDExample := imports.ID(50) + textSelExample := textselector.Expression("Links/21/Hash/Links/42/Hash") addExample(bitfield.NewFromSet([]uint64{5})) addExample(abi.RegisteredSealProof_StackedDrg32GiBV1_1) @@ -124,6 +126,7 @@ func init() { addExample(&storeIDExample) addExample(retrievalmarket.ClientEventDealAccepted) addExample(retrievalmarket.DealStatusNew) + addExample(&textSelExample) addExample(network.ReachabilityPublic) addExample(build.NewestNetworkVersion) addExample(map[string]int{"name": 42}) diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index d0409cc040900badfaede9d07260618de119cf0e..ed8dd69a7bcba096c02f5427b5b77896fe298eb2 100644 GIT binary patch delta 18596 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%0D?+ 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 diff --git a/cli/client.go b/cli/client.go index 549589d64b1..daaf5f3fe44 100644 --- a/cli/client.go +++ b/cli/client.go @@ -26,6 +26,7 @@ import ( datatransfer "github.com/filecoin-project/go-data-transfer" "github.com/ipfs/go-cid" "github.com/ipfs/go-cidutil/cidenc" + textselector "github.com/ipld/go-ipld-selector-text-lite" "github.com/libp2p/go-libp2p-core/peer" "github.com/multiformats/go-multibase" "github.com/urfave/cli/v2" @@ -1047,6 +1048,10 @@ var clientRetrieveCmd = &cli.Command{ Name: "miner", Usage: "miner address for retrieval, if not present it'll use local discovery", }, + &cli.StringFlag{ + Name: "datamodel-path-selector", + Usage: "a rudimentary (DM-level-only) text-path selector, allowing for sub-selection within a deal", + }, &cli.StringFlag{ Name: "maxPrice", Usage: fmt.Sprintf("maximum price the client is willing to consider (default: %s FIL)", DefaultMaxRetrievePrice), @@ -1182,6 +1187,10 @@ var clientRetrieveCmd = &cli.Command{ IsCAR: cctx.Bool("car"), } + if sel := textselector.Expression(cctx.String("datamodel-path-selector")); sel != "" { + order.DatamodelPathSelector = &sel + } + updates, err := fapi.ClientRetrieveWithEvents(ctx, *order, ref) if err != nil { return xerrors.Errorf("error setting up retrieval: %w", err) diff --git a/documentation/en/api-v0-methods.md b/documentation/en/api-v0-methods.md index f5907f49445..fa7ff41897d 100644 --- a/documentation/en/api-v0-methods.md +++ b/documentation/en/api-v0-methods.md @@ -1467,6 +1467,7 @@ Inputs: "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" }, "Piece": null, + "DatamodelPathSelector": "Links/21/Hash/Links/42/Hash", "Size": 42, "FromLocalCAR": "string value", "Total": "0", @@ -1521,6 +1522,7 @@ Inputs: "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" }, "Piece": null, + "DatamodelPathSelector": "Links/21/Hash/Links/42/Hash", "Size": 42, "FromLocalCAR": "string value", "Total": "0", diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index e77e0c7bfa8..08733c90559 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -1531,6 +1531,7 @@ Inputs: "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" }, "Piece": null, + "DatamodelPathSelector": "Links/21/Hash/Links/42/Hash", "Size": 42, "FromLocalCAR": "string value", "Total": "0", @@ -1585,6 +1586,7 @@ Inputs: "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" }, "Piece": null, + "DatamodelPathSelector": "Links/21/Hash/Links/42/Hash", "Size": 42, "FromLocalCAR": "string value", "Total": "0", diff --git a/documentation/en/cli-lotus.md b/documentation/en/cli-lotus.md index 65f8b4a6a60..7423f11d601 100644 --- a/documentation/en/cli-lotus.md +++ b/documentation/en/cli-lotus.md @@ -544,13 +544,14 @@ CATEGORY: RETRIEVAL OPTIONS: - --from value address to send transactions from - --car export to a car file instead of a regular file (default: false) - --miner value miner address for retrieval, if not present it'll use local discovery - --maxPrice value maximum price the client is willing to consider (default: 0.01 FIL) - --pieceCid value require data to be retrieved from a specific Piece CID - --allow-local (default: false) - --help, -h show help (default: false) + --from value address to send transactions from + --car export to a car file instead of a regular file (default: false) + --miner value miner address for retrieval, if not present it'll use local discovery + --datamodel-path-selector value a rudimentary (DM-level-only) text-path selector, allowing for sub-selection within a deal + --maxPrice value maximum price the client is willing to consider (default: 0.01 FIL) + --pieceCid value require data to be retrieved from a specific Piece CID + --allow-local (default: false) + --help, -h show help (default: false) ``` diff --git a/go.mod b/go.mod index 45cbc17c962..61b658e5b82 100644 --- a/go.mod +++ b/go.mod @@ -99,7 +99,9 @@ require ( github.com/ipfs/interface-go-ipfs-core v0.2.3 github.com/ipld/go-car v0.3.1-0.20210601190600-f512dac51e8e github.com/ipld/go-car/v2 v2.0.3-0.20210811121346-c514a30114d7 + github.com/ipld/go-codec-dagpb v1.3.0 github.com/ipld/go-ipld-prime v0.12.0 + github.com/ipld/go-ipld-selector-text-lite v0.0.0-20210817134355-4c190a2bb825 github.com/kelseyhightower/envconfig v1.4.0 github.com/libp2p/go-buffer-pool v0.0.2 github.com/libp2p/go-eventbus v0.2.1 diff --git a/go.sum b/go.sum index 05de56ed2eb..a451ae74251 100644 --- a/go.sum +++ b/go.sum @@ -784,6 +784,7 @@ github.com/ipld/go-ipld-prime v0.0.2-0.20200428162820-8b59dc292b8e/go.mod h1:uVI github.com/ipld/go-ipld-prime v0.5.1-0.20200828233916-988837377a7f/go.mod h1:0xEgdD6MKbZ1vF0GC+YcR/C4SQCAlRuOjIJ2i0HxqzM= github.com/ipld/go-ipld-prime v0.5.1-0.20201021195245-109253e8a018/go.mod h1:0xEgdD6MKbZ1vF0GC+YcR/C4SQCAlRuOjIJ2i0HxqzM= github.com/ipld/go-ipld-prime v0.9.0/go.mod h1:KvBLMr4PX1gWptgkzRjVZCrLmSGcZCb/jioOQwCqZN8= +github.com/ipld/go-ipld-prime v0.10.0/go.mod h1:KvBLMr4PX1gWptgkzRjVZCrLmSGcZCb/jioOQwCqZN8= github.com/ipld/go-ipld-prime v0.11.0/go.mod h1:+WIAkokurHmZ/KwzDOMUuoeJgaRQktHtEaLglS3ZeV8= github.com/ipld/go-ipld-prime v0.12.0 h1:JapyKWTsJgmhrPI7hfx4V798c/RClr85sXfBZnH1VIw= github.com/ipld/go-ipld-prime v0.12.0/go.mod h1:hy8b93WleDMRKumOJnTIrr0MbbFbx9GD6Kzxa53Xppc= @@ -791,6 +792,8 @@ github.com/ipld/go-ipld-prime-proto v0.0.0-20191113031812-e32bd156a1e5/go.mod h1 github.com/ipld/go-ipld-prime-proto v0.0.0-20200428191222-c1ffdadc01e1/go.mod h1:OAV6xBmuTLsPZ+epzKkPB1e25FHk/vCtyatkdHcArLs= github.com/ipld/go-ipld-prime-proto v0.0.0-20200922192210-9a2bfd4440a6/go.mod h1:3pHYooM9Ea65jewRwrb2u5uHZCNkNTe9ABsVB+SrkH0= github.com/ipld/go-ipld-prime-proto v0.1.0/go.mod h1:11zp8f3sHVgIqtb/c9Kr5ZGqpnCLF1IVTNOez9TopzE= +github.com/ipld/go-ipld-selector-text-lite v0.0.0-20210817134355-4c190a2bb825 h1:sGlmVUuWEhuJpVsErFqCHWy9XTsIy511hZWRWI/Lc4I= +github.com/ipld/go-ipld-selector-text-lite v0.0.0-20210817134355-4c190a2bb825/go.mod h1:U2CQmFb+uWzfIEF3I1arrDa5rwtj00PrpiwwCO+k1RM= github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52 h1:QG4CGBqCeuBo6aZlGAamSkxWdgWfZGeE49eUOWJPA4c= github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52/go.mod h1:fdg+/X9Gg4AsAIzWpEHwnqd+QY3b7lajxyjE1m4hkq4= github.com/jackpal/gateway v1.0.4/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= diff --git a/itests/deals_partial_retrieval_test.go b/itests/deals_partial_retrieval_test.go new file mode 100644 index 00000000000..c86fd7bc9ca --- /dev/null +++ b/itests/deals_partial_retrieval_test.go @@ -0,0 +1,177 @@ +package itests + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/filecoin-project/go-fil-markets/storagemarket" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/actors/policy" + "github.com/filecoin-project/lotus/itests/kit" + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/ipld/go-car" + textselector "github.com/ipld/go-ipld-selector-text-lite" + "github.com/stretchr/testify/require" +) + +// use the mainnet carfile as text fixture: it will always be here +// https://dweb.link/ipfs/bafy2bzacecnamqgqmifpluoeldx7zzglxcljo6oja4vrmtj7432rphldpdmm2/8/1/8/1/0/1/0 +var ( + sourceCar = "../build/genesis/mainnet.car" + carRoot, _ = cid.Parse("bafy2bzacecnamqgqmifpluoeldx7zzglxcljo6oja4vrmtj7432rphldpdmm2") + carCommp, _ = cid.Parse("baga6ea4seaqmrivgzei3fmx5qxtppwankmtou6zvigyjaveu3z2zzwhysgzuina") + carPieceSize = abi.PaddedPieceSize(2097152) + textSelector = textselector.Expression("8/1/8/1/0/1/0") + expectedResult = "fil/1/storagepo wer" +) + +func TestPartialRetrieval(t *testing.T) { + + ctx := context.Background() + + policy.SetPreCommitChallengeDelay(2) + kit.EnableLargeSectors(t) + kit.QuietMiningLogs() + client, miner, ens := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.MockProofs(), kit.SectorSize(512<<20)) + dh := kit.NewDealHarness(t, client, miner, miner) + ens.InterconnectAll().BeginMining(50 * time.Millisecond) + + _, err := client.ClientImport(ctx, api.FileRef{Path: sourceCar, IsCAR: true}) + require.NoError(t, err) + + caddr, err := client.WalletDefaultAddress(ctx) + require.NoError(t, err) + + // first test retrieval from local car, then do an actual deal + for _, fullCycle := range []bool{false, true} { + + var retOrder api.RetrievalOrder + + if !fullCycle { + + retOrder.FromLocalCAR = sourceCar + retOrder.Root = carRoot + + } else { + + dp := dh.DefaultStartDealParams() + dp.Data = &storagemarket.DataRef{ + // FIXME: figure out how to do this with an online partial transfer + TransferType: storagemarket.TTManual, + Root: carRoot, + PieceCid: &carCommp, + PieceSize: carPieceSize.Unpadded(), + } + proposalCid := dh.StartDeal(ctx, dp) + + // Wait for the deal to reach StorageDealCheckForAcceptance on the client + cd, err := client.ClientGetDealInfo(ctx, *proposalCid) + require.NoError(t, err) + require.Eventually(t, func() bool { + cd, _ := client.ClientGetDealInfo(ctx, *proposalCid) + return cd.State == storagemarket.StorageDealCheckForAcceptance + }, 30*time.Second, 1*time.Second, "actual deal status is %s", storagemarket.DealStates[cd.State]) + + err = miner.DealsImportData(ctx, *proposalCid, sourceCar) + require.NoError(t, err) + + // Wait for the deal to be published, we should be able to start retrieval right away + dh.WaitDealPublished(ctx, proposalCid) + + offers, err := client.ClientFindData(ctx, carRoot, nil) + require.NoError(t, err) + require.NotEmpty(t, offers, "no offers") + + retOrder = offers[0].Order(caddr) + } + + retOrder.DatamodelPathSelector = &textSelector + + // test retrieval of either data or constructing a partial selective-car + for _, retrieveAsCar := range []bool{false, true} { + outFile, err := ioutil.TempFile(t.TempDir(), "ret-file") + require.NoError(t, err) + defer outFile.Close() //nolint:errcheck + + require.NoError(t, testGenesisRetrieval( + ctx, + client, + retOrder, + &api.FileRef{ + Path: outFile.Name(), + IsCAR: retrieveAsCar, + }, + outFile, + )) + } + } +} + +func testGenesisRetrieval(ctx context.Context, client *kit.TestFullNode, retOrder api.RetrievalOrder, retRef *api.FileRef, outFile *os.File) error { + + if retOrder.Total.Nil() { + retOrder.Total = big.Zero() + } + if retOrder.UnsealPrice.Nil() { + retOrder.UnsealPrice = big.Zero() + } + + err := client.ClientRetrieve(ctx, retOrder, retRef) + if err != nil { + return err + } + + var data []byte + if !retRef.IsCAR { + + data, err = io.ReadAll(outFile) + if err != nil { + return err + } + + } else { + + cr, err := car.NewCarReader(outFile) + if err != nil { + return err + } + + if len(cr.Header.Roots) != 1 { + return fmt.Errorf("expected a single root in result car, got %d", len(cr.Header.Roots)) + } else if cr.Header.Roots[0].String() != carRoot.String() { + return fmt.Errorf("expected root cid '%s', got '%s'", carRoot.String(), cr.Header.Roots[0].String()) + } + + blks := make([]blocks.Block, 0) + for { + b, err := cr.Next() + if err == io.EOF { + break + } else if err != nil { + return err + } + + blks = append(blks, b) + } + + if len(blks) != 3 { + return fmt.Errorf("expected a car file with 3 blocks, got one with %d instead", len(blks)) + } + + data = blks[2].RawData() + } + + if string(data) != expectedResult { + return fmt.Errorf("retrieved data mismatch: expected '%s' got '%s'", expectedResult, data) + } + + return nil +} diff --git a/markets/utils/selectors.go b/markets/utils/selectors.go new file mode 100644 index 00000000000..c15b68dba59 --- /dev/null +++ b/markets/utils/selectors.go @@ -0,0 +1,91 @@ +package utils + +import ( + "bytes" + "context" + "fmt" + "io" + + // must be imported to init() raw-codec support + _ "github.com/ipld/go-ipld-prime/codec/raw" + + "github.com/ipfs/go-cid" + mdagipld "github.com/ipfs/go-ipld-format" + dagpb "github.com/ipld/go-codec-dagpb" + "github.com/ipld/go-ipld-prime" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + basicnode "github.com/ipld/go-ipld-prime/node/basic" + "github.com/ipld/go-ipld-prime/traversal" + "github.com/ipld/go-ipld-prime/traversal/selector" + selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" +) + +func TraverseDag( + ctx context.Context, + ds mdagipld.DAGService, + startFrom cid.Cid, + optionalSelector ipld.Node, + visitCallback traversal.AdvVisitFn, +) error { + + var parsedSelector selector.Selector + if optionalSelector == nil { + parsedSelector = selectorparse.CommonSelector_MatchAllRecursively + } else { + var err error + parsedSelector, err = selector.ParseSelector(optionalSelector) + if err != nil { + return err + } + } + + // not sure what this is for TBH: we also provide ctx in &traversal.Config{} + linkContext := ipld.LinkContext{Ctx: ctx} + + // this is what allows us to understand dagpb + nodePrototypeChooser := dagpb.AddSupportToChooser( + func(ipld.Link, ipld.LinkContext) (ipld.NodePrototype, error) { + return basicnode.Prototype.Any, nil + }, + ) + + // this is how we implement GETs + linkSystem := cidlink.DefaultLinkSystem() + linkSystem.StorageReadOpener = func(lctx ipld.LinkContext, lnk ipld.Link) (io.Reader, error) { + cl, isCid := lnk.(cidlink.Link) + if !isCid { + return nil, fmt.Errorf("unexpected link type %#v", lnk) + } + + node, err := ds.Get(lctx.Ctx, cl.Cid) + if err != nil { + return nil, err + } + + return bytes.NewBuffer(node.RawData()), nil + } + + // this is how we pull the start node out of the DS + startLink := cidlink.Link{Cid: startFrom} + startNodePrototype, err := nodePrototypeChooser(startLink, linkContext) + if err != nil { + return err + } + startNode, err := linkSystem.Load( + linkContext, + startLink, + startNodePrototype, + ) + if err != nil { + return err + } + + // this is the actual execution, invoking the supplied callback + return traversal.Progress{ + Cfg: &traversal.Config{ + Ctx: ctx, + LinkSystem: linkSystem, + LinkTargetNodePrototypeChooser: nodePrototypeChooser, + }, + }.WalkAdv(startNode, parsedSelector, visitCallback) +} diff --git a/node/impl/client/client.go b/node/impl/client/client.go index 5f08f93cbc6..2971d887248 100644 --- a/node/impl/client/client.go +++ b/node/impl/client/client.go @@ -24,10 +24,15 @@ import ( "github.com/ipfs/go-cid" offline "github.com/ipfs/go-ipfs-exchange-offline" files "github.com/ipfs/go-ipfs-files" + logging "github.com/ipfs/go-log/v2" "github.com/ipfs/go-merkledag" + "github.com/ipld/go-ipld-prime" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" basicnode "github.com/ipld/go-ipld-prime/node/basic" + "github.com/ipld/go-ipld-prime/traversal" "github.com/ipld/go-ipld-prime/traversal/selector" "github.com/ipld/go-ipld-prime/traversal/selector/builder" + textselector "github.com/ipld/go-ipld-selector-text-lite" "github.com/libp2p/go-libp2p-core/host" "github.com/libp2p/go-libp2p-core/peer" "github.com/multiformats/go-multibase" @@ -68,6 +73,8 @@ import ( "github.com/filecoin-project/lotus/node/repo" ) +var log = logging.Logger("client") + var DefaultHashFunction = uint64(mh.BLAKE2B_MIN + 31) // 8 days ~= SealDuration + PreCommit + MaxProveCommitDuration + 8 hour buffer @@ -500,7 +507,7 @@ func (a *API) ClientImport(ctx context.Context, ref api.FileRef) (res *api.Impor } if ref.IsCAR { - // user gave us a CAR fil, use it as-is + // user gave us a CAR file, use it as-is // validate that it's either a carv1 or carv2, and has one root. f, err := os.Open(ref.Path) if err != nil { @@ -835,6 +842,29 @@ func (a *API) clientRetrieve(ctx context.Context, order api.RetrievalOrder, ref } sel := shared.AllSelector() + if order.DatamodelPathSelector != nil { + + ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) + + selspec, err := textselector.SelectorSpecFromPath( + + *order.DatamodelPathSelector, + + // URGH - this is a direct copy from https://github.com/filecoin-project/go-fil-markets/blob/v1.12.0/shared/selectors.go#L10-L16 + // Unable to use it because we need the SelectorSpec, and markets exposes just a reified node + ssb.ExploreRecursive( + selector.RecursionLimitNone(), + ssb.ExploreAll(ssb.ExploreRecursiveEdge()), + ), + ) + if err != nil { + finish(xerrors.Errorf("failed to parse text-selector '%s': %w", *order.DatamodelPathSelector, err)) + return + } + + sel = selspec.Node() + log.Infof("partial retrieval of datamodel-path-selector %s/*", *order.DatamodelPathSelector) + } // summary: // 1. if we're retrieving from an import, FromLocalCAR will be set. @@ -961,8 +991,8 @@ func (a *API) clientRetrieve(ctx context.Context, order api.RetrievalOrder, ref // Are we outputting a CAR? if ref.IsCAR { - // not IPFS - just extract the CARv1 from the CARv2 we stored the retrieval in - if !retrieveIntoIPFS { + // not IPFS and we do full selection - just extract the CARv1 from the CARv2 we stored the retrieval in + if !retrieveIntoIPFS && order.DatamodelPathSelector == nil { finish(carv2.ExtractV1File(carPath, ref.Path)) return } @@ -995,6 +1025,40 @@ func (a *API) clientRetrieve(ctx context.Context, order api.RetrievalOrder, ref ds := merkledag.NewDAGService(blockservice.New(retrievalBs, offline.Exchange(retrievalBs))) root := order.Root + // if we used a selector - need to find the sub-root the user actually wanted to retrieve + if order.DatamodelPathSelector != nil { + + var subRootFound bool + + // no err check - we just compiled this before starting, but now we do not wrap a `*` + selspec, _ := textselector.SelectorSpecFromPath(*order.DatamodelPathSelector, nil) //nolint:errcheck + if err := utils.TraverseDag( + ctx, + ds, + root, + selspec.Node(), + func(p traversal.Progress, n ipld.Node, r traversal.VisitReason) error { + if r == traversal.VisitReason_SelectionMatch { + cidLnk, castOK := p.LastBlock.Link.(cidlink.Link) + if !castOK { + return xerrors.Errorf("cidlink cast unexpectedly failed on '%s'", p.LastBlock.Link.String()) + } + root = cidLnk.Cid + subRootFound = true + } + return nil + }, + ); err != nil { + finish(xerrors.Errorf("Finding partial retrieval sub-root: %w", err)) + return + } + + if !subRootFound { + finish(xerrors.Errorf("Path selection '%s' does not match a node within %s", order.DatamodelPathSelector, root)) + return + } + } + nd, err := ds.Get(ctx, root) if err != nil { finish(xerrors.Errorf("ClientRetrieve: %w", err))