From 8f39b1a9a9ccb1ac681ff876e8e135c9189e0572 Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Tue, 16 May 2023 09:25:43 -0400 Subject: [PATCH] feat(gateway): implement IPIP-402 extensions for gateway CAR requests --- gateway/blocks_gateway.go | 368 +++++++++++++++++++++++++++++++++++--- gateway/gateway.go | 30 +++- gateway/gateway_test.go | 4 +- gateway/handler_car.go | 85 ++++++++- gateway/handler_test.go | 6 +- gateway/metrics.go | 6 +- 6 files changed, 457 insertions(+), 42 deletions(-) diff --git a/gateway/blocks_gateway.go b/gateway/blocks_gateway.go index 1b47525c3..a65b26951 100644 --- a/gateway/blocks_gateway.go +++ b/gateway/blocks_gateway.go @@ -1,6 +1,7 @@ package gateway import ( + "bytes" "context" "errors" "fmt" @@ -15,9 +16,11 @@ import ( blockstore "github.com/ipfs/boxo/blockstore" nsopts "github.com/ipfs/boxo/coreiface/options/namesys" ifacepath "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/fetcher" bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice" "github.com/ipfs/boxo/files" - "github.com/ipfs/boxo/ipld/car" + carv2 "github.com/ipfs/boxo/ipld/car/v2" + "github.com/ipfs/boxo/ipld/car/v2/storage" "github.com/ipfs/boxo/ipld/merkledag" ufile "github.com/ipfs/boxo/ipld/unixfs/file" uio "github.com/ipfs/boxo/ipld/unixfs/io" @@ -29,10 +32,15 @@ import ( "github.com/ipfs/go-cid" format "github.com/ipfs/go-ipld-format" "github.com/ipfs/go-unixfsnode" + "github.com/ipfs/go-unixfsnode/data" dagpb "github.com/ipld/go-codec-dagpb" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/schema" + "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" routinghelpers "github.com/libp2p/go-libp2p-routing-helpers" "github.com/libp2p/go-libp2p/core/peer" @@ -223,38 +231,208 @@ func (api *BlocksGateway) Head(ctx context.Context, path ImmutablePath) (Content return md, fileNode, nil } -func (api *BlocksGateway) GetCAR(ctx context.Context, path ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { - // Same go-car settings as dag.export command - store := dagStore{api: api, ctx: ctx} +func (api *BlocksGateway) GetCAR(ctx context.Context, p ImmutablePath, params CarParams) (io.ReadCloser, <-chan error, error) { + contentPathStr := p.String() + if contentPathStr[0:6] != "/ipfs/" { + return nil, nil, fmt.Errorf("path does not have /ipfs/ prefix") + } + firstSegment, _, _ := strings.Cut(contentPathStr[6:], "/") + rootCid, err := cid.Decode(firstSegment) + if err != nil { + return nil, nil, err + } - // TODO: When switching to exposing path blocks we'll want to add these as well - roots, lastSeg, err := api.getPathRoots(ctx, path) + r, w := io.Pipe() + cw, err := storage.NewWritable(w, []cid.Cid{rootCid}, carv2.WriteAsCarV1(true)) if err != nil { - return ContentPathMetadata{}, nil, nil, err + return nil, nil, err } - md := ContentPathMetadata{ - PathSegmentRoots: roots, - LastSegment: lastSeg, + blockGetter := merkledag.NewDAGService(api.blockService).Session(ctx) + + blockGetter = &nodeGetterToCarExporer{ + ng: blockGetter, + cw: cw, } - rootCid := lastSeg.Cid() + // Setup the UnixFS resolver. + f := newNodeGetterFetcherSingleUseFactory(ctx, blockGetter) + pathResolver := resolver.NewBasicResolver(f) - // TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769 - // TODO: this is very slow if blocks are remote due to linear traversal. Do we need deterministic traversals here? - dag := car.Dag{Root: rootCid, Selector: selectorparse.CommonSelector_ExploreAllRecursively} - c := car.NewSelectiveCar(ctx, store, []car.Dag{dag}, car.TraverseLinksOnlyOnce()) - r, w := io.Pipe() + lsys := cidlink.DefaultLinkSystem() + unixfsnode.AddUnixFSReificationToLinkSystem(&lsys) errCh := make(chan error, 1) go func() { - carWriteErr := c.Write(w) + // TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769 + // TODO: this is very slow if blocks are remote due to linear traversal. Do we need deterministic traversals here? + carWriteErr := walkGatewaySimpleSelector(ctx, ipfspath.Path(contentPathStr), params, &lsys, pathResolver) pipeCloseErr := w.Close() errCh <- multierr.Combine(carWriteErr, pipeCloseErr) close(errCh) }() - return md, r, errCh, nil + return r, errCh, nil +} + +// walkGatewaySimpleSelector walks the subgraph described by the path and terminal element parameters +func walkGatewaySimpleSelector(ctx context.Context, p ipfspath.Path, params CarParams, lsys *ipld.LinkSystem, pathResolver resolver.Resolver) error { + // First resolve the path since we always need to + lastCid, remainder, err := pathResolver.ResolveToLastNode(ctx, p) + if err != nil { + return err + } + + // If we just need to resolve the path then we're done + if params.Scope == dagScopeBlock { + return nil + } + + // Note: as an implementation detail this is currently complicated + + lctx := ipld.LinkContext{Ctx: ctx} + pathTerminalCidLink := cidlink.Link{Cid: lastCid} + + // If we're asking for everything then give it + if params.Scope == dagScopeAll { + lastCidNode, err := lsys.Load(lctx, pathTerminalCidLink, basicnode.Prototype.Any) + if err != nil { + return err + } + + sel, err := selector.ParseSelector(selectorparse.CommonSelector_ExploreAllRecursively) + if err != nil { + return err + } + + if err := traversal.WalkMatching(lastCidNode, sel, func(progress traversal.Progress, node datamodel.Node) error { + return nil + }); err != nil { + return err + } + return nil + } + + // Check if the terminal node is UnixFS or not + + // Since we need more of the graph load it to figure out what we have + pc := dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) { + if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok { + return tlnkNd.LinkTargetNodePrototype(), nil + } + return basicnode.Prototype.Any, nil + }) + + np, err := pc(pathTerminalCidLink, lctx) + if err != nil { + return err + } + + lastCidNode, err := lsys.Load(lctx, pathTerminalCidLink, np) + if err != nil { + return err + } + + if pbn, ok := lastCidNode.(dagpb.PBNode); !ok { + // If it's not valid dag-pb then we're done + return nil + } else if len(remainder) > 0 { + // If we're trying to path into dag-pb node that's invalid and we're done + return nil + } else if !pbn.FieldData().Exists() { + // If it's not valid UnixFS then we're done + return nil + } else if unixfsFieldData, decodeErr := data.DecodeUnixFSData(pbn.Data.Must().Bytes()); decodeErr != nil { + // If it's not valid dag-pb and UnixFS then we're done + return nil + } else { + switch unixfsFieldData.FieldDataType().Int() { + case data.Data_Directory, data.Data_Symlink: + // These types are non-recursive so we're done + return nil + case data.Data_Raw, data.Data_Metadata: + // TODO: Is Data_Raw actually different from Data_File or is that a fiction? + // TODO: Deal with UnixFS Metadata being handled differently in boxo/ipld/unixfs than go-unixfsnode + // Not sure what to do here so just returning + return nil + case data.Data_HAMTShard: + // Return all elements in the map + _, err := lsys.KnownReifiers["unixfs-preload"](lctx, lastCidNode, lsys) + if err != nil { + return err + } + return nil + case data.Data_File: + nd, err := unixfsnode.Reify(lctx, lastCidNode, lsys) + if err != nil { + return err + } + + fnd, ok := nd.(datamodel.LargeBytesNode) + if !ok { + return fmt.Errorf("could not process file since it did not present as large bytes") + } + f, err := fnd.AsLargeBytes() + if err != nil { + return err + } + + if params.Range == nil { + _, err := io.Copy(io.Discard, f) + if err != nil { + return err + } + } + + entityRange := *params.Range + from := entityRange.From + + // If we're starting to read based on the end of the file, find out where that is + var fileLength int64 + foundFileLength := false + if entityRange.From < 0 { + fileLength, err = f.Seek(0, io.SeekEnd) + if err != nil { + return err + } + from = fileLength - entityRange.From + foundFileLength = true + } + + // If we're reading until the end of the file then do it + if entityRange.To == nil { + if _, err := f.Seek(from, io.SeekStart); err != nil { + return err + } + _, err = io.Copy(io.Discard, f) + return err + } + + to := *entityRange.To + if (*entityRange.To) < 0 && !foundFileLength { + fileLength, err = f.Seek(0, io.SeekEnd) + if err != nil { + return err + } + to = fileLength - *entityRange.To + foundFileLength = true + } + + numToRead := to - from + if numToRead < 0 { + return fmt.Errorf("tried to read less than zero bytes") + } + + if _, err := f.Seek(from, io.SeekStart); err != nil { + return err + } + _, err = io.CopyN(io.Discard, f, numToRead) + return err + default: + // Not a supported type, so we're done + return nil + } + } } func (api *BlocksGateway) getNode(ctx context.Context, path ImmutablePath) (ContentPathMetadata, format.Node, error) { @@ -326,16 +504,6 @@ func (api *BlocksGateway) getPathRoots(ctx context.Context, contentPath Immutabl return pathRoots, lastPath, nil } -// FIXME(@Jorropo): https://github.com/ipld/go-car/issues/315 -type dagStore struct { - api *BlocksGateway - ctx context.Context -} - -func (ds dagStore) Get(_ context.Context, c cid.Cid) (blocks.Block, error) { - return ds.api.blockService.GetBlock(ds.ctx, c) -} - func (api *BlocksGateway) ResolveMutable(ctx context.Context, p ifacepath.Path) (ImmutablePath, error) { err := p.IsValid() if err != nil { @@ -453,3 +621,147 @@ func (api *BlocksGateway) resolvePath(ctx context.Context, p ifacepath.Path) (if return ifacepath.NewResolvedPath(ipath, node, root, gopath.Join(rest...)), nil } + +type nodeGetterToCarExporer struct { + ng format.NodeGetter + cw storage.WritableCar +} + +func (n *nodeGetterToCarExporer) Get(ctx context.Context, c cid.Cid) (format.Node, error) { + nd, err := n.ng.Get(ctx, c) + if err != nil { + return nil, err + } + + if err := n.trySendBlock(ctx, nd); err != nil { + return nil, err + } + + return nd, nil +} + +func (n *nodeGetterToCarExporer) GetMany(ctx context.Context, cids []cid.Cid) <-chan *format.NodeOption { + ndCh := n.ng.GetMany(ctx, cids) + outCh := make(chan *format.NodeOption) + go func() { + defer close(outCh) + for nd := range ndCh { + if nd.Err == nil { + if err := n.trySendBlock(ctx, nd.Node); err != nil { + select { + case outCh <- &format.NodeOption{Err: err}: + case <-ctx.Done(): + } + return + } + select { + case outCh <- nd: + case <-ctx.Done(): + } + } + } + }() + return outCh +} + +func (n *nodeGetterToCarExporer) trySendBlock(ctx context.Context, block blocks.Block) error { + return n.cw.Put(ctx, block.Cid().KeyString(), block.RawData()) +} + +var _ format.NodeGetter = (*nodeGetterToCarExporer)(nil) + +type nodeGetterFetcherSingleUseFactory struct { + linkSystem ipld.LinkSystem + protoChooser traversal.LinkTargetNodePrototypeChooser +} + +func newNodeGetterFetcherSingleUseFactory(ctx context.Context, ng format.NodeGetter) *nodeGetterFetcherSingleUseFactory { + ls := cidlink.DefaultLinkSystem() + ls.TrustedStorage = true + ls.StorageReadOpener = blockOpener(ctx, ng) + ls.NodeReifier = unixfsnode.Reify + + pc := dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) { + if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok { + return tlnkNd.LinkTargetNodePrototype(), nil + } + return basicnode.Prototype.Any, nil + }) + + return &nodeGetterFetcherSingleUseFactory{ls, pc} +} + +func (n *nodeGetterFetcherSingleUseFactory) NewSession(ctx context.Context) fetcher.Fetcher { + return n +} + +func (n *nodeGetterFetcherSingleUseFactory) NodeMatching(ctx context.Context, root ipld.Node, selector ipld.Node, cb fetcher.FetchCallback) error { + return n.nodeMatching(ctx, n.blankProgress(ctx), root, selector, cb) +} + +func (n *nodeGetterFetcherSingleUseFactory) BlockOfType(ctx context.Context, link ipld.Link, nodePrototype ipld.NodePrototype) (ipld.Node, error) { + return n.linkSystem.Load(ipld.LinkContext{}, link, nodePrototype) +} + +func (n *nodeGetterFetcherSingleUseFactory) BlockMatchingOfType(ctx context.Context, root ipld.Link, selector ipld.Node, nodePrototype ipld.NodePrototype, cb fetcher.FetchCallback) error { + // retrieve first node + prototype, err := n.PrototypeFromLink(root) + if err != nil { + return err + } + node, err := n.BlockOfType(ctx, root, prototype) + if err != nil { + return err + } + + progress := n.blankProgress(ctx) + progress.LastBlock.Link = root + return n.nodeMatching(ctx, progress, node, selector, cb) +} + +func (n *nodeGetterFetcherSingleUseFactory) PrototypeFromLink(lnk ipld.Link) (ipld.NodePrototype, error) { + return n.protoChooser(lnk, ipld.LinkContext{}) +} + +func (n *nodeGetterFetcherSingleUseFactory) nodeMatching(ctx context.Context, initialProgress traversal.Progress, node ipld.Node, match ipld.Node, cb fetcher.FetchCallback) error { + matchSelector, err := selector.ParseSelector(match) + if err != nil { + return err + } + return initialProgress.WalkMatching(node, matchSelector, func(prog traversal.Progress, n ipld.Node) error { + return cb(fetcher.FetchResult{ + Node: n, + Path: prog.Path, + LastBlockPath: prog.LastBlock.Path, + LastBlockLink: prog.LastBlock.Link, + }) + }) +} + +func (n *nodeGetterFetcherSingleUseFactory) blankProgress(ctx context.Context) traversal.Progress { + return traversal.Progress{ + Cfg: &traversal.Config{ + LinkSystem: n.linkSystem, + LinkTargetNodePrototypeChooser: n.protoChooser, + }, + } +} + +func blockOpener(ctx context.Context, ng format.NodeGetter) ipld.BlockReadOpener { + return func(_ ipld.LinkContext, lnk ipld.Link) (io.Reader, error) { + cidLink, ok := lnk.(cidlink.Link) + if !ok { + return nil, fmt.Errorf("invalid link type for loading: %v", lnk) + } + + blk, err := ng.Get(ctx, cidLink.Cid) + if err != nil { + return nil, err + } + + return bytes.NewReader(blk.RawData()), nil + } +} + +var _ fetcher.Fetcher = (*nodeGetterFetcherSingleUseFactory)(nil) +var _ fetcher.Factory = (*nodeGetterFetcherSingleUseFactory)(nil) diff --git a/gateway/gateway.go b/gateway/gateway.go index b6f33da64..250a9eec6 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -48,6 +48,34 @@ func (i ImmutablePath) IsValid() error { var _ path.Path = (*ImmutablePath)(nil) +type CarParams struct { + Range *DagEntityByteRange + Scope DagScope +} + +// DagEntityByteRange describes a range request within a UnixFS file. +// From and To mostly follow [HTTP Byte Range] Request semantics. +// From >= 0 and To = nil: Get the file (From, Length) +// From >= 0 and To >= 0: Get the range (From, To) +// From >= 0 and To <0: Get the range (From, Length - To) +// From < 0 and To = nil: Get the file (Length - From, Length) +// From < 0 and To >= 0: Get the range (Length - From, To) +// From < 0 and To <0: Get the range (Length - From, Length - To) +// +// [HTTP Byte Range]: https://httpwg.org/specs/rfc9110.html#rfc.section.14.1.2 +type DagEntityByteRange struct { + From int64 + To *int64 +} + +type DagScope string + +const ( + dagScopeAll DagScope = "all" + dagScopeEntity DagScope = "entity" + dagScopeBlock DagScope = "block" +) + type ContentPathMetadata struct { PathSegmentRoots []cid.Cid LastSegment path.Resolved @@ -131,7 +159,7 @@ type IPFSBackend interface { // that may contain a single error for if any errors occur during the streaming. If there was an initial error the // error channel is nil // TODO: Make this function signature better - GetCAR(context.Context, ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) + GetCAR(context.Context, ImmutablePath, CarParams) (io.ReadCloser, <-chan error, error) // IsCached returns whether or not the path exists locally. IsCached(context.Context, path.Path) bool diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index 7eada3a1a..eb02ce0ef 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -123,8 +123,8 @@ func (api *mockAPI) Head(ctx context.Context, immutablePath ImmutablePath) (Cont return api.gw.Head(ctx, immutablePath) } -func (api *mockAPI) GetCAR(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { - return api.gw.GetCAR(ctx, immutablePath) +func (api *mockAPI) GetCAR(ctx context.Context, immutablePath ImmutablePath, params CarParams) (io.ReadCloser, <-chan error, error) { + return api.gw.GetCAR(ctx, immutablePath, params) } func (api *mockAPI) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, error) { diff --git a/gateway/handler_car.go b/gateway/handler_car.go index beb17396b..4217f3b5e 100644 --- a/gateway/handler_car.go +++ b/gateway/handler_car.go @@ -5,14 +5,21 @@ import ( "fmt" "io" "net/http" + "strconv" + "strings" "time" ipath "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/go-cid" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/multierr" ) +const carRangeBytesKey = "entity-bytes" +const carTerminalElementTypeKey = "dag-scope" + // serveCAR returns a CAR stream for specific DAG+selector func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ImmutablePath, contentPath ipath.Path, carVersion string, begin time.Time) bool { ctx, span := spanTrace(ctx, "Handler.ServeCAR", trace.WithAttributes(attribute.String("path", imPath.String()))) @@ -30,18 +37,50 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R return false } - pathMetadata, carFile, errCh, err := i.api.GetCAR(ctx, imPath) + queryParams := r.URL.Query() + rangeStr, hasRange := queryParams.Get(carRangeBytesKey), queryParams.Has(carRangeBytesKey) + scopeStr, hasScope := queryParams.Get(carTerminalElementTypeKey), queryParams.Has(carTerminalElementTypeKey) + + params := CarParams{} + if hasRange { + rng, err := rangeStrToByteRange(rangeStr) + if err != nil { + webError(w, err, http.StatusBadRequest) + return false + } + params.Range = &rng + } + + if hasScope { + switch s := DagScope(scopeStr); s { + case dagScopeEntity, dagScopeAll, dagScopeBlock: + params.Scope = s + default: + err := fmt.Errorf("unsupported dag-scope %s", scopeStr) + webError(w, err, http.StatusBadRequest) + return false + } + } else { + params.Scope = dagScopeAll + } + + carFile, errCh, err := i.api.GetCAR(ctx, imPath, params) if !i.handleRequestErrors(w, contentPath, err) { return false } defer carFile.Close() - if err := i.setIpfsRootsHeader(w, pathMetadata); err != nil { - webRequestError(w, err) - return false + contentPathStr := imPath.String() + if contentPathStr[0:6] != "/ipfs/" { + webError(w, fmt.Errorf("path does not have /ipfs/ prefix"), http.StatusInternalServerError) + } + firstSegment, _, _ := strings.Cut(contentPathStr[6:], "/") + rootCid, err := cid.Decode(firstSegment) + if err != nil { + webError(w, err, http.StatusInternalServerError) } - rootCid := pathMetadata.LastSegment.Cid() + w.Header().Set("X-Ipfs-Roots", rootCid.String()) // Set Content-Disposition var name string @@ -93,3 +132,39 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) return true } + +func rangeStrToByteRange(rangeStr string) (DagEntityByteRange, error) { + rangeElems := strings.Split(rangeStr, ":") + if len(rangeElems) != 2 { + return DagEntityByteRange{}, fmt.Errorf("invalid range") + } + from, err := strconv.ParseInt(rangeElems[0], 10, 64) + if err != nil { + return DagEntityByteRange{}, err + } + + if rangeElems[1] == "*" { + return DagEntityByteRange{ + From: from, + To: nil, + }, nil + } + + to, err := strconv.ParseInt(rangeElems[1], 10, 64) + if err != nil { + return DagEntityByteRange{}, err + } + + if from >= 0 && to >= 0 && from >= to { + return DagEntityByteRange{}, fmt.Errorf("cannot have a range where 'to' is not after 'from'") + } + + if from < 0 && to < 0 && from >= to { + return DagEntityByteRange{}, fmt.Errorf("cannot have a range where 'to' is not after 'from'") + } + + return DagEntityByteRange{ + From: from, + To: &to, + }, nil +} diff --git a/gateway/handler_test.go b/gateway/handler_test.go index 1e97f6e9f..0d7394da3 100644 --- a/gateway/handler_test.go +++ b/gateway/handler_test.go @@ -61,8 +61,8 @@ func (api *errorMockAPI) Head(ctx context.Context, path ImmutablePath) (ContentP return ContentPathMetadata{}, nil, api.err } -func (api *errorMockAPI) GetCAR(ctx context.Context, path ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { - return ContentPathMetadata{}, nil, nil, api.err +func (api *errorMockAPI) GetCAR(ctx context.Context, path ImmutablePath, params CarParams) (io.ReadCloser, <-chan error, error) { + return nil, nil, api.err } func (api *errorMockAPI) ResolveMutable(ctx context.Context, path ipath.Path) (ImmutablePath, error) { @@ -173,7 +173,7 @@ func (api *panicMockAPI) Head(ctx context.Context, immutablePath ImmutablePath) panic("i am panicking") } -func (api *panicMockAPI) GetCAR(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { +func (api *panicMockAPI) GetCAR(ctx context.Context, immutablePath ImmutablePath, params CarParams) (io.ReadCloser, <-chan error, error) { panic("i am panicking") } diff --git a/gateway/metrics.go b/gateway/metrics.go index f0de3c7d3..13eac5dae 100644 --- a/gateway/metrics.go +++ b/gateway/metrics.go @@ -120,17 +120,17 @@ func (b *ipfsBackendWithMetrics) ResolvePath(ctx context.Context, path Immutable return md, err } -func (b *ipfsBackendWithMetrics) GetCAR(ctx context.Context, path ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { +func (b *ipfsBackendWithMetrics) GetCAR(ctx context.Context, path ImmutablePath, params CarParams) (io.ReadCloser, <-chan error, error) { begin := time.Now() name := "IPFSBackend.GetCAR" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) defer span.End() - md, rc, errCh, err := b.api.GetCAR(ctx, path) + rc, errCh, err := b.api.GetCAR(ctx, path, params) // TODO: handle errCh b.updateApiCallMetric(name, err, begin) - return md, rc, errCh, err + return rc, errCh, err } func (b *ipfsBackendWithMetrics) IsCached(ctx context.Context, path path.Path) bool {