From 1b1d8919f7791fab6cfbd05010af23d6ab26de22 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 25412 -> 25455 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 | 4 +- go.sum | 6 +- itests/deals_partial_retrieval_test.go | 221 ++++++++++++++++++++ markets/utils/selectors.go | 89 ++++++++ node/impl/client/client.go | 76 ++++++- 13 files changed, 425 insertions(+), 15 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 e502c5fc992..49da7e48b03 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -809,6 +809,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 b04180e1947..3628a1441f2 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" @@ -930,9 +931,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 06a131c8e0ae318768080cf3858c76636917db9d..27f986f40dcc89f79c18b0d40abe914de0cd09ff 100644 GIT binary patch literal 25455 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 literal 25412 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ȋITP{*`dm^;^+s)>02`|mEhXRiAT$#KYd%%+ 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 AoB#j- 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 a3693261adc..f177c600878 100644 --- a/documentation/en/api-v0-methods.md +++ b/documentation/en/api-v0-methods.md @@ -1469,6 +1469,7 @@ Inputs: "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" }, "Piece": null, + "DatamodelPathSelector": "Links/21/Hash/Links/42/Hash", "Size": 42, "FromLocalCAR": "string value", "Total": "0", @@ -1523,6 +1524,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 f03203314ec..397ff3c6ff9 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -1481,6 +1481,7 @@ Inputs: "/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4" }, "Piece": null, + "DatamodelPathSelector": "Links/21/Hash/Links/42/Hash", "Size": 42, "FromLocalCAR": "string value", "Total": "0", @@ -1535,6 +1536,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 43d53e0e759..6e31b360272 100644 --- a/go.mod +++ b/go.mod @@ -100,7 +100,9 @@ require ( github.com/ipfs/interface-go-ipfs-core v0.4.0 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-ipld-prime v0.12.0 + github.com/ipld/go-codec-dagpb v1.3.0 + github.com/ipld/go-ipld-prime v0.12.3 + 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 147eeff573e..d340edba123 100644 --- a/go.sum +++ b/go.sum @@ -815,13 +815,17 @@ 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= +github.com/ipld/go-ipld-prime v0.12.3 h1:furVobw7UBLQZwlEwfE26tYORy3PAK8VYSgZOSr3JMQ= +github.com/ipld/go-ipld-prime v0.12.3/go.mod h1:PaeLYq8k6dJLmDUSLrzkEpoGV4PEfe/1OtFN/eALOc8= github.com/ipld/go-ipld-prime-proto v0.0.0-20191113031812-e32bd156a1e5/go.mod h1:gcvzoEDBjwycpXt3LBE061wT9f46szXGHAmj9uoP6fU= 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.5/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..926d9ad28b2 --- /dev/null +++ b/itests/deals_partial_retrieval_test.go @@ -0,0 +1,221 @@ +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") + textSelectorNonLink = textselector.Expression("8/1/8/1/0/1") + textSelectorNonexistent = textselector.Expression("42") + expectedResult = "fil/1/storagepower" +) + +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, + )) + + // UGH if I do not sleep here, I get things like: + /* + retrieval failed: Retrieve failed: there is an active retrieval deal with peer 12D3KooWK9fB9a3HZ4PQLVmEQ6pweMMn5CAyKtumB71CPTnuBDi6 for payload CID bafy2bzacecnamqgqmifpluoeldx7zzglxcljo6oja4vrmtj7432rphldpdmm2 (retrieval deal ID 1631259332180384709, state DealStatusFinalizingBlockstore) - existing deal must be cancelled before starting a new retrieval deal: + github.com/filecoin-project/lotus/node/impl/client.(*API).ClientRetrieve + /home/circleci/project/node/impl/client/client.go:774 + */ + time.Sleep(time.Second) + } + } + + // ensure non-existent paths fail + require.EqualError( + t, + testGenesisRetrieval( + ctx, + client, + api.RetrievalOrder{ + FromLocalCAR: sourceCar, + Root: carRoot, + DatamodelPathSelector: &textSelectorNonexistent, + }, + &api.FileRef{}, + nil, + ), + fmt.Sprintf("retrieval failed: path selection '%s' does not match a node within %s", textSelectorNonexistent, carRoot), + ) + + // ensure non-boundary retrievals fail + require.EqualError( + t, + testGenesisRetrieval( + ctx, + client, + api.RetrievalOrder{ + FromLocalCAR: sourceCar, + Root: carRoot, + DatamodelPathSelector: &textSelectorNonLink, + }, + &api.FileRef{}, + nil, + ), + fmt.Sprintf("retrieval failed: error while locating partial retrieval sub-root: unsupported selection path '%s' does not correspond to a node boundary (a.k.a. CID link)", textSelectorNonLink), + ) +} + +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..7d40ba6dd85 --- /dev/null +++ b/markets/utils/selectors.go @@ -0,0 +1,89 @@ +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 { + + if optionalSelector == nil { + optionalSelector = selectorparse.CommonSelector_MatchAllRecursively + } + + 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..09f52280b0d 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,46 @@ 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 { + + if p.LastBlock.Path.String() != p.Path.String() { + return xerrors.Errorf("unsupported selection path '%s' does not correspond to a node boundary (a.k.a. CID link)", p.Path.String()) + } + + 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("error while locating 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))