kubo/core/commands/object/object.go
Stephen Whitmore 0a45ada4ed CLI: discoverability and consistency (#2542)
* reduces help indent from 4 to 2 spaces

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* reduces horz/vert space taken by "ipfs" cmd

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* show subcommands on shorthelp

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Drops colons at the end of cmd headings.

This makes command headings consistent with the output of 'ipfs', which
does not include colons.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* more consistent output between short-/long-help

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Puts DESCRIPTION before SUBCOMMANDS.

Users likely want to understand what a command does before worrying
about its subcommands.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Keeps ipfs cmd from outputting its subcmds twice.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes redundant synopsis from "file"

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes extra whitespace from longhelp

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Consistent spacing whether SUBCMDS or not.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes redundant SUBCMD output from ipfs object.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes redundant synopsis from "name"

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Newline after Description only if it exists.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes redundant synopsis from "bootstrap"

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes redundant synopsis from "swarm"

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes trailing newline in ping help.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Prints shorthelp on parse error.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* tiny comment fixes

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* updates README usage

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Don't include extra whitespace if no .MoreHelp

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* description improvements

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Hides the obscure 'file' subcommand.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Splits 'ipfs daemon' into Short and Long help.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes redundant synopsis from "config"

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Wraps lines to keep from going over 80.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* specify repo separately

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* s/structure/hierarchy

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* missing .

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes trailing colon from 'usage' test.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Updates sharness test error messages.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes trailing colon from 'usage' test.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Updates add-symlink to use /bin/sh.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Removes "hierarchy".

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>

* Updates "ipfs ping" synopsis.

* Updates t0040 with latest wording.

* Removes unnecessary daemon setup.

License: MIT
Signed-off-by: Stephen Whitmore <noffle@ipfs.io>
2016-04-28 14:08:32 -07:00

600 lines
15 KiB
Go

package objectcmd
import (
"bytes"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
"text/tabwriter"
mh "gx/ipfs/QmYf7ng2hG5XBtJA3tN34DQ2GUN5HNksEw1rLDkmr6vGku/go-multihash"
cmds "github.com/ipfs/go-ipfs/commands"
core "github.com/ipfs/go-ipfs/core"
dag "github.com/ipfs/go-ipfs/merkledag"
path "github.com/ipfs/go-ipfs/path"
ft "github.com/ipfs/go-ipfs/unixfs"
)
// 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
type Node struct {
Links []Link
Data string
}
type Link struct {
Name, Hash string
Size uint64
}
type Object struct {
Hash string `json:"Hash,omitempty"`
Links []Link `json:"Links,omitempty"`
}
var ObjectCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Interact with ipfs objects.",
ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
},
Subcommands: map[string]*cmds.Command{
"data": ObjectDataCmd,
"diff": ObjectDiffCmd,
"get": ObjectGetCmd,
"links": ObjectLinksCmd,
"new": ObjectNewCmd,
"patch": ObjectPatchCmd,
"put": ObjectPutCmd,
"stat": ObjectStatCmd,
},
}
var ObjectDataCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Outputs the raw bytes in an IPFS object.",
ShortDescription: `
'ipfs object data' is a plumbing command for retrieving the raw bytes stored in
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
`,
LongDescription: `
'ipfs object data' is a plumbing command for retrieving 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.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
fpath := path.Path(req.Arguments()[0])
node, err := core.Resolve(req.Context(), n, fpath)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
res.SetOutput(bytes.NewReader(node.Data))
},
}
var ObjectLinksCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Outputs the links pointed to by the specified object.",
ShortDescription: `
'ipfs object links' is a plumbing command for retrieving the links from
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
`,
},
Arguments: []cmds.Argument{
cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
},
Options: []cmds.Option{
cmds.BoolOption("headers", "v", "Print table headers (Hash, Size, Name)."),
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
// get options early -> exit early in case of error
if _, _, err := req.Option("headers").Bool(); err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
fpath := path.Path(req.Arguments()[0])
node, err := core.Resolve(req.Context(), n, fpath)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
output, err := getOutput(node)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
res.SetOutput(output)
},
Marshalers: cmds.MarshalerMap{
cmds.Text: func(res cmds.Response) (io.Reader, error) {
object := res.Output().(*Object)
buf := new(bytes.Buffer)
w := tabwriter.NewWriter(buf, 1, 2, 1, ' ', 0)
headers, _, _ := res.Request().Option("headers").Bool()
if headers {
fmt.Fprintln(w, "Hash\tSize\tName\t")
}
for _, link := range object.Links {
fmt.Fprintf(w, "%s\t%v\t%s\t\n", link.Hash, link.Size, link.Name)
}
w.Flush()
return buf, nil
},
},
Type: Object{},
}
var ObjectGetCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Get and serialize the DAG node named by <key>.",
ShortDescription: `
'ipfs object get' is a plumbing command for retrieving 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.
`,
LongDescription: `
'ipfs object get' is a plumbing command for retrieving 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" or "-enc" flag)`,
},
Arguments: []cmds.Argument{
cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
fpath := path.Path(req.Arguments()[0])
object, err := core.Resolve(req.Context(), n, fpath)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
node := &Node{
Links: make([]Link, len(object.Links)),
Data: string(object.Data),
}
for i, link := range object.Links {
node.Links[i] = Link{
Hash: link.Hash.B58String(),
Name: link.Name,
Size: link.Size,
}
}
res.SetOutput(node)
},
Type: Node{},
Marshalers: cmds.MarshalerMap{
cmds.EncodingType("protobuf"): func(res cmds.Response) (io.Reader, error) {
node := res.Output().(*Node)
// deserialize the Data field as text as this was the standard behaviour
object, err := deserializeNode(node, "text")
if err != nil {
return nil, err
}
marshaled, err := object.Marshal()
if err != nil {
return nil, err
}
return bytes.NewReader(marshaled), nil
},
},
}
var ObjectStatCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Get stats for the DAG node named by <key>.",
ShortDescription: `
'ipfs object stat' is a plumbing command to print DAG node statistics.
<key> is a base58 encoded multihash. It outputs to stdout:
NumLinks int number of links in link table
BlockSize int size of the raw, encoded data
LinksSize int size of the links segment
DataSize int size of the data segment
CumulativeSize int cumulative size of object and its references
`,
},
Arguments: []cmds.Argument{
cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
fpath := path.Path(req.Arguments()[0])
object, err := core.Resolve(req.Context(), n, fpath)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
ns, err := object.Stat()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
res.SetOutput(ns)
},
Type: dag.NodeStat{},
Marshalers: cmds.MarshalerMap{
cmds.Text: func(res cmds.Response) (io.Reader, error) {
ns := res.Output().(*dag.NodeStat)
buf := new(bytes.Buffer)
w := func(s string, n int) {
fmt.Fprintf(buf, "%s: %d\n", s, n)
}
w("NumLinks", ns.NumLinks)
w("BlockSize", ns.BlockSize)
w("LinksSize", ns.LinksSize)
w("DataSize", ns.DataSize)
w("CumulativeSize", ns.CumulativeSize)
return buf, nil
},
},
}
var ObjectPutCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Stores input as a DAG object, outputs its key.",
ShortDescription: `
'ipfs object put' is a plumbing command for storing DAG nodes.
It reads from stdin, and the output is a base58 encoded multihash.
`,
LongDescription: `
'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 the --inputenc flag.
--inputenc may be one of the following:
* "protobuf"
* "json" (default)
Examples:
$ echo '{ "Data": "abc" }' | ipfs object put
This creates a node with the data 'abc' and no links. For an object with links,
create a file named 'node.json' with the contents:
{
"Data": "another",
"Links": [ {
"Name": "some link",
"Hash": "QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V",
"Size": 8
} ]
}
And then run:
$ ipfs object put node.json
`,
},
Arguments: []cmds.Argument{
cmds.FileArg("data", true, false, "Data to be stored as a DAG object.").EnableStdin(),
},
Options: []cmds.Option{
cmds.StringOption("inputenc", "Encoding type of input data, either \"protobuf\" or \"json\"."),
cmds.StringOption("datafieldenc", "Encoding type of the data field, either \"text\" or \"base64\".").Default("text"),
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
input, err := req.Files().NextFile()
if err != nil && err != io.EOF {
res.SetError(err, cmds.ErrNormal)
return
}
inputenc, found, err := req.Option("inputenc").String()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
if !found {
inputenc = "json"
}
datafieldenc, found, err := req.Option("datafieldenc").String()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
output, err := objectPut(n, input, inputenc, datafieldenc)
if err != nil {
errType := cmds.ErrNormal
if err == ErrUnknownObjectEnc {
errType = cmds.ErrClient
}
res.SetError(err, errType)
return
}
res.SetOutput(output)
},
Marshalers: cmds.MarshalerMap{
cmds.Text: func(res cmds.Response) (io.Reader, error) {
object := res.Output().(*Object)
return strings.NewReader("added " + object.Hash + "\n"), nil
},
},
Type: Object{},
}
var ObjectNewCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Creates a new object from an ipfs template.",
ShortDescription: `
'ipfs object new' is a plumbing command for creating new DAG nodes.
`,
LongDescription: `
'ipfs object new' is a plumbing command for creating new DAG nodes.
By default it creates and returns a new empty merkledag node, but
you may pass an optional template argument to create a preformatted
node.
Available templates:
* unixfs-dir
`,
},
Arguments: []cmds.Argument{
cmds.StringArg("template", false, false, "Template to use. Optional."),
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
node := new(dag.Node)
if len(req.Arguments()) == 1 {
template := req.Arguments()[0]
var err error
node, err = nodeFromTemplate(template)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
}
k, err := n.DAG.Add(node)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
res.SetOutput(&Object{Hash: k.B58String()})
},
Marshalers: cmds.MarshalerMap{
cmds.Text: func(res cmds.Response) (io.Reader, error) {
object := res.Output().(*Object)
return strings.NewReader(object.Hash + "\n"), nil
},
},
Type: Object{},
}
func nodeFromTemplate(template string) (*dag.Node, error) {
switch template {
case "unixfs-dir":
nd := new(dag.Node)
nd.Data = ft.FolderPBData()
return nd, nil
default:
return nil, fmt.Errorf("template '%s' not found", template)
}
}
// ErrEmptyNode is returned when the input to 'ipfs object put' contains no data
var ErrEmptyNode = errors.New("no data or links in this node")
// 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, dataFieldEncoding string) (*Object, error) {
data, err := ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
if err != nil {
return nil, err
}
if len(data) >= inputLimit {
return nil, ErrObjectTooLarge
}
var dagnode *dag.Node
switch getObjectEnc(encoding) {
case objectEncodingJSON:
node := new(Node)
err = json.Unmarshal(data, node)
if err != nil {
return nil, err
}
// check that we have data in the Node to add
// otherwise we will add the empty object without raising an error
if NodeEmpty(node) {
return nil, ErrEmptyNode
}
dagnode, err = deserializeNode(node, dataFieldEncoding)
if err != nil {
return nil, err
}
case objectEncodingProtobuf:
dagnode, err = dag.DecodeProtobuf(data)
case objectEncodingXML:
node := new(Node)
err = xml.Unmarshal(data, node)
if err != nil {
return nil, err
}
// check that we have data in the Node to add
// otherwise we will add the empty object without raising an error
if NodeEmpty(node) {
return nil, ErrEmptyNode
}
dagnode, err = deserializeNode(node, dataFieldEncoding)
if err != nil {
return nil, err
}
default:
return nil, ErrUnknownObjectEnc
}
if err != nil {
return nil, err
}
_, err = n.DAG.Add(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"
objectEncodingXML = "xml"
)
func getObjectEnc(o interface{}) objectEncoding {
v, ok := o.(string)
if !ok {
// chosen as default because it's human readable
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.B58String(),
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
}
// converts the Node object into a real dag.Node
func deserializeNode(node *Node, dataFieldEncoding string) (*dag.Node, error) {
dagnode := new(dag.Node)
switch dataFieldEncoding {
case "text":
dagnode.Data = []byte(node.Data)
case "base64":
dagnode.Data, _ = base64.StdEncoding.DecodeString(node.Data)
default:
return nil, fmt.Errorf("Unkown data field encoding")
}
dagnode.Links = make([]*dag.Link, len(node.Links))
for i, link := range node.Links {
hash, err := mh.FromB58String(link.Hash)
if err != nil {
return nil, err
}
dagnode.Links[i] = &dag.Link{
Name: link.Name,
Size: link.Size,
Hash: hash,
}
}
return dagnode, nil
}
func NodeEmpty(node *Node) bool {
return (node.Data == "" && len(node.Links) == 0)
}