diff --git a/core/commands/add.go b/core/commands/add.go index 3925c81d44c..2d3722380c2 100644 --- a/core/commands/add.go +++ b/core/commands/add.go @@ -269,6 +269,12 @@ You can now check what blocks have been created by: return } + base, _, err := HandleCidBase(req, env) + if err != nil { + res.SetError(err, cmdkit.ErrNormal) + return + } + fileAdder.Out = outChan fileAdder.Chunker = chunker fileAdder.Progress = progress @@ -280,6 +286,7 @@ You can now check what blocks have been created by: fileAdder.RawLeaves = rawblks fileAdder.NoCopy = nocopy fileAdder.Prefix = &prefix + fileAdder.Base = base if hash { md := dagtest.Mock() diff --git a/core/commands/files.go b/core/commands/files.go index edfdd8b19f9..58ad6cf4f1b 100644 --- a/core/commands/files.go +++ b/core/commands/files.go @@ -27,6 +27,7 @@ import ( humanize "gx/ipfs/QmPSBJL4momYnE7DcUyk2DVhD6rH488ZmHBGLbxNdhU44K/go-humanize" mh "gx/ipfs/QmPnFwZ2JXKnXgMw8CdBPxn7FWh6LLdjUjxV1fKHuJnkr8/go-multihash" offline "gx/ipfs/QmS6mo1dPpHdYsVkm27BRZDLxpKBCiJKUH8fHX15XFfMez/go-ipfs-exchange-offline" + mbase "gx/ipfs/QmSbvata2WqNkqGtZNg8MR3SKwnB8iQ7vTPJgWqB8bC5kR/go-multibase" cid "gx/ipfs/QmYVNvtQkeZ6AKSwDrjQTs432QtL6umrrK41EBq3cu7iSP/go-cid" ipld "gx/ipfs/QmZtNq8dArGfnpCZfx2pUNY7UcjGhVp5qqwQ4hH6mpTMRQ/go-ipld-format" logging "gx/ipfs/QmcVVHfdyv15GVPk7NrxdWjh2hLVccXnoD8j2tyQShiXJb/go-log" @@ -128,6 +129,12 @@ var filesStatCmd = &cmds.Command{ withLocal, _ := req.Options["with-local"].(bool) + base, _, err := HandleCidBase(req, env) + if err != nil { + res.SetError(err, cmdkit.ErrNormal) + return + } + var dagserv ipld.DAGService if withLocal { // an offline DAGService will not fetch from the network @@ -145,7 +152,7 @@ var filesStatCmd = &cmds.Command{ return } - o, err := statNode(nd) + o, err := statNode(nd, base) if err != nil { res.SetError(err, cmdkit.ErrNormal) return @@ -217,7 +224,7 @@ func statGetFormatOptions(req *cmds.Request) (string, error) { } } -func statNode(nd ipld.Node) (*statOutput, error) { +func statNode(nd ipld.Node, base mbase.Encoder) (*statOutput, error) { c := nd.Cid() cumulsize, err := nd.Size() @@ -243,7 +250,7 @@ func statNode(nd ipld.Node) (*statOutput, error) { } return &statOutput{ - Hash: c.String(), + Hash: c.Encode(base), Blocks: len(nd.Links()), Size: d.GetFilesize(), CumulativeSize: cumulsize, @@ -251,7 +258,7 @@ func statNode(nd ipld.Node) (*statOutput, error) { }, nil case *dag.RawNode: return &statOutput{ - Hash: c.String(), + Hash: c.Encode(base), Blocks: 0, Size: cumulsize, CumulativeSize: cumulsize, @@ -437,11 +444,13 @@ Examples: long, _, _ := req.Option("l").Bool() + base, _, ctx, err := HandleCidBaseOld(req, req.Context()) + switch fsn := fsn.(type) { case *mfs.Directory: if !long { var output []mfs.NodeListing - names, err := fsn.ListNames(req.Context()) + names, err := fsn.ListNames(ctx) if err != nil { res.SetError(err, cmdkit.ErrNormal) return @@ -454,7 +463,7 @@ Examples: } res.SetOutput(&filesLsOutput{output}) } else { - listing, err := fsn.List(req.Context()) + listing, err := fsn.List(ctx) if err != nil { res.SetError(err, cmdkit.ErrNormal) return @@ -480,7 +489,7 @@ Examples: res.SetError(err, cmdkit.ErrNormal) return } - out.Entries[0].Hash = nd.Cid().String() + out.Entries[0].Hash = nd.Cid().Encode(base) } res.SetOutput(out) return diff --git a/core/commands/ls.go b/core/commands/ls.go index 332d3b85125..14226716dd3 100644 --- a/core/commands/ls.go +++ b/core/commands/ls.go @@ -9,6 +9,7 @@ import ( blockservice "github.com/ipfs/go-ipfs/blockservice" cmds "github.com/ipfs/go-ipfs/commands" core "github.com/ipfs/go-ipfs/core" + misc "github.com/ipfs/go-ipfs/core/misc" e "github.com/ipfs/go-ipfs/core/commands/e" merkledag "github.com/ipfs/go-ipfs/merkledag" path "github.com/ipfs/go-ipfs/path" @@ -77,6 +78,12 @@ The JSON output contains type information. return } + _, _, ctx, err := HandleCidBaseOld(req, req.Context()) + if err != nil { + res.SetError(err, cmdkit.ErrNormal) + return + } + dserv := nd.DAG if !resolve { offlineexch := offline.Exchange(nd.Blockstore) @@ -99,7 +106,7 @@ The JSON output contains type information. ResolveOnce: uio.ResolveUnixfsOnce, } - dagnode, err := core.Resolve(req.Context(), nd.Namesys, r, p) + dagnode, err := core.Resolve(ctx, nd.Namesys, r, p) if err != nil { res.SetError(err, cmdkit.ErrNormal) return @@ -120,7 +127,7 @@ The JSON output contains type information. if dir == nil { links = dagnode.Links() } else { - links, err = dir.Links(req.Context()) + links, err = dir.Links(ctx) if err != nil { res.SetError(err, cmdkit.ErrNormal) return @@ -140,7 +147,7 @@ The JSON output contains type information. // No need to check with raw leaves t = unixfspb.Data_File case cid.DagProtobuf: - linkNode, err := link.GetNode(req.Context(), dserv) + linkNode, err := link.GetNode(ctx, dserv) if err == ipld.ErrNotFound && !resolve { // not an error linkNode = nil @@ -158,9 +165,10 @@ The JSON output contains type information. t = d.GetType() } } + base := misc.GetCidBase(ctx, paths[i]) output[i].Links[j] = LsLink{ Name: link.Name, - Hash: link.Cid.String(), + Hash: link.Cid.Encode(base), Size: link.Size, Type: t, } diff --git a/core/commands/pin.go b/core/commands/pin.go index 199bbab9ad9..0eca196a29b 100644 --- a/core/commands/pin.go +++ b/core/commands/pin.go @@ -79,8 +79,14 @@ var addPinCmd = &cmds.Command{ } showProgress, _, _ := req.Option("progress").Bool() + _, _, ctx, err := HandleCidBase(req, ctx) + if err != nil { + res.SetError(err, cmdkit.ErrNormal) + return + } + if !showProgress { - added, err := corerepo.Pin(n, req.Context(), req.Arguments(), recursive) + added, err := corerepo.Pin(n, ctx, req.Arguments(), recursive) if err != nil { res.SetError(err, cmdkit.ErrNormal) return @@ -92,7 +98,7 @@ var addPinCmd = &cmds.Command{ out := make(chan interface{}) res.SetOutput((<-chan interface{})(out)) v := new(dag.ProgressTracker) - ctx := v.DeriveContext(req.Context()) + ctx := v.DeriveContext(ctx) type pinResult struct { pins []*cid.Cid @@ -201,7 +207,7 @@ collected if needed. (By default, recursively. Use -r=false for direct pins.) return } - removed, err := corerepo.Unpin(n, req.Context(), req.Arguments(), recursive) + removed, err := corerepo.Unpin(n, ctx, req.Arguments(), recursive) if err != nil { res.SetError(err, cmdkit.ErrNormal) return @@ -305,9 +311,9 @@ Example: var keys map[string]RefKeyObject if len(req.Arguments()) > 0 { - keys, err = pinLsKeys(req.Context(), req.Arguments(), typeStr, n) + keys, err = pinLsKeys(ctx, req.Arguments(), typeStr, n) } else { - keys, err = pinLsAll(req.Context(), typeStr, n) + keys, err = pinLsAll(ctx, typeStr, n) } if err != nil { @@ -394,19 +400,19 @@ new pin and removing the old one. ResolveOnce: uio.ResolveUnixfsOnce, } - fromc, err := core.ResolveToCid(req.Context(), n.Namesys, r, from) + fromc, err := core.ResolveToCid(ctx, n.Namesys, r, from) if err != nil { res.SetError(err, cmdkit.ErrNormal) return } - toc, err := core.ResolveToCid(req.Context(), n.Namesys, r, to) + toc, err := core.ResolveToCid(ctx, n.Namesys, r, to) if err != nil { res.SetError(err, cmdkit.ErrNormal) return } - err = n.Pinning.Update(req.Context(), fromc, toc, unpin) + err = n.Pinning.Update(ctx, fromc, toc, unpin) if err != nil { res.SetError(err, cmdkit.ErrNormal) return @@ -454,7 +460,7 @@ var verifyPinCmd = &cmds.Command{ explain: !quiet, includeOk: verbose, } - out := pinVerify(req.Context(), n, opts) + out := pinVerify(ctx, n, opts) res.SetOutput(out) }, diff --git a/core/commands/refs.go b/core/commands/refs.go index ee9caad570d..23dcd396741 100644 --- a/core/commands/refs.go +++ b/core/commands/refs.go @@ -7,6 +7,7 @@ import ( "io" "strings" + misc "github.com/ipfs/go-ipfs/core/misc" cmds "github.com/ipfs/go-ipfs/commands" "github.com/ipfs/go-ipfs/core" e "github.com/ipfs/go-ipfs/core/commands/e" @@ -106,6 +107,12 @@ NOTE: List all references recursively by using the flag '-r'. format = " -> " } + _, _, ctx, err = HandleCidBaseOld(req, ctx) + if err != nil { + res.SetError(err, cmdkit.ErrNormal) + return + } + objs, err := objectsForPaths(ctx, n, req.Arguments()) if err != nil { res.SetError(err, cmdkit.ErrNormal) @@ -158,6 +165,12 @@ Displays the hashes of all local objects. return } + base, _, ctx, err := HandleCidBaseOld(req, req.Context()) + if err != nil { + res.SetError(err, cmdkit.ErrNormal) + return + } + // todo: make async allKeys, err := n.Blockstore.AllKeysChan(ctx) if err != nil { @@ -173,7 +186,7 @@ Displays the hashes of all local objects. for k := range allKeys { select { - case out <- &RefWrapper{Ref: k.String()}: + case out <- &RefWrapper{Ref: k.Encode(base)}: case <-req.Context().Done(): return } @@ -323,15 +336,17 @@ func (rw *RefWriter) WriteEdge(from, to *cid.Cid, linkname string) error { } } + base := misc.GetCidBase(rw.Ctx, "") + var s string switch { case rw.PrintFmt != "": s = rw.PrintFmt - s = strings.Replace(s, "", from.String(), -1) - s = strings.Replace(s, "", to.String(), -1) + s = strings.Replace(s, "", from.Encode(base), -1) + s = strings.Replace(s, "", to.Encode(base), -1) s = strings.Replace(s, "", linkname, -1) default: - s += to.String() + s += to.Encode(base) } rw.out <- &RefWrapper{Ref: s} diff --git a/core/commands/resolve.go b/core/commands/resolve.go index 39c76f6249b..69c3dc20ec0 100644 --- a/core/commands/resolve.go +++ b/core/commands/resolve.go @@ -9,6 +9,7 @@ import ( cmds "github.com/ipfs/go-ipfs/commands" "github.com/ipfs/go-ipfs/core" e "github.com/ipfs/go-ipfs/core/commands/e" + misc "github.com/ipfs/go-ipfs/core/misc" ns "github.com/ipfs/go-ipfs/namesys" nsopts "github.com/ipfs/go-ipfs/namesys/opts" path "github.com/ipfs/go-ipfs/path" @@ -87,6 +88,12 @@ Resolve the value of an IPFS DAG path: name := req.Arguments()[0] recursive, _, _ := req.Option("recursive").Bool() + _, _, ctx, err := HandleCidBaseOld(req, req.Context()) + if err != nil { + res.SetError(err, cmdkit.ErrNormal) + return + } + // the case when ipns is resolved step by step if strings.HasPrefix(name, "/ipns/") && !recursive { rc, rcok, _ := req.Option("dht-record-count").Int() @@ -107,7 +114,7 @@ Resolve the value of an IPFS DAG path: } ropts = append(ropts, nsopts.DhtTimeout(d)) } - p, err := n.Namesys.Resolve(req.Context(), name, ropts...) + p, err := n.Namesys.Resolve(ctx, name, ropts...) // ErrResolveRecursion is fine if err != nil && err != ns.ErrResolveRecursion { res.SetError(err, cmdkit.ErrNormal) @@ -124,15 +131,17 @@ Resolve the value of an IPFS DAG path: return } - node, err := core.Resolve(req.Context(), n.Namesys, n.Resolver, p) + node, err := core.Resolve(ctx, n.Namesys, n.Resolver, p) if err != nil { res.SetError(err, cmdkit.ErrNormal) return } c := node.Cid() + base := misc.GetCidBase(ctx, name) + pathStr := "/ipfs/" + c.Encode(base) - res.SetOutput(&ResolvedPath{path.FromCid(c)}) + res.SetOutput(&ResolvedPath{path.FromString(pathStr)}) }, Marshalers: cmds.MarshalerMap{ cmds.Text: func(res cmds.Response) (io.Reader, error) { diff --git a/core/commands/root.go b/core/commands/root.go index d5e884d3901..096462e1ba3 100644 --- a/core/commands/root.go +++ b/core/commands/root.go @@ -3,6 +3,7 @@ package commands import ( "io" "strings" + "context" oldcmds "github.com/ipfs/go-ipfs/commands" lgc "github.com/ipfs/go-ipfs/commands/legacy" @@ -12,6 +13,7 @@ import ( unixfs "github.com/ipfs/go-ipfs/core/commands/unixfs" "gx/ipfs/QmNueRyPRQiV7PUEpnP4GgGLuK1rKQLaRW7sfPvUetYig1/go-ipfs-cmds" + mbase "gx/ipfs/QmSbvata2WqNkqGtZNg8MR3SKwnB8iQ7vTPJgWqB8bC5kR/go-multibase" logging "gx/ipfs/QmcVVHfdyv15GVPk7NrxdWjh2hLVccXnoD8j2tyQShiXJb/go-log" "gx/ipfs/QmdE4gMduCKCGAcczM2F5ioYDfdeKuPix138wrES1YSr7f/go-ipfs-cmdkit" ) @@ -19,7 +21,7 @@ import ( var log = logging.Logger("core/commands") const ( - ApiOption = "api" + ApiOption = "api" ) var Root = &cmds.Command{ @@ -91,6 +93,7 @@ The CLI will exit with one of the following values: cmdkit.BoolOption("h", "Show a short version of the command help text."), cmdkit.BoolOption("local", "L", "Run the command locally, instead of using the daemon."), cmdkit.StringOption(ApiOption, "Use a specific API instance (defaults to /ip4/127.0.0.1/tcp/5001)"), + cmdkit.StringOption("cid-base", "mbase", "Multi-base to use to encode version 1 CIDs in output."), // global options, added to every command cmds.OptionEncodingType, @@ -218,3 +221,37 @@ func MessageTextMarshaler(res oldcmds.Response) (io.Reader, error) { return strings.NewReader(out.Message), nil } + +// HandleCidBase handles processing of the "cid-base" flag. It +// currently checks for the "cid-base" flag and replacesing the +// requests context with a new one that adds a "cid-base" vaue. +func HandleCidBase(req *cmds.Request, env cmds.Environment) (mbase.Encoder, bool, error) { + baseStr, _ := req.Options["cid-base"].(string) + if baseStr != "" { + encoder, err := mbase.EncoderByName(baseStr) + if err != nil { + return encoder, false, err + } + req.Context = context.WithValue(req.Context, "cid-base", encoder) + return encoder, true, err + } + encoder, _ := mbase.NewEncoder(mbase.Base58BTC) + return encoder, false, nil +} + +// HandleCidBaseFlagOld is like HandleCidBase but works with the old +// commands interface. Since it is not possible to change the context +// using this interface a new context is returned instead. +func HandleCidBaseOld(req oldcmds.Request, ctx context.Context) (mbase.Encoder, bool, context.Context, error) { + baseStr, _, _ := req.Option("cid-base").String() + if baseStr != "" { + encoder, err := mbase.EncoderByName(baseStr) + if err != nil { + return encoder, false, ctx, err + } + ctx = context.WithValue(ctx, "cid-base", encoder) + return encoder, true, ctx, err + } + encoder, _ := mbase.NewEncoder(mbase.Base58BTC) + return encoder, false, ctx, nil +} diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index 1236b30f576..967665dec25 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -23,11 +23,11 @@ import ( uio "github.com/ipfs/go-ipfs/unixfs/io" humanize "gx/ipfs/QmPSBJL4momYnE7DcUyk2DVhD6rH488ZmHBGLbxNdhU44K/go-humanize" + multibase "gx/ipfs/QmSbvata2WqNkqGtZNg8MR3SKwnB8iQ7vTPJgWqB8bC5kR/go-multibase" chunker "gx/ipfs/QmVDjhUMtkRskBFAVNwyXuLSKbeAya7JKPnzAxMKDaK4x4/go-ipfs-chunker" cid "gx/ipfs/QmYVNvtQkeZ6AKSwDrjQTs432QtL6umrrK41EBq3cu7iSP/go-cid" routing "gx/ipfs/QmZ383TySJVeZWzGnWui6pRcKyYZk9VkKTuW7tmKRWk5au/go-libp2p-routing" ipld "gx/ipfs/QmZtNq8dArGfnpCZfx2pUNY7UcjGhVp5qqwQ4hH6mpTMRQ/go-ipld-format" - multibase "gx/ipfs/QmexBtiTTEwwn42Yi6ouKt6VqzpA6wjJgiW1oh9VfaRrup/go-multibase" ) const ( diff --git a/core/coreunix/add.go b/core/coreunix/add.go index 2196521be42..b8af76b506a 100644 --- a/core/coreunix/add.go +++ b/core/coreunix/add.go @@ -20,6 +20,7 @@ import ( unixfs "github.com/ipfs/go-ipfs/unixfs" posinfo "gx/ipfs/QmSHjPDw8yNgLZ7cBfX7w3Smn7PHwYhNEpd4LHQQxUg35L/go-ipfs-posinfo" + mbase "gx/ipfs/QmSbvata2WqNkqGtZNg8MR3SKwnB8iQ7vTPJgWqB8bC5kR/go-multibase" chunker "gx/ipfs/QmVDjhUMtkRskBFAVNwyXuLSKbeAya7JKPnzAxMKDaK4x4/go-ipfs-chunker" cid "gx/ipfs/QmYVNvtQkeZ6AKSwDrjQTs432QtL6umrrK41EBq3cu7iSP/go-cid" ipld "gx/ipfs/QmZtNq8dArGfnpCZfx2pUNY7UcjGhVp5qqwQ4hH6mpTMRQ/go-ipld-format" @@ -55,6 +56,7 @@ type AddedObject struct { // NewAdder Returns a new Adder used for a file add operation. func NewAdder(ctx context.Context, p pin.Pinner, bs bstore.GCBlockstore, ds ipld.DAGService) (*Adder, error) { + b, _ := mbase.NewEncoder(mbase.Base58BTC) return &Adder{ ctx: ctx, pinning: p, @@ -66,6 +68,7 @@ func NewAdder(ctx context.Context, p pin.Pinner, bs bstore.GCBlockstore, ds ipld Trickle: false, Wrap: false, Chunker: "", + Base: b, }, nil } @@ -90,6 +93,7 @@ type Adder struct { unlocker bstore.Unlocker tempRoot *cid.Cid Prefix *cid.Prefix + Base mbase.Encoder liveNodes uint64 } @@ -276,7 +280,7 @@ func (adder *Adder) outputDirs(path string, fsn mfs.FSNode) error { return err } - return outputDagnode(adder.Out, path, nd) + return outputDagnode(adder.Out, path, nd, adder.Base) default: return fmt.Errorf("unrecognized fsn type: %#v", fsn) } @@ -398,7 +402,7 @@ func (adder *Adder) addNode(node ipld.Node, path string) error { } if !adder.Silent { - return outputDagnode(adder.Out, path, node) + return outputDagnode(adder.Out, path, node, adder.Base) } return nil } @@ -533,12 +537,12 @@ func (adder *Adder) maybePauseForGC() error { } // outputDagnode sends dagnode info over the output channel -func outputDagnode(out chan interface{}, name string, dn ipld.Node) error { +func outputDagnode(out chan interface{}, name string, dn ipld.Node, base mbase.Encoder) error { if out == nil { return nil } - o, err := getOutput(dn) + o, err := getOutput(dn, base) if err != nil { return err } @@ -553,7 +557,7 @@ func outputDagnode(out chan interface{}, name string, dn ipld.Node) error { } // from core/commands/object.go -func getOutput(dagnode ipld.Node) (*Object, error) { +func getOutput(dagnode ipld.Node, base mbase.Encoder) (*Object, error) { c := dagnode.Cid() s, err := dagnode.Size() if err != nil { @@ -561,7 +565,7 @@ func getOutput(dagnode ipld.Node) (*Object, error) { } output := &Object{ - Hash: c.String(), + Hash: c.Encode(base), Size: strconv.FormatUint(s, 10), Links: make([]Link, len(dagnode.Links())), } diff --git a/core/misc/cid.go b/core/misc/cid.go new file mode 100644 index 00000000000..e931b8aa2ce --- /dev/null +++ b/core/misc/cid.go @@ -0,0 +1,32 @@ +package misc + +import ( + "context" + "strings" + + mbase "gx/ipfs/QmSbvata2WqNkqGtZNg8MR3SKwnB8iQ7vTPJgWqB8bC5kR/go-multibase" +) + + +// GetCidBase gets the cid base to use from either the context or +// another cid or path +func GetCidBase(ctx context.Context, cidStr string) mbase.Encoder { + encoder, ok := ctx.Value("cid-base").(mbase.Encoder) + if ok { + return encoder + } + defaultEncoder, _ := mbase.NewEncoder(mbase.Base58BTC) + if cidStr != "" { + cidStr = strings.TrimPrefix(cidStr, "/ipfs/") + if cidStr == "" || strings.HasPrefix(cidStr, "Qm") { + return defaultEncoder + } + encoder, err := mbase.NewEncoder(mbase.Encoding(cidStr[0])) + if err != nil { + return defaultEncoder + } + return encoder + } + return defaultEncoder +} + diff --git a/mfs/dir.go b/mfs/dir.go index a98ce859eca..2eaf0d387fd 100644 --- a/mfs/dir.go +++ b/mfs/dir.go @@ -9,6 +9,7 @@ import ( "sync" "time" + misc "github.com/ipfs/go-ipfs/core/misc" dag "github.com/ipfs/go-ipfs/merkledag" ft "github.com/ipfs/go-ipfs/unixfs" uio "github.com/ipfs/go-ipfs/unixfs/io" @@ -261,6 +262,7 @@ func (d *Directory) List(ctx context.Context) ([]NodeListing, error) { func (d *Directory) ForEachEntry(ctx context.Context, f func(NodeListing) error) error { d.lock.Lock() defer d.lock.Unlock() + base := misc.GetCidBase(ctx, "") return d.unixfsDir.ForEachLink(ctx, func(l *ipld.Link) error { c, err := d.childUnsync(l.Name) if err != nil { @@ -275,7 +277,7 @@ func (d *Directory) ForEachEntry(ctx context.Context, f func(NodeListing) error) child := NodeListing{ Name: l.Name, Type: int(c.Type()), - Hash: nd.Cid().String(), + Hash: nd.Cid().Encode(base), } if c, ok := c.(*File); ok { diff --git a/package.json b/package.json index 9b29239eace..307fe678f42 100644 --- a/package.json +++ b/package.json @@ -265,9 +265,9 @@ }, { "author": "whyrusleeping", - "hash": "QmexBtiTTEwwn42Yi6ouKt6VqzpA6wjJgiW1oh9VfaRrup", + "hash": "QmSbvata2WqNkqGtZNg8MR3SKwnB8iQ7vTPJgWqB8bC5kR", "name": "go-multibase", - "version": "0.2.6" + "version": "0.2.7" }, { "author": "multiformats",