object.go 15.1 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
	mh "gx/ipfs/QmYf7ng2hG5XBtJA3tN34DQ2GUN5HNksEw1rLDkmr6vGku/go-multihash"
16

17 18 19 20
	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"
21
	ft "github.com/ipfs/go-ipfs/unixfs"
22 23 24 25 26 27 28
)

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

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

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

type Object struct {
	Hash  string
	Links []Link
}

44
var ObjectCmd = &cmds.Command{
45
	Helptext: cmds.HelpText{
rht's avatar
rht committed
46
		Tagline: "Interact with ipfs objects.",
Matt Bell's avatar
Matt Bell committed
47 48 49 50
		ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
		Synopsis: `
Jeromy's avatar
Jeromy committed
51 52 53 54 55 56 57 58
ipfs object data <key>           - Outputs raw bytes in an object
ipfs object get <key>            - Get the DAG node named by <key>
ipfs object links <key>          - Outputs links pointed to by object
ipfs object new <template>       - Create new ipfs objects
ipfs object patch <args>         - Create new object from old ones
ipfs object put <data>           - Stores input, outputs its key
ipfs object stat <key>           - Outputs statistics of object
ipfs object diff <key1> <key2>   - Diffs two given objects
Matt Bell's avatar
Matt Bell committed
59
`,
60
	},
61 62

	Subcommands: map[string]*cmds.Command{
63 64
		"data":  ObjectDataCmd,
		"get":   ObjectGetCmd,
Richard Littauer's avatar
Richard Littauer committed
65
		"links": ObjectLinksCmd,
66 67
		"new":   ObjectNewCmd,
		"patch": ObjectPatchCmd,
Richard Littauer's avatar
Richard Littauer committed
68 69
		"put":   ObjectPutCmd,
		"stat":  ObjectStatCmd,
Jeromy's avatar
Jeromy committed
70
		"diff":  ObjectDiffCmd,
71 72 73
	},
}

74
var ObjectDataCmd = &cmds.Command{
75
	Helptext: cmds.HelpText{
rht's avatar
rht committed
76
		Tagline: "Outputs the raw bytes in an IPFS object.",
77
		ShortDescription: `
rht's avatar
rht committed
78
'ipfs object data' is a plumbing command for retrieving the raw bytes stored in
79 80 81 82
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
`,
		LongDescription: `
rht's avatar
rht committed
83
'ipfs object data' is a plumbing command for retrieving the raw bytes stored in
84 85
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
86 87 88 89

Note that the "--encoding" option does not affect the output, since the
output is the raw data of the object.
`,
90
	},
91 92

	Arguments: []cmds.Argument{
93
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
94
	},
95
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
96
		n, err := req.InvocContext().GetNode()
97
		if err != nil {
98 99
			res.SetError(err, cmds.ErrNormal)
			return
100
		}
101

Jeromy's avatar
Jeromy committed
102
		fpath := path.Path(req.Arguments()[0])
Jeromy's avatar
Jeromy committed
103
		node, err := core.Resolve(req.Context(), n, fpath)
104 105 106 107
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
108
		res.SetOutput(bytes.NewReader(node.Data))
109 110 111
	},
}

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

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

135 136 137 138 139 140
		// 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
141
		fpath := path.Path(req.Arguments()[0])
Jeromy's avatar
Jeromy committed
142
		node, err := core.Resolve(req.Context(), n, fpath)
143 144 145 146 147
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		output, err := getOutput(node)
148 149 150 151 152
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(output)
153
	},
154
	Marshalers: cmds.MarshalerMap{
155
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
156
			object := res.Output().(*Object)
157 158
			buf := new(bytes.Buffer)
			w := tabwriter.NewWriter(buf, 1, 2, 1, ' ', 0)
159 160 161 162
			headers, _, _ := res.Request().Option("headers").Bool()
			if headers {
				fmt.Fprintln(w, "Hash\tSize\tName\t")
			}
163 164 165
			for _, link := range object.Links {
				fmt.Fprintf(w, "%s\t%v\t%s\t\n", link.Hash, link.Size, link.Name)
			}
166
			w.Flush()
167
			return buf, nil
168 169
		},
	},
170
	Type: Object{},
171 172
}

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

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

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

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

Jeromy's avatar
Jeromy committed
205
		object, err := core.Resolve(req.Context(), n, fpath)
206
		if err != nil {
207 208
			res.SetError(err, cmds.ErrNormal)
			return
209 210
		}

211 212
		node := &Node{
			Links: make([]Link, len(object.Links)),
213
			Data:  string(object.Data),
214 215 216 217 218 219 220 221 222 223
		}

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

224
		res.SetOutput(node)
225
	},
226
	Type: Node{},
227
	Marshalers: cmds.MarshalerMap{
228
		cmds.EncodingType("protobuf"): func(res cmds.Response) (io.Reader, error) {
229
			node := res.Output().(*Node)
230 231
			// deserialize the Data field as text as this was the standard behaviour
			object, err := deserializeNode(node, "text")
232 233
			if err != nil {
				return nil, err
234
			}
235 236 237 238 239 240

			marshaled, err := object.Marshal()
			if err != nil {
				return nil, err
			}
			return bytes.NewReader(marshaled), nil
241 242 243 244
		},
	},
}

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

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

Jeromy's avatar
Jeromy committed
272
		object, err := core.Resolve(req.Context(), n, fpath)
273
		if err != nil {
274 275
			res.SetError(err, cmds.ErrNormal)
			return
276 277 278 279
		}

		ns, err := object.Stat()
		if err != nil {
280 281
			res.SetError(err, cmds.ErrNormal)
			return
282 283
		}

284
		res.SetOutput(ns)
285 286 287 288
	},
	Type: dag.NodeStat{},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
289
			ns := res.Output().(*dag.NodeStat)
290

291
			buf := new(bytes.Buffer)
292
			w := func(s string, n int) {
293
				fmt.Fprintf(buf, "%s: %d\n", s, n)
294 295 296 297 298 299 300
			}
			w("NumLinks", ns.NumLinks)
			w("BlockSize", ns.BlockSize)
			w("LinksSize", ns.LinksSize)
			w("DataSize", ns.DataSize)
			w("CumulativeSize", ns.CumulativeSize)

301
			return buf, nil
302 303 304 305
		},
	},
}

306
var ObjectPutCmd = &cmds.Command{
307
	Helptext: cmds.HelpText{
rht's avatar
rht committed
308
		Tagline: "Stores input as a DAG object, outputs its key.",
309 310 311 312 313 314
		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.
315 316
It reads from stdin, and the output is a base58 encoded multihash.

Henry's avatar
Henry committed
317 318
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
319
	* "protobuf"
Henry's avatar
Henry committed
320
	* "json" (default)
Dylan Powers's avatar
Dylan Powers committed
321 322 323

Examples:

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

326 327
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
328 329 330 331 332 333 334 335 336 337

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

338
And then run:
Dylan Powers's avatar
Dylan Powers committed
339

340
	$ ipfs object put node.json
341
`,
342
	},
343 344

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

358 359
		input, err := req.Files().NextFile()
		if err != nil && err != io.EOF {
360 361
			res.SetError(err, cmds.ErrNormal)
			return
362 363
		}

Henry's avatar
Henry committed
364 365 366 367 368 369 370 371
		inputenc, found, err := req.Option("inputenc").String()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		if !found {
			inputenc = "json"
		}
372

373 374 375 376 377
		datafieldenc, found, err := req.Option("datafieldenc").String()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
slothbag's avatar
slothbag committed
378

379
		output, err := objectPut(n, input, inputenc, datafieldenc)
380 381 382 383 384
		if err != nil {
			errType := cmds.ErrNormal
			if err == ErrUnknownObjectEnc {
				errType = cmds.ErrClient
			}
385 386
			res.SetError(err, errType)
			return
387 388
		}

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

400
var ObjectNewCmd = &cmds.Command{
401
	Helptext: cmds.HelpText{
rht's avatar
rht committed
402
		Tagline: "Creates a new object from an ipfs template.",
403 404 405 406 407 408 409 410
		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.
411 412 413

Available templates:
	* unixfs-dir
414 415 416
`,
	},
	Arguments: []cmds.Argument{
417
		cmds.StringArg("template", false, false, "Template to use. Optional."),
418 419
	},
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
420
		n, err := req.InvocContext().GetNode()
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
		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)
	}
}

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

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

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

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

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

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

494
		dagnode, err = deserializeNode(node, dataFieldEncoding)
495 496 497
		if err != nil {
			return nil, err
		}
498 499

	case objectEncodingProtobuf:
500
		dagnode, err = dag.DecodeProtobuf(data)
501

502 503 504 505 506 507 508 509 510 511 512 513 514
	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
		}

515
		dagnode, err = deserializeNode(node, dataFieldEncoding)
516 517 518 519
		if err != nil {
			return nil, err
		}

520 521 522 523 524 525 526 527
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

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

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{
Michael Muré's avatar
Michael Muré committed
564
		Hash:  key.B58String(),
565 566 567 568 569 570 571 572 573 574 575 576 577
		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
}
578 579

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

591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
	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
}
606 607 608 609

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