Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor gateway api to operate on higher level semantics #176

Merged
merged 70 commits into from
Mar 29, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
8ba923b
feat: refactor gateway api to operate on higher level semantics
aschmahmann Feb 16, 2023
0ead6cd
fix: respond to comments
aschmahmann Feb 20, 2023
8704fef
next iteration: moved _redirects and index.html back to being the gat…
aschmahmann Feb 26, 2023
0b6c1c5
rename GatewayMetadata -> ContentPathMetadata
aschmahmann Feb 26, 2023
32eeec6
Merge branch 'main' into feat/gateway-refactor
aschmahmann Feb 27, 2023
9e32875
go mod tidy
aschmahmann Feb 27, 2023
7d145b5
make tests mostly pass, includes change to go-unixfs
aschmahmann Feb 28, 2023
ebbe85d
Merge branch 'main' into feat/gateway-refactor
aschmahmann Mar 1, 2023
8413c2b
use shared BlocksGatway
aschmahmann Mar 1, 2023
2b172e7
fixup most errors, expand definition of NotFound
aschmahmann Mar 1, 2023
461fca2
change error reporting to pass a test
aschmahmann Mar 1, 2023
8fca1ae
tmp: point at go-unixfs commit hash
aschmahmann Mar 1, 2023
2bba208
downgrade go-car+otel dependencies and tidy
aschmahmann Mar 1, 2023
2631a5c
fix: bump go-unixfs dep that was missed previously
aschmahmann Mar 1, 2023
6652355
bump go-unixfs to support cid metadata in symlinks
aschmahmann Mar 1, 2023
3f135ce
fix it so we don't cache /ipns/somedir/index.html
aschmahmann Mar 1, 2023
bbdc88f
feat(gateway): allow content-type for UnixFS data to be determined by…
aschmahmann Mar 3, 2023
f8b96e5
refactor(gateway): move public sentinel errors to the same file as th…
aschmahmann Mar 3, 2023
146bbff
docs(gateway): add clarification to ResolveMutable
aschmahmann Mar 3, 2023
707bec5
feat: fix errors and expose ErrServiceUnavailable instead of go-ipfs-…
aschmahmann Mar 3, 2023
382ceaa
fix more errors, assert index.html content type as text/html
aschmahmann Mar 3, 2023
526e9e0
cleanup some todos
aschmahmann Mar 3, 2023
ba19090
refactor dirEntryMetadata name
aschmahmann Mar 3, 2023
80bebc1
explicitly handle an impossible error
aschmahmann Mar 3, 2023
09dc72d
fixup some text
aschmahmann Mar 3, 2023
f4b3232
more 500s and removing todos
aschmahmann Mar 3, 2023
54b7644
docs: improve gateway API docs
aschmahmann Mar 3, 2023
42dfc9d
blocks-gateway: remove extraneous methods
aschmahmann Mar 5, 2023
b4a6bb7
blocks-gateway: return non-UnixFS codecs from Get, except for non-Uni…
aschmahmann Mar 5, 2023
61f233c
fix _redirects caching/mutability
aschmahmann Mar 5, 2023
0caed11
fix gateway car requests
aschmahmann Mar 5, 2023
8ff5626
switch multierr lib
aschmahmann Mar 5, 2023
7275da3
reorder some imports
aschmahmann Mar 5, 2023
7af0fc6
fix: set X-Ipfs-Roots for CARs
aschmahmann Mar 5, 2023
ac23a54
Merge branch 'main' into feat/gateway-refactor
aschmahmann Mar 6, 2023
5efb46f
fix: restore error handling lost in merge commit
aschmahmann Mar 6, 2023
ea590f1
feat: switch Get to be multiple functions and implement range requests
aschmahmann Mar 6, 2023
4a49a38
feat: add ResolvePath to gateway API
aschmahmann Mar 6, 2023
36c78c0
fix error shadowing
aschmahmann Mar 7, 2023
62c6100
chore: cleanup linter errors
aschmahmann Mar 7, 2023
8c1c4d0
change GetCAR return signature to make the linter happy
aschmahmann Mar 7, 2023
14e7ffb
fix: uint64 cannot be < 0
hacdias Mar 7, 2023
17f8d70
chore: go mod tidy
hacdias Mar 7, 2023
e7366ca
apply some suggestions by @lidel
hacdias Mar 9, 2023
cde04e5
Merge branch 'main' into feat/gateway-refactor
hacdias Mar 9, 2023
db8b79e
feat: use rawRecord value for ipns record etag
hacdias Mar 9, 2023
2411c38
refactor: call addUserHeaders and set X-Ipfs-Path earlier
hacdias Mar 9, 2023
e0a3b8b
docs: apply suggestions
hacdias Mar 9, 2023
2615467
docs: clean explanation in isErrNotFound
hacdias Mar 9, 2023
524f4fd
fix: panic on invalid ipns
hacdias Mar 10, 2023
c9bed98
improved errors, renamed variables, remove comments (lidel feedback)
hacdias Mar 10, 2023
2eb8fb6
wrap directory entries for fast listing 🌮
hacdias Mar 13, 2023
ae65e02
feat: add default dns resolvers
hacdias Mar 14, 2023
167a8a8
Merge branch 'main' into feat/gateway-refactor
hacdias Mar 15, 2023
cbf04b3
feat: switch Get to return a union of files.File and a directory list…
aschmahmann Mar 14, 2023
cd0193d
docs: change contract on GetRange to indicate the full file must stil…
aschmahmann Mar 14, 2023
5f7a833
fix: switch _redirects to properly use root path instead of a partial…
aschmahmann Mar 15, 2023
e928893
refactor: remove unused types
hacdias Mar 15, 2023
c3d7b6f
refactor: make unixfs dir handling clearer
hacdias Mar 16, 2023
a05f54c
docs: handleWebRequestErrors
lidel Mar 18, 2023
47d3e19
chore: rename to directoryMetadata
lidel Mar 18, 2023
3de5712
change errors: graph size -> DAG size
aschmahmann Mar 20, 2023
1c6d038
Merge branch 'main' into feat/gateway-refactor
hacdias Mar 21, 2023
784eed6
fix merge
hacdias Mar 21, 2023
1c4716f
fix: ensure basic ipld codecs are registered
hacdias Mar 21, 2023
925f76d
Merge branch 'main' into feat/gateway-refactor
aschmahmann Mar 27, 2023
b5beaa2
more boxo related fixups
aschmahmann Mar 27, 2023
2e4b7c3
more boxo related fixups
aschmahmann Mar 28, 2023
ffa3aad
cleanup variable name in example
aschmahmann Mar 28, 2023
360b031
Merge branch 'main' into feat/gateway-refactor
hacdias Mar 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions examples/gateway/common/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"
"errors"
"fmt"
"io"
"net/http"
gopath "path"
"strings"

"github.com/ipfs/go-blockservice"
"github.com/ipfs/go-cid"
Expand All @@ -27,10 +29,12 @@ import (
iface "github.com/ipfs/interface-go-ipfs-core"
nsopts "github.com/ipfs/interface-go-ipfs-core/options/namesys"
ifacepath "github.com/ipfs/interface-go-ipfs-core/path"
"github.com/ipld/go-car"
dagpb "github.com/ipld/go-codec-dagpb"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/schema"
selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/routing"
mc "github.com/multiformats/go-multicodec"
Expand Down Expand Up @@ -62,6 +66,8 @@ type BlocksGateway struct {
routing routing.ValueStore
}

var _ gateway.API = (*BlocksGateway)(nil)

func NewBlocksGateway(blockService blockservice.BlockService, routing routing.ValueStore) (*BlocksGateway, error) {
// Setup the DAG services, which use the CAR block store.
dagService := merkledag.NewDAGService(blockService)
Expand Down Expand Up @@ -99,6 +105,196 @@ func NewBlocksGateway(blockService blockservice.BlockService, routing routing.Va
}, nil
}

func (api *BlocksGateway) Get(ctx context.Context, path gateway.ImmutablePath, opt ...gateway.GetOpt) (gateway.GatewayMetadata, files.Node, error) {
var opts gateway.GetOptions
for _, o := range opt {
if err := o(&opts); err != nil {
return gateway.GatewayMetadata{}, nil, err
}
}

roots, lastSeg, err := api.getPathRoots(ctx, path)
if err != nil {
return gateway.GatewayMetadata{}, nil, err
}

md := gateway.GatewayMetadata{
PathSegmentRoots: roots,
LastSegment: lastSeg,
}

lastRoot := lastSeg.Cid()

nd, err := api.dagService.Get(ctx, lastRoot)
if err != nil {
return gateway.GatewayMetadata{}, nil, err
}

if opts.RawBlock {
return md, files.NewBytesFile(nd.RawData()), nil
}

// This code path covers full graph, single file/directory, and range requests
rootCodec := nd.Cid().Prefix().GetCodec()
if rootCodec == uint64(mc.Raw) {
return md, files.NewBytesFile(nd.RawData()), nil
}
if rootCodec != uint64(mc.DagPb) {
return md, nil, fmt.Errorf("data is not UnixFS")
}
f, err := ufile.NewUnixfsFile(ctx, api.dagService, nd)
if err != nil {
return md, nil, err
}
return md, f, nil
}

func (api *BlocksGateway) Head(ctx context.Context, path gateway.ImmutablePath) (gateway.GatewayMetadata, files.Node, error) {
roots, lastSeg, err := api.getPathRoots(ctx, path)
if err != nil {
return gateway.GatewayMetadata{}, nil, err
}

md := gateway.GatewayMetadata{
PathSegmentRoots: roots,
LastSegment: lastSeg,
}

lastRoot := lastSeg.Cid()

nd, err := api.dagService.Get(ctx, lastRoot)
if err != nil {
return gateway.GatewayMetadata{}, nil, err
}

rootCodec := nd.Cid().Prefix().GetCodec()
if rootCodec != uint64(mc.DagPb) {
return md, files.NewBytesFile(nd.RawData()), nil
}

// TODO: We're not handling non-UnixFS dag-pb. There's a bit of a discrepancy between what we want from a HEAD request and a Resolve request here and we're using this for both
fileNode, err := ufile.NewUnixfsFile(ctx, api.dagService, nd)
if err != nil {
return gateway.GatewayMetadata{}, nil, err
}

return md, fileNode, nil
}

func (api *BlocksGateway) GetCAR(ctx context.Context, path gateway.ImmutablePath) (gateway.GatewayMetadata, io.ReadCloser, error, <-chan error) {
// Same go-car settings as dag.export command
store := dagStore{api: api, ctx: ctx}

// TODO: When switching to exposing path blocks we'll want to add these as well
roots, lastSeg, err := api.getPathRoots(ctx, path)
if err != nil {
return gateway.GatewayMetadata{}, nil, err, nil
}

md := gateway.GatewayMetadata{
PathSegmentRoots: roots,
LastSegment: lastSeg,
}

rootCid := lastSeg.Cid()

// 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()

errCh := make(chan error, 1)
go func() {
errCh <- c.Write(w)
close(errCh)
}()

return md, r, nil, errCh
}

func (api *BlocksGateway) getPathRoots(ctx context.Context, contentPath gateway.ImmutablePath) ([]cid.Cid, ifacepath.Resolved, error) {
/*
These are logical roots where each CID represent one path segment
and resolves to either a directory or the root block of a file.
The main purpose of this header is allow HTTP caches to do smarter decisions
around cache invalidation (eg. keep specific subdirectory/file if it did not change)
A good example is Wikipedia, which is HAMT-sharded, but we only care about
logical roots that represent each segment of the human-readable content
path:
Given contentPath = /ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey
rootCidList is a generated by doing `ipfs resolve -r` on each sub path:
/ipns/en.wikipedia-on-ipfs.org → bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze
/ipns/en.wikipedia-on-ipfs.org/wiki/ → bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4
/ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey → bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma
The result is an ordered array of values:
X-Ipfs-Roots: bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze,bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4,bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma
Note that while the top one will change every time any article is changed,
the last root (responsible for specific article) may not change at all.
*/
var sp strings.Builder
var pathRoots []cid.Cid
contentPathStr := contentPath.String()
pathSegments := strings.Split(contentPathStr[6:], "/")
sp.WriteString(contentPathStr[:5]) // /ipfs or /ipns
var lastPath ifacepath.Resolved
for _, root := range pathSegments {
if root == "" {
continue
}
sp.WriteString("/")
sp.WriteString(root)
resolvedSubPath, err := api.ResolvePath(ctx, ifacepath.New(sp.String()))
if err != nil {
return nil, nil, err
}
lastPath = resolvedSubPath
pathRoots = append(pathRoots, lastPath.Cid())
}

pathRoots = pathRoots[:len(pathRoots)-1]
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.GetBlock(ds.ctx, c)
}

func (api *BlocksGateway) ResolveMutable(ctx context.Context, p ifacepath.Path) (gateway.ImmutablePath, error) {
err := p.IsValid()
if err != nil {
return gateway.ImmutablePath{}, err
}

ipath := ipfspath.Path(p.String())
switch ipath.Segments()[0] {
case "ipns":
ipath, err = resolve.ResolveIPNS(ctx, api.namesys, ipath)
if err != nil {
return gateway.ImmutablePath{}, err
}
imPath, err := gateway.NewImmutablePath(ifacepath.New(ipath.String()))
if err != nil {
return gateway.ImmutablePath{}, err
}
return imPath, nil
case "ipfs":
imPath, err := gateway.NewImmutablePath(ifacepath.New(ipath.String()))
if err != nil {
return gateway.ImmutablePath{}, err
}
return imPath, nil
default:
return gateway.ImmutablePath{}, fmt.Errorf("unsupported path namespace: %s", p.Namespace())
}
}

func (api *BlocksGateway) GetUnixFsNode(ctx context.Context, p ifacepath.Resolved) (files.Node, error) {
nd, err := api.resolveNode(ctx, p)
if err != nil {
Expand Down
13 changes: 12 additions & 1 deletion files/readerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ type ReaderFile struct {
}

func NewBytesFile(b []byte) File {
return &ReaderFile{"", NewReaderFile(bytes.NewReader(b)), nil, int64(len(b))}
return &ReaderFile{"", bytesReaderCloser{bytes.NewReader(b)}, nil, int64(len(b))}
}

// TODO: Is this the best way to fix this bug?
// The bug is we want to be an io.ReadSeekCloser, but bytes.NewReader only gives a io.ReadSeeker and io.NopCloser
// effectively removes the io.Seeker ability from the passed in io.Reader
type bytesReaderCloser struct {
*bytes.Reader
}

func (b bytesReaderCloser) Close() error {
return nil
Comment on lines 20 to +32
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a bug and a small fix for it. I might be missing something though about why it was done this way.

}

func NewReaderFile(reader io.Reader) File {
Expand Down
Loading