diff --git a/core/commands2/add.go b/core/commands2/add.go index 104f506ad8745e26da35b640b75381ff7a9bf53e..c96a920c4c4f545d8ba3fc21085249b14cce50b7 100644 --- a/core/commands2/add.go +++ b/core/commands2/add.go @@ -97,12 +97,7 @@ func add(n *core.IpfsNode, readers []io.Reader) ([]*dag.Node, error) { return nil, err } - err = n.DAG.AddRecursive(node) // add the file to the graph + local storage - if err != nil { - return nil, err - } - - err = n.Pinning.Pin(node, true) // ensure we keep it + err = addNode(n, node) if err != nil { return nil, err } @@ -111,3 +106,17 @@ func add(n *core.IpfsNode, readers []io.Reader) ([]*dag.Node, error) { } return dagnodes, nil } + +func addNode(n *core.IpfsNode, node *dag.Node) error { + err := n.DAG.AddRecursive(node) // add the file to the graph + local storage + if err != nil { + return err + } + + err = n.Pinning.Pin(node, true) // ensure we keep it + if err != nil { + return err + } + + return nil +} diff --git a/core/commands2/object.go b/core/commands2/object.go new file mode 100644 index 0000000000000000000000000000000000000000..3affff3c7ce1fbe91dbd37546c5b0adaad62cd5e --- /dev/null +++ b/core/commands2/object.go @@ -0,0 +1,297 @@ +package commands + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "io/ioutil" + + cmds "github.com/jbenet/go-ipfs/commands" + "github.com/jbenet/go-ipfs/core" + dag "github.com/jbenet/go-ipfs/merkledag" +) + +// ErrObjectTooLarge is returned when too much data was read from stdin. current limit 512k +var ErrObjectTooLarge = errors.New("input object was too large. limit is 512kbytes") + +const inputLimit = 512 * 1024 + +var objectCmd = &cmds.Command{ + Description: "Interact with ipfs objects", + Help: `'ipfs object' is a plumbing command used to manipulate DAG objects directly.`, + + Subcommands: map[string]*cmds.Command{ + "data": objectDataCmd, + "links": objectLinksCmd, + "get": objectGetCmd, + "put": objectPutCmd, + }, +} + +var objectDataCmd = &cmds.Command{ + Description: "Outputs the raw bytes in an IPFS object", + Help: `ipfs data is a plumbing command for retreiving the raw bytes stored in a DAG node. +It outputs to stdout, and <key> is a base58 encoded multihash. + +Note that the "--encoding" option does not affect the output, since the +output is the raw data of the object. +`, + + Arguments: []cmds.Argument{ + cmds.Argument{"key", cmds.ArgString, true, false, + "Key of the object to retrieve, in base58-encoded multihash format"}, + }, + Run: func(res cmds.Response, req cmds.Request) { + n := req.Context().Node + + key, ok := req.Arguments()[0].(string) + if !ok { + res.SetError(errors.New("cast error"), cmds.ErrNormal) + return + } + + reader, err := objectData(n, key) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + res.SetOutput(reader) + }, +} + +var objectLinksCmd = &cmds.Command{ + Description: "Outputs the links pointed to by the specified object", + Help: `'ipfs block get' is a plumbing command for retreiving raw IPFS blocks. +It outputs to stdout, and <key> is a base58 encoded multihash.`, + + Arguments: []cmds.Argument{ + cmds.Argument{"key", cmds.ArgString, true, false, + "Key of the object to retrieve, in base58-encoded multihash format"}, + }, + Run: func(res cmds.Response, req cmds.Request) { + n := req.Context().Node + + key, ok := req.Arguments()[0].(string) + if !ok { + res.SetError(errors.New("cast error"), cmds.ErrNormal) + return + } + + output, err := objectLinks(n, key) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + res.SetOutput(output) + }, + Type: &Object{}, +} + +var objectGetCmd = &cmds.Command{ + Description: "Get and serialize the DAG node named by <key>", + Help: `'ipfs object get' is a plumbing command for retreiving DAG nodes. +It serializes the DAG node to the format specified by the "--encoding" flag. +It outputs to stdout, and <key> is a base58 encoded multihash. + +This command outputs data in the following encodings: "protobuf", "json", "xml" +(Specified by the "--encoding" flag)`, + + Arguments: []cmds.Argument{ + cmds.Argument{"key", cmds.ArgString, true, false, + "Key of the object to retrieve, in base58-encoded multihash format"}, + }, + Run: func(res cmds.Response, req cmds.Request) { + n := req.Context().Node + + key, ok := req.Arguments()[0].(string) + if !ok { + res.SetError(errors.New("cast error"), cmds.ErrNormal) + return + } + + object, err := objectGet(n, key) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + res.SetOutput(object) + }, + Type: &dag.Node{}, + Marshallers: map[cmds.EncodingType]cmds.Marshaller{ + cmds.EncodingType("protobuf"): func(res cmds.Response) ([]byte, error) { + object := res.Output().(*dag.Node) + return object.Marshal() + }, + }, +} + +var objectPutCmd = &cmds.Command{ + Description: "Stores input as a DAG object, outputs its key", + Help: `'ipfs object put' is a plumbing command for storing DAG nodes. +It reads from stdin, and the output is a base58 encoded multihash. + +Data should be in the format specified by <encoding>. +<encoding> may be one of the following: + * "protobuf" + * "json" +`, + + Arguments: []cmds.Argument{ + cmds.Argument{"data", cmds.ArgFile, true, false, + "Data to be stored as a DAG object, encoded as specified in <encoding>"}, + cmds.Argument{"encoding", cmds.ArgString, true, false, + "Encoding type of <data>, either \"protobuf\" or \"json\""}, + }, + Run: func(res cmds.Response, req cmds.Request) { + n := req.Context().Node + + input, ok := req.Arguments()[0].(io.Reader) + if !ok { + res.SetError(errors.New("cast error"), cmds.ErrNormal) + return + } + + encoding, ok := req.Arguments()[1].(string) + if !ok { + res.SetError(errors.New("cast error"), cmds.ErrNormal) + return + } + + output, err := objectPut(n, input, encoding) + if err != nil { + errType := cmds.ErrNormal + if err == ErrUnknownObjectEnc { + errType = cmds.ErrClient + } + res.SetError(err, errType) + return + } + + res.SetOutput(output) + }, + Type: &Object{}, +} + +// objectData takes a key string and writes out the raw bytes of that node (if there is one) +func objectData(n *core.IpfsNode, key string) (io.Reader, error) { + dagnode, err := n.Resolver.ResolvePath(key) + if err != nil { + return nil, err + } + + log.Debugf("objectData: found dagnode %q (# of bytes: %d - # links: %d)", key, len(dagnode.Data), len(dagnode.Links)) + + return bytes.NewReader(dagnode.Data), nil +} + +// objectLinks takes a key string and lists the links it points to +func objectLinks(n *core.IpfsNode, key string) (*Object, error) { + dagnode, err := n.Resolver.ResolvePath(key) + if err != nil { + return nil, err + } + + log.Debugf("objectLinks: found dagnode %q (# of bytes: %d - # links: %d)", key, len(dagnode.Data), len(dagnode.Links)) + + return getOutput(dagnode) +} + +// objectGet takes a key string from args and a format option and serializes the dagnode to that format +func objectGet(n *core.IpfsNode, key string) (*dag.Node, error) { + dagnode, err := n.Resolver.ResolvePath(key) + if err != nil { + return nil, err + } + + log.Debugf("objectGet: found dagnode %q (# of bytes: %d - # links: %d)", key, len(dagnode.Data), len(dagnode.Links)) + + return dagnode, nil +} + +// objectPut takes a format option, serializes bytes from stdin and updates the dag with that data +func objectPut(n *core.IpfsNode, input io.Reader, encoding string) (*Object, error) { + var ( + dagnode *dag.Node + data []byte + err error + ) + + data, err = ioutil.ReadAll(io.LimitReader(input, inputLimit+10)) + if err != nil { + return nil, err + } + + if len(data) >= inputLimit { + return nil, ErrObjectTooLarge + } + + switch getObjectEnc(encoding) { + case objectEncodingJSON: + dagnode = new(dag.Node) + err = json.Unmarshal(data, dagnode) + + case objectEncodingProtobuf: + dagnode, err = dag.Decoded(data) + + default: + return nil, ErrUnknownObjectEnc + } + + if err != nil { + return nil, err + } + + err = addNode(n, dagnode) + if err != nil { + return nil, err + } + + return getOutput(dagnode) +} + +// ErrUnknownObjectEnc is returned if a invalid encoding is supplied +var ErrUnknownObjectEnc = errors.New("unknown object encoding") + +type objectEncoding string + +const ( + objectEncodingJSON objectEncoding = "json" + objectEncodingProtobuf = "protobuf" +) + +func getObjectEnc(o interface{}) objectEncoding { + v, ok := o.(string) + if !ok { + // chosen as default because it's human readable + log.Warning("option is not a string - falling back to json") + return objectEncodingJSON + } + + return objectEncoding(v) +} + +func getOutput(dagnode *dag.Node) (*Object, error) { + key, err := dagnode.Key() + if err != nil { + return nil, err + } + + output := &Object{ + Hash: key.Pretty(), + Links: make([]Link, len(dagnode.Links)), + } + + for i, link := range dagnode.Links { + output.Links[i] = Link{ + Name: link.Name, + Hash: link.Hash.B58String(), + Size: link.Size, + } + } + + return output, nil +} diff --git a/core/commands2/root.go b/core/commands2/root.go index 1afac44f43807fc33c913826ab8bef392455fcbb..e66c71cf69a1acf03d7f67d175f94b2f06ac9d52 100644 --- a/core/commands2/root.go +++ b/core/commands2/root.go @@ -70,6 +70,7 @@ var rootSubcommands = map[string]*cmds.Command{ "mount": mountCmd, "block": blockCmd, "update": updateCmd, + "object": objectCmd, } func init() {