331 lines
8.6 KiB
Go
331 lines
8.6 KiB
Go
// Package adder implements functionality to add content to IPFS daemons
|
|
// managed by the Cluster.
|
|
package adder
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"strings"
|
|
|
|
"github.com/ipfs-cluster/ipfs-cluster/adder/ipfsadd"
|
|
"github.com/ipfs-cluster/ipfs-cluster/api"
|
|
"github.com/ipfs/go-unixfs"
|
|
"github.com/ipld/go-car"
|
|
peer "github.com/libp2p/go-libp2p-core/peer"
|
|
|
|
cid "github.com/ipfs/go-cid"
|
|
files "github.com/ipfs/go-ipfs-files"
|
|
cbor "github.com/ipfs/go-ipld-cbor"
|
|
ipld "github.com/ipfs/go-ipld-format"
|
|
logging "github.com/ipfs/go-log/v2"
|
|
merkledag "github.com/ipfs/go-merkledag"
|
|
multihash "github.com/multiformats/go-multihash"
|
|
)
|
|
|
|
var logger = logging.Logger("adder")
|
|
|
|
// go-merkledag does this, but it may be moved.
|
|
// We include for explicitness.
|
|
func init() {
|
|
ipld.Register(cid.DagProtobuf, merkledag.DecodeProtobufBlock)
|
|
ipld.Register(cid.Raw, merkledag.DecodeRawBlock)
|
|
ipld.Register(cid.DagCBOR, cbor.DecodeBlock)
|
|
}
|
|
|
|
// ClusterDAGService is an implementation of ipld.DAGService plus a Finalize
|
|
// method. ClusterDAGServices can be used to provide Adders with a different
|
|
// add implementation.
|
|
type ClusterDAGService interface {
|
|
ipld.DAGService
|
|
// Finalize receives the IPFS content root CID as
|
|
// returned by the ipfs adder.
|
|
Finalize(ctx context.Context, ipfsRoot api.Cid) (api.Cid, error)
|
|
// Allocations returns the allocations made by the cluster DAG service
|
|
// for the added content.
|
|
Allocations() []peer.ID
|
|
}
|
|
|
|
// A dagFormatter can create dags from files.Node. It can keep state
|
|
// to add several files to the same dag.
|
|
type dagFormatter interface {
|
|
Add(name string, f files.Node) (api.Cid, error)
|
|
}
|
|
|
|
// Adder is used to add content to IPFS Cluster using an implementation of
|
|
// ClusterDAGService.
|
|
type Adder struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
dgs ClusterDAGService
|
|
|
|
params api.AddParams
|
|
|
|
// AddedOutput updates are placed on this channel
|
|
// whenever a block is processed. They contain information
|
|
// about the block, the CID, the Name etc. and are mostly
|
|
// meant to be streamed back to the user.
|
|
output chan api.AddedOutput
|
|
}
|
|
|
|
// New returns a new Adder with the given ClusterDAGService, add options and a
|
|
// channel to send updates during the adding process.
|
|
//
|
|
// An Adder may only be used once.
|
|
func New(ds ClusterDAGService, p api.AddParams, out chan api.AddedOutput) *Adder {
|
|
// Discard all progress update output as the caller has not provided
|
|
// a channel for them to listen on.
|
|
if out == nil {
|
|
out = make(chan api.AddedOutput, 100)
|
|
go func() {
|
|
for range out {
|
|
}
|
|
}()
|
|
}
|
|
|
|
return &Adder{
|
|
dgs: ds,
|
|
params: p,
|
|
output: out,
|
|
}
|
|
}
|
|
|
|
func (a *Adder) setContext(ctx context.Context) {
|
|
if a.ctx == nil { // only allows first context
|
|
ctxc, cancel := context.WithCancel(ctx)
|
|
a.ctx = ctxc
|
|
a.cancel = cancel
|
|
}
|
|
}
|
|
|
|
// FromMultipart adds content from a multipart.Reader. The adder will
|
|
// no longer be usable after calling this method.
|
|
func (a *Adder) FromMultipart(ctx context.Context, r *multipart.Reader) (api.Cid, error) {
|
|
logger.Debugf("adding from multipart with params: %+v", a.params)
|
|
|
|
f, err := files.NewFileFromPartReader(r, "multipart/form-data")
|
|
if err != nil {
|
|
return api.CidUndef, err
|
|
}
|
|
defer f.Close()
|
|
return a.FromFiles(ctx, f)
|
|
}
|
|
|
|
// FromFiles adds content from a files.Directory. The adder will no longer
|
|
// be usable after calling this method.
|
|
func (a *Adder) FromFiles(ctx context.Context, f files.Directory) (api.Cid, error) {
|
|
logger.Debug("adding from files")
|
|
a.setContext(ctx)
|
|
|
|
if a.ctx.Err() != nil { // don't allow running twice
|
|
return api.CidUndef, a.ctx.Err()
|
|
}
|
|
|
|
defer a.cancel()
|
|
defer close(a.output)
|
|
|
|
var dagFmtr dagFormatter
|
|
var err error
|
|
switch a.params.Format {
|
|
case "", "unixfs":
|
|
dagFmtr, err = newIpfsAdder(ctx, a.dgs, a.params, a.output)
|
|
|
|
case "car":
|
|
dagFmtr, err = newCarAdder(ctx, a.dgs, a.params, a.output)
|
|
default:
|
|
err = errors.New("bad dag formatter option")
|
|
}
|
|
if err != nil {
|
|
return api.CidUndef, err
|
|
}
|
|
|
|
// setup wrapping
|
|
if a.params.Wrap {
|
|
f = files.NewSliceDirectory(
|
|
[]files.DirEntry{files.FileEntry("", f)},
|
|
)
|
|
}
|
|
|
|
it := f.Entries()
|
|
var adderRoot api.Cid
|
|
for it.Next() {
|
|
select {
|
|
case <-a.ctx.Done():
|
|
return api.CidUndef, a.ctx.Err()
|
|
default:
|
|
logger.Debugf("ipfsAdder AddFile(%s)", it.Name())
|
|
|
|
adderRoot, err = dagFmtr.Add(it.Name(), it.Node())
|
|
if err != nil {
|
|
logger.Error("error adding to cluster: ", err)
|
|
return api.CidUndef, err
|
|
}
|
|
}
|
|
// TODO (hector): We can only add a single CAR file for the
|
|
// moment.
|
|
if a.params.Format == "car" {
|
|
break
|
|
}
|
|
}
|
|
if it.Err() != nil {
|
|
return api.CidUndef, it.Err()
|
|
}
|
|
|
|
clusterRoot, err := a.dgs.Finalize(a.ctx, adderRoot)
|
|
if err != nil {
|
|
logger.Error("error finalizing adder:", err)
|
|
return api.CidUndef, err
|
|
}
|
|
logger.Infof("%s successfully added to cluster", clusterRoot)
|
|
return clusterRoot, nil
|
|
}
|
|
|
|
// A wrapper around the ipfsadd.Adder to satisfy the dagFormatter interface.
|
|
type ipfsAdder struct {
|
|
*ipfsadd.Adder
|
|
}
|
|
|
|
func newIpfsAdder(ctx context.Context, dgs ClusterDAGService, params api.AddParams, out chan api.AddedOutput) (*ipfsAdder, error) {
|
|
iadder, err := ipfsadd.NewAdder(ctx, dgs, dgs.Allocations)
|
|
if err != nil {
|
|
logger.Error(err)
|
|
return nil, err
|
|
}
|
|
|
|
iadder.Trickle = params.Layout == "trickle"
|
|
iadder.RawLeaves = params.RawLeaves
|
|
iadder.Chunker = params.Chunker
|
|
iadder.Out = out
|
|
iadder.Progress = params.Progress
|
|
iadder.NoCopy = params.NoCopy
|
|
|
|
// Set up prefi
|
|
prefix, err := merkledag.PrefixForCidVersion(params.CidVersion)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bad CID Version: %s", err)
|
|
}
|
|
|
|
hashFunCode, ok := multihash.Names[strings.ToLower(params.HashFun)]
|
|
if !ok {
|
|
return nil, errors.New("hash function name not known")
|
|
}
|
|
prefix.MhType = hashFunCode
|
|
prefix.MhLength = -1
|
|
iadder.CidBuilder = &prefix
|
|
return &ipfsAdder{
|
|
Adder: iadder,
|
|
}, nil
|
|
}
|
|
|
|
func (ia *ipfsAdder) Add(name string, f files.Node) (api.Cid, error) {
|
|
// In order to set the AddedOutput names right, we use
|
|
// OutputPrefix:
|
|
//
|
|
// When adding a folder, this is the root folder name which is
|
|
// prepended to the addedpaths. When adding a single file,
|
|
// this is the name of the file which overrides the empty
|
|
// AddedOutput name.
|
|
//
|
|
// After coreunix/add.go was refactored in go-ipfs and we
|
|
// followed suit, it no longer receives the name of the
|
|
// file/folder being added and does not emit AddedOutput
|
|
// events with the right names. We addressed this by adding
|
|
// OutputPrefix to our version. go-ipfs modifies emitted
|
|
// events before sending to user).
|
|
ia.OutputPrefix = name
|
|
|
|
nd, err := ia.AddAllAndPin(f)
|
|
if err != nil {
|
|
return api.CidUndef, err
|
|
}
|
|
return api.NewCid(nd.Cid()), nil
|
|
}
|
|
|
|
// An adder to add CAR files. It is at the moment very basic, and can
|
|
// add a single CAR file with a single root. Ideally, it should be able to
|
|
// add more complex, or several CARs by wrapping them with a single root.
|
|
// But for that we would need to keep state and track an MFS root similarly to
|
|
// what the ipfsadder does.
|
|
type carAdder struct {
|
|
ctx context.Context
|
|
dgs ClusterDAGService
|
|
params api.AddParams
|
|
output chan api.AddedOutput
|
|
}
|
|
|
|
func newCarAdder(ctx context.Context, dgs ClusterDAGService, params api.AddParams, out chan api.AddedOutput) (*carAdder, error) {
|
|
return &carAdder{
|
|
ctx: ctx,
|
|
dgs: dgs,
|
|
params: params,
|
|
output: out,
|
|
}, nil
|
|
}
|
|
|
|
// Add takes a node which should be a CAR file and nothing else and
|
|
// adds its blocks using the ClusterDAGService.
|
|
func (ca *carAdder) Add(name string, fn files.Node) (api.Cid, error) {
|
|
if ca.params.Wrap {
|
|
return api.CidUndef, errors.New("cannot wrap a CAR file upload")
|
|
}
|
|
|
|
f, ok := fn.(files.File)
|
|
if !ok {
|
|
return api.CidUndef, errors.New("expected CAR file is not of type file")
|
|
}
|
|
carReader, err := car.NewCarReader(f)
|
|
if err != nil {
|
|
return api.CidUndef, err
|
|
}
|
|
|
|
if len(carReader.Header.Roots) != 1 {
|
|
return api.CidUndef, errors.New("only CAR files with a single root are supported")
|
|
}
|
|
|
|
root := carReader.Header.Roots[0]
|
|
bytes := uint64(0)
|
|
size := uint64(0)
|
|
|
|
for {
|
|
block, err := carReader.Next()
|
|
if err != nil && err != io.EOF {
|
|
return api.CidUndef, err
|
|
} else if block == nil {
|
|
break
|
|
}
|
|
|
|
bytes += uint64(len(block.RawData()))
|
|
|
|
nd, err := ipld.Decode(block)
|
|
if err != nil {
|
|
return api.CidUndef, err
|
|
}
|
|
|
|
// If the root is in the CAR and the root is a UnixFS
|
|
// node, then set the size in the output object.
|
|
if nd.Cid().Equals(root) {
|
|
ufs, err := unixfs.ExtractFSNode(nd)
|
|
if err == nil {
|
|
size = ufs.FileSize()
|
|
}
|
|
}
|
|
|
|
err = ca.dgs.Add(ca.ctx, nd)
|
|
if err != nil {
|
|
return api.CidUndef, err
|
|
}
|
|
}
|
|
|
|
ca.output <- api.AddedOutput{
|
|
Name: name,
|
|
Cid: api.NewCid(root),
|
|
Bytes: bytes,
|
|
Size: size,
|
|
Allocations: ca.dgs.Allocations(),
|
|
}
|
|
|
|
return api.NewCid(root), nil
|
|
}
|