object.go 15 KB
Newer Older
1
package objectcmd
2 3 4

import (
	"bytes"
5
	"encoding/base64"
6
	"encoding/json"
7
	"encoding/xml"
8
	"errors"
9
	"fmt"
10 11
	"io"
	"io/ioutil"
12
	"strings"
13
	"text/tabwriter"
14

15 16 17 18
	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"
19
	ft "github.com/ipfs/go-ipfs/unixfs"
20

21 22
	cid "gx/ipfs/QmTprEaAA2A9bst5XH7exuyi5KzNMK3SEDNN8rBDnKWcUS/go-cid"
	node "gx/ipfs/QmYNyRZJBUYPNrLszFmrBrPJbsBh2vMsefz5gnDpB5M1P6/go-ipld-format"
23 24
)

25 26
// ErrObjectTooLarge is returned when too much data was read from stdin. current limit 2m
var ErrObjectTooLarge = errors.New("input object was too large. limit is 2mbytes")
27

kpcyrd's avatar
kpcyrd committed
28
const inputLimit = 2 << 20
29

30 31
type Node struct {
	Links []Link
32
	Data  string
33 34
}

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

type Object struct {
Jeromy's avatar
Jeromy committed
41 42
	Hash  string `json:"Hash,omitempty"`
	Links []Link `json:"Links,omitempty"`
43 44
}

45
var ObjectCmd = &cmds.Command{
46
	Helptext: cmds.HelpText{
47
		Tagline: "Interact with IPFS objects.",
Matt Bell's avatar
Matt Bell committed
48 49 50
		ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
51
	},
52 53

	Subcommands: map[string]*cmds.Command{
54
		"data":  ObjectDataCmd,
Jeromy's avatar
Jeromy committed
55
		"diff":  ObjectDiffCmd,
56
		"get":   ObjectGetCmd,
Richard Littauer's avatar
Richard Littauer committed
57
		"links": ObjectLinksCmd,
58 59
		"new":   ObjectNewCmd,
		"patch": ObjectPatchCmd,
Richard Littauer's avatar
Richard Littauer committed
60 61
		"put":   ObjectPutCmd,
		"stat":  ObjectStatCmd,
62 63 64
	},
}

65
var ObjectDataCmd = &cmds.Command{
66
	Helptext: cmds.HelpText{
67
		Tagline: "Output the raw bytes of an IPFS object.",
68
		ShortDescription: `
69 70
'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.
71 72
`,
		LongDescription: `
73 74
'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.
75

76 77
Note that the "--encoding" option does not affect the output, since the output
is the raw data of the object.
78
`,
79
	},
80 81

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

Jeromy's avatar
Jeromy committed
91 92 93 94 95 96
		fpath, err := path.ParsePath(req.Arguments()[0])
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

97
		node, err := core.Resolve(req.Context(), n.Namesys, n.Resolver, fpath)
98 99 100 101
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
102 103 104 105 106 107 108 109

		pbnode, ok := node.(*dag.ProtoNode)
		if !ok {
			res.SetError(dag.ErrNotProtobuf, cmds.ErrNormal)
			return
		}

		res.SetOutput(bytes.NewReader(pbnode.Data()))
110 111 112
	},
}

113
var ObjectLinksCmd = &cmds.Command{
114
	Helptext: cmds.HelpText{
115
		Tagline: "Output the links pointed to by the specified object.",
116
		ShortDescription: `
rht's avatar
rht committed
117
'ipfs object links' is a plumbing command for retrieving the links from
Matt Bell's avatar
Matt Bell committed
118 119
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
120 121
`,
	},
122 123

	Arguments: []cmds.Argument{
Jeromy's avatar
Jeromy committed
124
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
125
	},
palkeo's avatar
palkeo committed
126
	Options: []cmds.Option{
127
		cmds.BoolOption("headers", "v", "Print table headers (Hash, Size, Name).").Default(false),
palkeo's avatar
palkeo committed
128
	},
129
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
130
		n, err := req.InvocContext().GetNode()
131
		if err != nil {
132 133
			res.SetError(err, cmds.ErrNormal)
			return
134
		}
135

136 137 138 139 140 141
		// get options early -> exit early in case of error
		if _, _, err := req.Option("headers").Bool(); err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

Jeromy's avatar
Jeromy committed
142
		fpath := path.Path(req.Arguments()[0])
143
		node, err := core.Resolve(req.Context(), n.Namesys, n.Resolver, fpath)
144 145 146 147
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
148

149
		output, err := getOutput(node)
150 151 152 153 154
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(output)
155
	},
156
	Marshalers: cmds.MarshalerMap{
157
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
158
			object := res.Output().(*Object)
159 160
			buf := new(bytes.Buffer)
			w := tabwriter.NewWriter(buf, 1, 2, 1, ' ', 0)
161 162 163 164
			headers, _, _ := res.Request().Option("headers").Bool()
			if headers {
				fmt.Fprintln(w, "Hash\tSize\tName\t")
			}
165 166 167
			for _, link := range object.Links {
				fmt.Fprintf(w, "%s\t%v\t%s\t\n", link.Hash, link.Size, link.Name)
			}
168
			w.Flush()
169
			return buf, nil
170 171
		},
	},
172
	Type: Object{},
173 174
}

175
var ObjectGetCmd = &cmds.Command{
176
	Helptext: cmds.HelpText{
rht's avatar
rht committed
177
		Tagline: "Get and serialize the DAG node named by <key>.",
178
		ShortDescription: `
rht's avatar
rht committed
179
'ipfs object get' is a plumbing command for retrieving DAG nodes.
180 181
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
182 183
`,
		LongDescription: `
rht's avatar
rht committed
184
'ipfs object get' is a plumbing command for retrieving DAG nodes.
185 186
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
187

188 189 190 191
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
192
(Specified by the "--encoding" or "--enc" flag)`,
193
	},
194 195

	Arguments: []cmds.Argument{
Jeromy's avatar
Jeromy committed
196
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
197
	},
198
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
199
		n, err := req.InvocContext().GetNode()
200
		if err != nil {
201 202
			res.SetError(err, cmds.ErrNormal)
			return
203
		}
204

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

207
		object, err := core.Resolve(req.Context(), n.Namesys, n.Resolver, fpath)
208
		if err != nil {
209 210
			res.SetError(err, cmds.ErrNormal)
			return
211 212
		}

213 214 215 216 217 218
		pbo, ok := object.(*dag.ProtoNode)
		if !ok {
			res.SetError(dag.ErrNotProtobuf, cmds.ErrNormal)
			return
		}

219
		node := &Node{
220 221
			Links: make([]Link, len(object.Links())),
			Data:  string(pbo.Data()),
222 223
		}

224
		for i, link := range object.Links() {
225
			node.Links[i] = Link{
226
				Hash: link.Cid.String(),
227 228 229 230 231
				Name: link.Name,
				Size: link.Size,
			}
		}

232
		res.SetOutput(node)
233
	},
234
	Type: Node{},
235
	Marshalers: cmds.MarshalerMap{
236
		cmds.Protobuf: func(res cmds.Response) (io.Reader, error) {
237
			node := res.Output().(*Node)
238 239
			// deserialize the Data field as text as this was the standard behaviour
			object, err := deserializeNode(node, "text")
240 241
			if err != nil {
				return nil, err
242
			}
243 244 245 246 247 248

			marshaled, err := object.Marshal()
			if err != nil {
				return nil, err
			}
			return bytes.NewReader(marshaled), nil
249 250 251 252
		},
	},
}

253
var ObjectStatCmd = &cmds.Command{
254
	Helptext: cmds.HelpText{
rht's avatar
rht committed
255
		Tagline: "Get stats for the DAG node named by <key>.",
256 257 258 259 260 261 262 263 264 265 266 267 268
		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{
Jeromy's avatar
Jeromy committed
269
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
270
	},
271
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
272
		n, err := req.InvocContext().GetNode()
273
		if err != nil {
274 275
			res.SetError(err, cmds.ErrNormal)
			return
276 277
		}

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

280
		object, err := core.Resolve(req.Context(), n.Namesys, n.Resolver, fpath)
281
		if err != nil {
282 283
			res.SetError(err, cmds.ErrNormal)
			return
284 285 286 287
		}

		ns, err := object.Stat()
		if err != nil {
288 289
			res.SetError(err, cmds.ErrNormal)
			return
290 291
		}

292
		res.SetOutput(ns)
293
	},
Jeromy's avatar
Jeromy committed
294
	Type: node.NodeStat{},
295 296
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
Jeromy's avatar
Jeromy committed
297
			ns := res.Output().(*node.NodeStat)
298

299
			buf := new(bytes.Buffer)
300
			w := func(s string, n int) {
301
				fmt.Fprintf(buf, "%s: %d\n", s, n)
302 303 304 305 306 307 308
			}
			w("NumLinks", ns.NumLinks)
			w("BlockSize", ns.BlockSize)
			w("LinksSize", ns.LinksSize)
			w("DataSize", ns.DataSize)
			w("CumulativeSize", ns.CumulativeSize)

309
			return buf, nil
310 311 312 313
		},
	},
}

314
var ObjectPutCmd = &cmds.Command{
315
	Helptext: cmds.HelpText{
316
		Tagline: "Store input as a DAG object, print its key.",
317 318 319 320 321 322
		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.
323 324
It reads from stdin, and the output is a base58 encoded multihash.

Henry's avatar
Henry committed
325 326
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
327
	* "protobuf"
Henry's avatar
Henry committed
328
	* "json" (default)
Dylan Powers's avatar
Dylan Powers committed
329 330 331

Examples:

332
	$ echo '{ "Data": "abc" }' | ipfs object put
Dylan Powers's avatar
Dylan Powers committed
333

334 335
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:
Dylan Powers's avatar
Dylan Powers committed
336 337 338 339 340 341 342 343 344 345

    {
        "Data": "another",
        "Links": [ {
            "Name": "some link",
            "Hash": "QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCVVEcRTWJBmLgR39V",
            "Size": 8
        } ]
    }

346
And then run:
Dylan Powers's avatar
Dylan Powers committed
347

348
	$ ipfs object put node.json
349
`,
350
	},
351 352

	Arguments: []cmds.Argument{
353
		cmds.FileArg("data", true, false, "Data to be stored as a DAG object.").EnableStdin(),
Henry's avatar
Henry committed
354 355
	},
	Options: []cmds.Option{
356
		cmds.StringOption("inputenc", "Encoding type of input data. One of: {\"protobuf\", \"json\"}.").Default("json"),
slothbag's avatar
slothbag committed
357
		cmds.StringOption("datafieldenc", "Encoding type of the data field, either \"text\" or \"base64\".").Default("text"),
358
	},
359
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
360
		n, err := req.InvocContext().GetNode()
361
		if err != nil {
362 363
			res.SetError(err, cmds.ErrNormal)
			return
364
		}
365

366 367
		input, err := req.Files().NextFile()
		if err != nil && err != io.EOF {
368 369
			res.SetError(err, cmds.ErrNormal)
			return
370 371
		}

372
		inputenc, _, err := req.Option("inputenc").String()
Henry's avatar
Henry committed
373 374 375 376
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
377

378
		datafieldenc, _, err := req.Option("datafieldenc").String()
379 380 381 382
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
slothbag's avatar
slothbag committed
383

384
		output, err := objectPut(n, input, inputenc, datafieldenc)
385 386 387 388 389
		if err != nil {
			errType := cmds.ErrNormal
			if err == ErrUnknownObjectEnc {
				errType = cmds.ErrClient
			}
390 391
			res.SetError(err, errType)
			return
392 393
		}

394
		res.SetOutput(output)
395
	},
396
	Marshalers: cmds.MarshalerMap{
397
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
398
			object := res.Output().(*Object)
399
			return strings.NewReader("added " + object.Hash + "\n"), nil
400 401
		},
	},
402
	Type: Object{},
403 404
}

405
var ObjectNewCmd = &cmds.Command{
406
	Helptext: cmds.HelpText{
407
		Tagline: "Create a new object from an ipfs template.",
408 409 410 411 412 413 414 415
		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.
416 417 418

Available templates:
	* unixfs-dir
419 420 421
`,
	},
	Arguments: []cmds.Argument{
422
		cmds.StringArg("template", false, false, "Template to use. Optional."),
423 424
	},
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
425
		n, err := req.InvocContext().GetNode()
426 427 428 429 430
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

431
		node := new(dag.ProtoNode)
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
		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
		}
Jeromy's avatar
Jeromy committed
447
		res.SetOutput(&Object{Hash: k.String()})
448 449 450 451 452 453 454 455 456 457
	},
	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{},
}

458
func nodeFromTemplate(template string) (*dag.ProtoNode, error) {
459 460
	switch template {
	case "unixfs-dir":
461
		return ft.EmptyDirNode(), nil
462 463 464 465 466
	default:
		return nil, fmt.Errorf("template '%s' not found", template)
	}
}

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

470
// objectPut takes a format option, serializes bytes from stdin and updates the dag with that data
471
func objectPut(n *core.IpfsNode, input io.Reader, encoding string, dataFieldEncoding string) (*Object, error) {
472

473
	data, err := ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
474 475 476 477 478 479 480 481
	if err != nil {
		return nil, err
	}

	if len(data) >= inputLimit {
		return nil, ErrObjectTooLarge
	}

482
	var dagnode *dag.ProtoNode
483 484
	switch getObjectEnc(encoding) {
	case objectEncodingJSON:
485 486 487 488 489 490
		node := new(Node)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}

491 492
		// check that we have data in the Node to add
		// otherwise we will add the empty object without raising an error
493
		if NodeEmpty(node) {
494 495 496
			return nil, ErrEmptyNode
		}

497
		dagnode, err = deserializeNode(node, dataFieldEncoding)
498 499 500
		if err != nil {
			return nil, err
		}
501 502

	case objectEncodingProtobuf:
503
		dagnode, err = dag.DecodeProtobuf(data)
504

505 506 507 508 509 510 511 512 513 514 515 516 517
	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
		}

518
		dagnode, err = deserializeNode(node, dataFieldEncoding)
519 520 521 522
		if err != nil {
			return nil, err
		}

523 524 525 526 527 528 529 530
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

531
	_, err = n.DAG.Add(dagnode)
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
	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"
547
	objectEncodingXML                     = "xml"
548 549 550 551 552 553 554 555 556 557 558 559
)

func getObjectEnc(o interface{}) objectEncoding {
	v, ok := o.(string)
	if !ok {
		// chosen as default because it's human readable
		return objectEncodingJSON
	}

	return objectEncoding(v)
}

Jeromy's avatar
Jeromy committed
560
func getOutput(dagnode node.Node) (*Object, error) {
Jeromy's avatar
Jeromy committed
561
	c := dagnode.Cid()
562
	output := &Object{
Jeromy's avatar
Jeromy committed
563
		Hash:  c.String(),
564
		Links: make([]Link, len(dagnode.Links())),
565 566
	}

567
	for i, link := range dagnode.Links() {
568 569
		output.Links[i] = Link{
			Name: link.Name,
570
			Hash: link.Cid.String(),
571 572 573 574 575 576
			Size: link.Size,
		}
	}

	return output, nil
}
577

578
// converts the Node object into a real dag.ProtoNode
Jeromy's avatar
Jeromy committed
579
func deserializeNode(nd *Node, dataFieldEncoding string) (*dag.ProtoNode, error) {
580
	dagnode := new(dag.ProtoNode)
slothbag's avatar
slothbag committed
581 582
	switch dataFieldEncoding {
	case "text":
Jeromy's avatar
Jeromy committed
583
		dagnode.SetData([]byte(nd.Data))
slothbag's avatar
slothbag committed
584
	case "base64":
Jeromy's avatar
Jeromy committed
585
		data, _ := base64.StdEncoding.DecodeString(nd.Data)
586
		dagnode.SetData(data)
slothbag's avatar
slothbag committed
587 588
	default:
		return nil, fmt.Errorf("Unkown data field encoding")
589 590
	}

Jeromy's avatar
Jeromy committed
591 592
	dagnode.SetLinks(make([]*node.Link, len(nd.Links)))
	for i, link := range nd.Links {
593
		c, err := cid.Decode(link.Hash)
594 595 596
		if err != nil {
			return nil, err
		}
Jeromy's avatar
Jeromy committed
597
		dagnode.Links()[i] = &node.Link{
598 599
			Name: link.Name,
			Size: link.Size,
600
			Cid:  c,
601 602 603 604 605
		}
	}

	return dagnode, nil
}
606 607 608 609

func NodeEmpty(node *Node) bool {
	return (node.Data == "" && len(node.Links) == 0)
}