From c542cb52aa082d2e346979e02c30996accfb7d93 Mon Sep 17 00:00:00 2001
From: Matt Bell <mappum@gmail.com>
Date: Sat, 8 Nov 2014 19:56:44 -0800
Subject: [PATCH] core/commands2: Added 'object' command

---
 core/commands2/add.go    |  21 ++-
 core/commands2/object.go | 297 +++++++++++++++++++++++++++++++++++++++
 core/commands2/root.go   |   1 +
 3 files changed, 313 insertions(+), 6 deletions(-)
 create mode 100644 core/commands2/object.go

diff --git a/core/commands2/add.go b/core/commands2/add.go
index 104f506ad..c96a920c4 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 000000000..3affff3c7
--- /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 1afac44f4..e66c71cf6 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() {
-- 
GitLab