diff --git a/gateway/blocks_backend.go b/gateway/blocks_backend.go index b9663e63b..f7e760609 100644 --- a/gateway/blocks_backend.go +++ b/gateway/blocks_backend.go @@ -233,6 +233,24 @@ func (bb *BlocksBackend) Head(ctx context.Context, path ImmutablePath) (ContentP } func (bb *BlocksBackend) GetCAR(ctx context.Context, p ImmutablePath, params *CarParams) (ContentPathMetadata, io.ReadCloser, error) { + // Check if we support the request order. On unknown, change it to DFS. We change + // the parameter directly, which means that the caller can use the value to later construct + // the Content-Type header. + switch params.Order { + case DagOrderUnknown: + params.Order = DagOrderDFS + case DagOrderDFS: + // Do nothing + default: + return ContentPathMetadata{}, nil, fmt.Errorf("unsupported order: %s", params.Order) + } + + // Similarly, if params.Duplicates is not set, let's set it to false. + if params.Duplicates == nil { + v := false + params.Duplicates = &v + } + pathMetadata, err := bb.ResolvePath(ctx, p) if err != nil { return ContentPathMetadata{}, nil, err @@ -243,12 +261,6 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p ImmutablePath, params *Ca return ContentPathMetadata{}, nil, fmt.Errorf("path does not have /ipfs/ prefix") } - // If params.Duplicates is not set, let's set it to false. - if params.Duplicates == nil { - v := false - params.Duplicates = &v - } - r, w := io.Pipe() go func() { cw, err := storage.NewWritable( diff --git a/gateway/gateway.go b/gateway/gateway.go index 64a2047bc..50ec55882 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -287,7 +287,10 @@ type IPFSBackend interface { ResolvePath(context.Context, ImmutablePath) (ContentPathMetadata, error) // GetCAR returns a CAR file for the given immutable path. It returns an error - // if there was an issue before the CAR streaming begins. + // if there was an issue before the CAR streaming begins. If [CarParams.Duplicates] + // is nil, or if [CaraParams.Order] is Unknown, the implementer should change it + // such that the caller can form the response "Content-Type" header with the most + // amount of information. GetCAR(context.Context, ImmutablePath, *CarParams) (ContentPathMetadata, io.ReadCloser, error) // IsCached returns whether or not the path exists locally. diff --git a/gateway/handler_car.go b/gateway/handler_car.go index c4da2f0c3..7ca8fc957 100644 --- a/gateway/handler_car.go +++ b/gateway/handler_car.go @@ -68,7 +68,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R addCacheControlHeaders(w, r, rq.contentPath, rootCid, carResponseFormat) // Generate the CAR Etag. - etag := getCarEtag(rq.immutablePath, *params, rootCid) + etag := getCarEtag(rq.immutablePath, params, rootCid) w.Header().Set("Etag", etag) // Terminate early if Etag matches. We cannot rely on handleIfNoneMatch since @@ -90,7 +90,7 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R // sub-DAGs and IPLD selectors: https://github.com/ipfs/go-ipfs/issues/8769 w.Header().Set("Accept-Ranges", "none") - w.Header().Set("Content-Type", getContentTypeFromCarParams(*params)) + w.Header().Set("Content-Type", getContentTypeFromCarParams(params)) w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^) _, copyErr := io.Copy(w, carFile) @@ -113,29 +113,6 @@ func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.R return true } -func getContentTypeFromCarParams(params CarParams) string { - h := strings.Builder{} - h.WriteString(carResponseFormat) - h.WriteString("; version=1; order=") - - if params.Order != "" { - h.WriteString(string(params.Order)) - } else { - h.WriteString(string(DagOrderUnknown)) - } - - if params.Duplicates != nil { - h.WriteString("; dups=") - if *params.Duplicates { - h.WriteString("y") - } else { - h.WriteString("n") - } - } - - return h.String() -} - func getCarParams(r *http.Request, formatParams map[string]string) (*CarParams, error) { queryParams := r.URL.Query() rangeStr, hasRange := queryParams.Get(carRangeBytesKey), queryParams.Has(carRangeBytesKey) @@ -188,6 +165,29 @@ func getCarParams(r *http.Request, formatParams map[string]string) (*CarParams, return ¶ms, nil } +func getContentTypeFromCarParams(params *CarParams) string { + h := strings.Builder{} + h.WriteString(carResponseFormat) + h.WriteString("; version=1; order=") + + if params.Order != "" { + h.WriteString(string(params.Order)) + } else { + h.WriteString(string(DagOrderUnknown)) + } + + if params.Duplicates != nil { + h.WriteString("; dups=") + if *params.Duplicates { + h.WriteString("y") + } else { + h.WriteString("n") + } + } + + return h.String() +} + func getCarRootCidAndLastSegment(imPath ImmutablePath) (cid.Cid, string, error) { imPathStr := imPath.String() if !strings.HasPrefix(imPathStr, "/ipfs/") { @@ -209,7 +209,7 @@ func getCarRootCidAndLastSegment(imPath ImmutablePath) (cid.Cid, string, error) return rootCid, lastSegment, err } -func getCarEtag(imPath ImmutablePath, params CarParams, rootCid cid.Cid) string { +func getCarEtag(imPath ImmutablePath, params *CarParams, rootCid cid.Cid) string { data := imPath.String() if params.Scope != DagScopeAll { data += "." + string(params.Scope) diff --git a/gateway/handler_car_test.go b/gateway/handler_car_test.go index 34a032dbb..ad3ced51b 100644 --- a/gateway/handler_car_test.go +++ b/gateway/handler_car_test.go @@ -131,7 +131,7 @@ func TestContentTypeFromCarParams(t *testing.T) { {CarParams{Duplicates: &F}, "application/vnd.ipld.car; version=1; order=unk; dups=n"}, } for _, test := range tests { - header := getContentTypeFromCarParams(test.params) + header := getContentTypeFromCarParams(&test.params) assert.Equal(t, test.header, header) } } @@ -148,24 +148,24 @@ func TestGetCarEtag(t *testing.T) { t.Run("Etag with entity-bytes=0:* is the same as without query param", func(t *testing.T) { t.Parallel() - noRange := getCarEtag(imPath, CarParams{}, cid) - withRange := getCarEtag(imPath, CarParams{Range: &DagByteRange{From: 0}}, cid) + noRange := getCarEtag(imPath, &CarParams{}, cid) + withRange := getCarEtag(imPath, &CarParams{Range: &DagByteRange{From: 0}}, cid) require.Equal(t, noRange, withRange) }) t.Run("Etag with entity-bytes=1:* is different than without query param", func(t *testing.T) { t.Parallel() - noRange := getCarEtag(imPath, CarParams{}, cid) - withRange := getCarEtag(imPath, CarParams{Range: &DagByteRange{From: 1}}, cid) + noRange := getCarEtag(imPath, &CarParams{}, cid) + withRange := getCarEtag(imPath, &CarParams{Range: &DagByteRange{From: 1}}, cid) require.NotEqual(t, noRange, withRange) }) t.Run("Etags with different dag-scope are different", func(t *testing.T) { t.Parallel() - a := getCarEtag(imPath, CarParams{Scope: DagScopeAll}, cid) - b := getCarEtag(imPath, CarParams{Scope: DagScopeEntity}, cid) + a := getCarEtag(imPath, &CarParams{Scope: DagScopeAll}, cid) + b := getCarEtag(imPath, &CarParams{Scope: DagScopeEntity}, cid) require.NotEqual(t, a, b) }) }