object.go 15.5 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
		output, 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 407 408 409 410 411 412 413 414 415 416 417 418 419 420
		if dopin {
			c, err := cid.Decode(output.Hash)
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}

			n.Pinning.PinWithMode(c, pin.Recursive)
			err = n.Pinning.Flush()
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
		}

421
		res.SetOutput(output)
422
	},
423
	Marshalers: cmds.MarshalerMap{
424
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
425
			object := res.Output().(*Object)
426
			return strings.NewReader("added " + object.Hash + "\n"), nil
427 428
		},
	},
429
	Type: Object{},
430 431
}

432
var ObjectNewCmd = &cmds.Command{
433
	Helptext: cmds.HelpText{
434
		Tagline: "Create a new object from an ipfs template.",
435 436 437 438 439 440 441 442
		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.
443 444 445

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

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

485
func nodeFromTemplate(template string) (*dag.ProtoNode, error) {
486 487
	switch template {
	case "unixfs-dir":
488
		return ft.EmptyDirNode(), nil
489 490 491 492 493
	default:
		return nil, fmt.Errorf("template '%s' not found", template)
	}
}

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

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

500
	data, err := ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
501 502 503 504 505 506 507 508
	if err != nil {
		return nil, err
	}

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

509
	var dagnode *dag.ProtoNode
510 511
	switch getObjectEnc(encoding) {
	case objectEncodingJSON:
512 513 514 515 516 517
		node := new(Node)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}

518 519
		// check that we have data in the Node to add
		// otherwise we will add the empty object without raising an error
520
		if NodeEmpty(node) {
521 522 523
			return nil, ErrEmptyNode
		}

524
		dagnode, err = deserializeNode(node, dataFieldEncoding)
525 526 527
		if err != nil {
			return nil, err
		}
528 529

	case objectEncodingProtobuf:
530
		dagnode, err = dag.DecodeProtobuf(data)
531

532 533 534 535 536 537 538 539 540 541 542 543 544
	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
		}

545
		dagnode, err = deserializeNode(node, dataFieldEncoding)
546 547 548 549
		if err != nil {
			return nil, err
		}

550 551 552 553 554 555 556 557
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

558
	_, err = n.DAG.Add(dagnode)
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
	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"
574
	objectEncodingXML                     = "xml"
575 576 577 578 579 580 581 582 583 584 585 586
)

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
587
func getOutput(dagnode node.Node) (*Object, error) {
Jeromy's avatar
Jeromy committed
588
	c := dagnode.Cid()
589
	output := &Object{
Jeromy's avatar
Jeromy committed
590
		Hash:  c.String(),
591
		Links: make([]Link, len(dagnode.Links())),
592 593
	}

594
	for i, link := range dagnode.Links() {
595 596
		output.Links[i] = Link{
			Name: link.Name,
597
			Hash: link.Cid.String(),
598 599 600 601 602 603
			Size: link.Size,
		}
	}

	return output, nil
}
604

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

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

	return dagnode, nil
}
633 634 635 636

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