object.go 15.4 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"
Łukasz Magiera's avatar
Łukasz Magiera committed
19
	pin "github.com/ipfs/go-ipfs/pin"
20
	ft "github.com/ipfs/go-ipfs/unixfs"
21

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

26 27
// 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")
28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

137 138 139 140 141 142
		// 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
143
		fpath := path.Path(req.Arguments()[0])
144
		node, err := core.Resolve(req.Context(), n.Namesys, n.Resolver, fpath)
145 146 147 148
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
149

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Examples:

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

335 336
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
337 338 339 340 341 342 343 344 345 346

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

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

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

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

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

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

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

Łukasz Magiera's avatar
Łukasz Magiera committed
386 387 388 389 390 391 392 393 394 395
		dopin, _, err := req.Option("pin").Bool()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		if dopin {
			defer n.Blockstore.PinLock().Unlock()
		}

396
		objectCid, err := objectPut(n, input, inputenc, datafieldenc)
397 398 399 400 401
		if err != nil {
			errType := cmds.ErrNormal
			if err == ErrUnknownObjectEnc {
				errType = cmds.ErrClient
			}
402 403
			res.SetError(err, errType)
			return
404 405
		}

Łukasz Magiera's avatar
Łukasz Magiera committed
406
		if dopin {
407
			n.Pinning.PinWithMode(objectCid, pin.Recursive)
Łukasz Magiera's avatar
Łukasz Magiera committed
408 409 410 411 412 413 414
			err = n.Pinning.Flush()
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
		}

415
		res.SetOutput(&Object{Hash: objectCid.String()})
416
	},
417
	Marshalers: cmds.MarshalerMap{
418
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
419 420
			object := res.Output().(*Object)
			return strings.NewReader("added " + object.Hash + "\n"), nil
421 422
		},
	},
423
	Type: Object{},
424 425
}

426
var ObjectNewCmd = &cmds.Command{
427
	Helptext: cmds.HelpText{
428
		Tagline: "Create a new object from an ipfs template.",
429 430 431 432 433 434 435 436
		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.
437 438 439

Available templates:
	* unixfs-dir
440 441 442
`,
	},
	Arguments: []cmds.Argument{
443
		cmds.StringArg("template", false, false, "Template to use. Optional."),
444 445
	},
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
446
		n, err := req.InvocContext().GetNode()
447 448 449 450 451
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

452
		node := new(dag.ProtoNode)
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
		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
468
		res.SetOutput(&Object{Hash: k.String()})
469 470 471 472 473 474 475 476 477 478
	},
	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{},
}

479
func nodeFromTemplate(template string) (*dag.ProtoNode, error) {
480 481
	switch template {
	case "unixfs-dir":
482
		return ft.EmptyDirNode(), nil
483 484 485 486 487
	default:
		return nil, fmt.Errorf("template '%s' not found", template)
	}
}

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

491
// objectPut takes a format option, serializes bytes from stdin and updates the dag with that data
492
func objectPut(n *core.IpfsNode, input io.Reader, encoding string, dataFieldEncoding string) (*cid.Cid, error) {
493

494
	data, err := ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
495 496 497 498 499 500 501 502
	if err != nil {
		return nil, err
	}

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

503
	var dagnode *dag.ProtoNode
504 505
	switch getObjectEnc(encoding) {
	case objectEncodingJSON:
506 507 508 509 510 511
		node := new(Node)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}

512 513
		// check that we have data in the Node to add
		// otherwise we will add the empty object without raising an error
514
		if NodeEmpty(node) {
515 516 517
			return nil, ErrEmptyNode
		}

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

	case objectEncodingProtobuf:
524
		dagnode, err = dag.DecodeProtobuf(data)
525

526 527 528 529 530 531 532 533 534 535 536 537 538
	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
		}

539
		dagnode, err = deserializeNode(node, dataFieldEncoding)
540 541 542 543
		if err != nil {
			return nil, err
		}

544 545 546 547 548 549 550 551
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

552
	_, err = n.DAG.Add(dagnode)
553 554 555 556
	if err != nil {
		return nil, err
	}

557
	return dagnode.Cid(), nil
558 559 560 561 562 563 564 565 566 567
}

// 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"
568
	objectEncodingXML                     = "xml"
569 570 571 572 573 574 575 576 577 578 579 580
)

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
581
func getOutput(dagnode node.Node) (*Object, error) {
Jeromy's avatar
Jeromy committed
582
	c := dagnode.Cid()
583
	output := &Object{
Jeromy's avatar
Jeromy committed
584
		Hash:  c.String(),
585
		Links: make([]Link, len(dagnode.Links())),
586 587
	}

588
	for i, link := range dagnode.Links() {
589 590
		output.Links[i] = Link{
			Name: link.Name,
591
			Hash: link.Cid.String(),
592 593 594 595 596 597
			Size: link.Size,
		}
	}

	return output, nil
}
598

599
// converts the Node object into a real dag.ProtoNode
Jeromy's avatar
Jeromy committed
600
func deserializeNode(nd *Node, dataFieldEncoding string) (*dag.ProtoNode, error) {
601
	dagnode := new(dag.ProtoNode)
slothbag's avatar
slothbag committed
602 603
	switch dataFieldEncoding {
	case "text":
Jeromy's avatar
Jeromy committed
604
		dagnode.SetData([]byte(nd.Data))
slothbag's avatar
slothbag committed
605
	case "base64":
Jeromy's avatar
Jeromy committed
606
		data, _ := base64.StdEncoding.DecodeString(nd.Data)
607
		dagnode.SetData(data)
slothbag's avatar
slothbag committed
608 609
	default:
		return nil, fmt.Errorf("Unkown data field encoding")
610 611
	}

Jeromy's avatar
Jeromy committed
612 613
	dagnode.SetLinks(make([]*node.Link, len(nd.Links)))
	for i, link := range nd.Links {
614
		c, err := cid.Decode(link.Hash)
615 616 617
		if err != nil {
			return nil, err
		}
Jeromy's avatar
Jeromy committed
618
		dagnode.Links()[i] = &node.Link{
619 620
			Name: link.Name,
			Size: link.Size,
621
			Cid:  c,
622 623 624 625 626
		}
	}

	return dagnode, nil
}
627 628 629 630

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