kubo/core/corehttp/gateway_handler_car.go
2022-07-06 18:40:37 +02:00

100 lines
3.5 KiB
Go

package corehttp
import (
"context"
"fmt"
"net/http"
"time"
blocks "github.com/ipfs/go-block-format"
cid "github.com/ipfs/go-cid"
coreiface "github.com/ipfs/interface-go-ipfs-core"
ipath "github.com/ipfs/interface-go-ipfs-core/path"
"github.com/ipfs/kubo/tracing"
gocar "github.com/ipld/go-car"
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// serveCAR returns a CAR stream for specific DAG+selector
func (i *gatewayHandler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) {
ctx, span := tracing.Span(ctx, "Gateway", "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String())))
defer span.End()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
switch carVersion {
case "": // noop, client does not care about version
case "1": // noop, we support this
default:
err := fmt.Errorf("only version=1 is supported")
webError(w, "unsupported CAR version", err, http.StatusBadRequest)
return
}
rootCid := resolvedPath.Cid()
// Set Content-Disposition
var name string
if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
name = urlFilename
} else {
name = rootCid.String() + ".car"
}
setContentDispositionHeader(w, name, "attachment")
// Set Cache-Control (same logic as for a regular files)
addCacheControlHeaders(w, r, contentPath, rootCid)
// Weak Etag W/ because we can't guarantee byte-for-byte identical
// responses, but still want to benefit from HTTP Caching. Two CAR
// responses for the same CID and selector will be logically equivalent,
// but when CAR is streamed, then in theory, blocks may arrive from
// datastore in non-deterministic order.
etag := `W/` + getEtag(r, rootCid)
w.Header().Set("Etag", etag)
// Finish early if Etag match
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
// Make it clear we don't support range-requests over a car stream
// Partial downloads and resumes should be handled using requests for
// sub-DAGs and IPLD selectors: https://github.com/ipfs/go-ipfs/issues/8769
w.Header().Set("Accept-Ranges", "none")
w.Header().Set("Content-Type", "application/vnd.ipld.car; version=1")
w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^)
// Same go-car settings as dag.export command
store := dagStore{dag: i.api.Dag(), ctx: ctx}
// TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769
dag := gocar.Dag{Root: rootCid, Selector: selectorparse.CommonSelector_ExploreAllRecursively}
car := gocar.NewSelectiveCar(ctx, store, []gocar.Dag{dag}, gocar.TraverseLinksOnlyOnce())
if err := car.Write(w); err != nil {
// We return error as a trailer, however it is not something browsers can access
// (https://github.com/mdn/browser-compat-data/issues/14703)
// Due to this, we suggest client always verify that
// the received CAR stream response is matching requested DAG selector
w.Header().Set("X-Stream-Error", err.Error())
return
}
// Update metrics
i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds())
}
// FIXME(@Jorropo): https://github.com/ipld/go-car/issues/315
type dagStore struct {
dag coreiface.APIDagService
ctx context.Context
}
func (ds dagStore) Get(_ context.Context, c cid.Cid) (blocks.Block, error) {
return ds.dag.Get(ds.ctx, c)
}