object.go 12.3 KB
Newer Older
1 2 3 4 5 6
package commands

import (
	"bytes"
	"encoding/json"
	"errors"
7
	"fmt"
8 9
	"io"
	"io/ioutil"
10
	"strings"
11
	"text/tabwriter"
12

13 14
	mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"

15
	cmds "github.com/jbenet/go-ipfs/commands"
16
	core "github.com/jbenet/go-ipfs/core"
17
	dag "github.com/jbenet/go-ipfs/merkledag"
Jeromy's avatar
Jeromy committed
18
	path "github.com/jbenet/go-ipfs/path"
19 20 21 22 23 24 25
)

// 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

26 27
type Node struct {
	Links []Link
28
	Data  string
29 30
}

31 32 33 34 35 36 37 38 39 40
type Link struct {
	Name, Hash string
	Size       uint64
}

type Object struct {
	Hash  string
	Links []Link
}

41
var ObjectCmd = &cmds.Command{
42
	Helptext: cmds.HelpText{
Matt Bell's avatar
Matt Bell committed
43 44 45 46 47
		Tagline: "Interact with ipfs objects",
		ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
		Synopsis: `
Henry's avatar
Henry committed
48 49 50 51 52
ipfs object get <key>   - Get the DAG node named by <key>
ipfs object put <data>  - Stores input, outputs its key
ipfs object data <key>  - Outputs raw bytes in an object
ipfs object links <key> - Outputs links pointed to by object
ipfs object stat <key>  - Outputs statistics of object
Matt Bell's avatar
Matt Bell committed
53
`,
54
	},
55 56 57 58 59 60

	Subcommands: map[string]*cmds.Command{
		"data":  objectDataCmd,
		"links": objectLinksCmd,
		"get":   objectGetCmd,
		"put":   objectPutCmd,
61
		"stat":  objectStatCmd,
62 63 64 65
	},
}

var objectDataCmd = &cmds.Command{
66 67 68
	Helptext: cmds.HelpText{
		Tagline: "Outputs the raw bytes in an IPFS object",
		ShortDescription: `
69 70 71 72 73 74 75 76
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.
`,
		LongDescription: `
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.
77 78 79 80

Note that the "--encoding" option does not affect the output, since the
output is the raw data of the object.
`,
81
	},
82 83

	Arguments: []cmds.Argument{
84
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format").EnableStdin(),
85
	},
86
	Run: func(req cmds.Request, res cmds.Response) {
87 88
		n, err := req.Context().GetNode()
		if err != nil {
89 90
			res.SetError(err, cmds.ErrNormal)
			return
91
		}
92

Jeromy's avatar
Jeromy committed
93 94
		fpath := path.Path(req.Arguments()[0])
		output, err := objectData(n, fpath)
95 96 97 98 99
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(output)
100 101 102 103
	},
}

var objectLinksCmd = &cmds.Command{
104 105 106
	Helptext: cmds.HelpText{
		Tagline: "Outputs the links pointed to by the specified object",
		ShortDescription: `
Matt Bell's avatar
Matt Bell committed
107 108 109
'ipfs object links' is a plumbing command for retreiving the links from
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
110 111
`,
	},
112 113

	Arguments: []cmds.Argument{
114
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format").EnableStdin(),
115
	},
116
	Run: func(req cmds.Request, res cmds.Response) {
117 118
		n, err := req.Context().GetNode()
		if err != nil {
119 120
			res.SetError(err, cmds.ErrNormal)
			return
121
		}
122

Jeromy's avatar
Jeromy committed
123 124
		fpath := path.Path(req.Arguments()[0])
		output, err := objectLinks(n, fpath)
125 126 127 128 129
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(output)
130
	},
131
	Marshalers: cmds.MarshalerMap{
132
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
133
			object := res.Output().(*Object)
134 135
			var buf bytes.Buffer
			w := tabwriter.NewWriter(&buf, 1, 2, 1, ' ', 0)
136 137 138 139
			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)
			}
140 141
			w.Flush()
			return &buf, nil
142 143
		},
	},
144
	Type: Object{},
145 146 147
}

var objectGetCmd = &cmds.Command{
148 149 150 151
	Helptext: cmds.HelpText{
		Tagline: "Get and serialize the DAG node named by <key>",
		ShortDescription: `
'ipfs object get' is a plumbing command for retreiving DAG nodes.
152 153
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
154 155 156
`,
		LongDescription: `
'ipfs object get' is a plumbing command for retreiving DAG nodes.
157 158
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
159

160 161 162 163
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
164
(Specified by the "--encoding" or "-enc" flag)`,
165
	},
166 167

	Arguments: []cmds.Argument{
168
		cmds.StringArg("key", true, false, "Key of the object to retrieve (in base58-encoded multihash format)").EnableStdin(),
169
	},
170
	Run: func(req cmds.Request, res cmds.Response) {
171 172
		n, err := req.Context().GetNode()
		if err != nil {
173 174
			res.SetError(err, cmds.ErrNormal)
			return
175
		}
176

Jeromy's avatar
Jeromy committed
177
		fpath := path.Path(req.Arguments()[0])
178

Jeromy's avatar
Jeromy committed
179
		object, err := objectGet(n, fpath)
180
		if err != nil {
181 182
			res.SetError(err, cmds.ErrNormal)
			return
183 184
		}

185 186
		node := &Node{
			Links: make([]Link, len(object.Links)),
187
			Data:  string(object.Data),
188 189 190 191 192 193 194 195 196 197
		}

		for i, link := range object.Links {
			node.Links[i] = Link{
				Hash: link.Hash.B58String(),
				Name: link.Name,
				Size: link.Size,
			}
		}

198
		res.SetOutput(node)
199
	},
200
	Type: Node{},
201
	Marshalers: cmds.MarshalerMap{
202
		cmds.EncodingType("protobuf"): func(res cmds.Response) (io.Reader, error) {
203
			node := res.Output().(*Node)
204 205 206
			object, err := deserializeNode(node)
			if err != nil {
				return nil, err
207
			}
208 209 210 211 212 213

			marshaled, err := object.Marshal()
			if err != nil {
				return nil, err
			}
			return bytes.NewReader(marshaled), nil
214 215 216 217
		},
	},
}

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
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{
234
		cmds.StringArg("key", true, false, "Key of the object to retrieve (in base58-encoded multihash format)").EnableStdin(),
235
	},
236
	Run: func(req cmds.Request, res cmds.Response) {
237 238
		n, err := req.Context().GetNode()
		if err != nil {
239 240
			res.SetError(err, cmds.ErrNormal)
			return
241 242
		}

Jeromy's avatar
Jeromy committed
243
		fpath := path.Path(req.Arguments()[0])
244

Jeromy's avatar
Jeromy committed
245
		object, err := objectGet(n, fpath)
246
		if err != nil {
247 248
			res.SetError(err, cmds.ErrNormal)
			return
249 250 251 252
		}

		ns, err := object.Stat()
		if err != nil {
253 254
			res.SetError(err, cmds.ErrNormal)
			return
255 256
		}

257
		res.SetOutput(ns)
258 259 260 261
	},
	Type: dag.NodeStat{},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
262
			ns := res.Output().(*dag.NodeStat)
263 264 265

			var buf bytes.Buffer
			w := func(s string, n int) {
266
				fmt.Fprintf(&buf, "%s: %d\n", s, n)
267 268 269 270 271 272 273 274 275 276 277 278
			}
			w("NumLinks", ns.NumLinks)
			w("BlockSize", ns.BlockSize)
			w("LinksSize", ns.LinksSize)
			w("DataSize", ns.DataSize)
			w("CumulativeSize", ns.CumulativeSize)

			return &buf, nil
		},
	},
}

279
var objectPutCmd = &cmds.Command{
280 281 282 283 284 285 286 287
	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.
288 289
It reads from stdin, and the output is a base58 encoded multihash.

Henry's avatar
Henry committed
290 291
Data should be in the format specified by the --inputenc flag.
--inputenc may be one of the following:
Matt Bell's avatar
Matt Bell committed
292
	* "protobuf"
Henry's avatar
Henry committed
293
	* "json" (default)
294
`,
295
	},
296 297

	Arguments: []cmds.Argument{
Henry's avatar
Henry committed
298 299 300 301
		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\""),
302
	},
303
	Run: func(req cmds.Request, res cmds.Response) {
304 305
		n, err := req.Context().GetNode()
		if err != nil {
306 307
			res.SetError(err, cmds.ErrNormal)
			return
308
		}
309

310 311
		input, err := req.Files().NextFile()
		if err != nil && err != io.EOF {
312 313
			res.SetError(err, cmds.ErrNormal)
			return
314 315
		}

Henry's avatar
Henry committed
316 317 318 319 320 321 322 323
		inputenc, found, err := req.Option("inputenc").String()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		if !found {
			inputenc = "json"
		}
324

Henry's avatar
Henry committed
325
		output, err := objectPut(n, input, inputenc)
326 327 328 329 330
		if err != nil {
			errType := cmds.ErrNormal
			if err == ErrUnknownObjectEnc {
				errType = cmds.ErrClient
			}
331 332
			res.SetError(err, errType)
			return
333 334
		}

335
		res.SetOutput(output)
336
	},
337
	Marshalers: cmds.MarshalerMap{
338
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
339
			object := res.Output().(*Object)
340
			return strings.NewReader("added " + object.Hash), nil
341 342
		},
	},
343
	Type: Object{},
344 345 346
}

// objectData takes a key string and writes out the raw bytes of that node (if there is one)
Jeromy's avatar
Jeromy committed
347 348
func objectData(n *core.IpfsNode, fpath path.Path) (io.Reader, error) {
	dagnode, err := n.Resolver.ResolvePath(fpath)
349 350 351 352
	if err != nil {
		return nil, err
	}

Jeromy's avatar
Jeromy committed
353
	log.Debugf("objectData: found dagnode %s (# of bytes: %d - # links: %d)", fpath, len(dagnode.Data), len(dagnode.Links))
354 355 356 357 358

	return bytes.NewReader(dagnode.Data), nil
}

// objectLinks takes a key string and lists the links it points to
Jeromy's avatar
Jeromy committed
359 360
func objectLinks(n *core.IpfsNode, fpath path.Path) (*Object, error) {
	dagnode, err := n.Resolver.ResolvePath(fpath)
361 362 363 364
	if err != nil {
		return nil, err
	}

Jeromy's avatar
Jeromy committed
365
	log.Debugf("objectLinks: found dagnode %s (# of bytes: %d - # links: %d)", fpath, len(dagnode.Data), len(dagnode.Links))
366 367 368 369 370

	return getOutput(dagnode)
}

// objectGet takes a key string from args and a format option and serializes the dagnode to that format
Jeromy's avatar
Jeromy committed
371 372
func objectGet(n *core.IpfsNode, fpath path.Path) (*dag.Node, error) {
	dagnode, err := n.Resolver.ResolvePath(fpath)
373 374 375 376
	if err != nil {
		return nil, err
	}

Jeromy's avatar
Jeromy committed
377
	log.Debugf("objectGet: found dagnode %s (# of bytes: %d - # links: %d)", fpath, len(dagnode.Data), len(dagnode.Links))
378 379 380 381

	return dagnode, nil
}

382 383 384
// ErrEmptyNode is returned when the input to 'ipfs object put' contains no data
var ErrEmptyNode = errors.New("no data or links in this node")

385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
// 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:
404 405 406 407 408 409
		node := new(Node)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}

410 411 412 413 414 415
		// check that we have data in the Node to add
		// otherwise we will add the empty object without raising an error
		if node.Data == "" && len(node.Links) == 0 {
			return nil, ErrEmptyNode
		}

416 417 418 419
		dagnode, err = deserializeNode(node)
		if err != nil {
			return nil, err
		}
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481

	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
}
482 483 484 485

// converts the Node object into a real dag.Node
func deserializeNode(node *Node) (*dag.Node, error) {
	dagnode := new(dag.Node)
486
	dagnode.Data = []byte(node.Data)
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
	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
}