object.go 14.6 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
type Link struct {
	Name, Hash string
	Size       uint64
}

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

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
		ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
50
	},
51 52

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

64
var ObjectDataCmd = &cmds.Command{
65
	Helptext: cmds.HelpText{
rht's avatar
rht committed
66
		Tagline: "Outputs the raw bytes in an IPFS object.",
67
		ShortDescription: `
rht's avatar
rht committed
68
'ipfs object data' is a plumbing command for retrieving the raw bytes stored in
69 70 71 72
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
`,
		LongDescription: `
rht's avatar
rht committed
73
'ipfs object data' is a plumbing command for retrieving the raw bytes stored in
74 75
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
76 77 78 79

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

	Arguments: []cmds.Argument{
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
		fpath := path.Path(req.Arguments()[0])
Jeromy's avatar
Jeromy committed
93
		node, err := core.Resolve(req.Context(), n, fpath)
94 95 96 97
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
98
		res.SetOutput(bytes.NewReader(node.Data))
99 100 101
	},
}

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

	Arguments: []cmds.Argument{
113
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
114
	},
palkeo's avatar
palkeo committed
115
	Options: []cmds.Option{
116
		cmds.BoolOption("headers", "v", "Print table headers (Hash, Size, Name).").Default(false),
palkeo's avatar
palkeo committed
117
	},
118
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
119
		n, err := req.InvocContext().GetNode()
120
		if err != nil {
121 122
			res.SetError(err, cmds.ErrNormal)
			return
123
		}
124

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

163
var ObjectGetCmd = &cmds.Command{
164
	Helptext: cmds.HelpText{
rht's avatar
rht committed
165
		Tagline: "Get and serialize the DAG node named by <key>.",
166
		ShortDescription: `
rht's avatar
rht committed
167
'ipfs object get' is a plumbing command for retrieving DAG nodes.
168 169
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
170 171
`,
		LongDescription: `
rht's avatar
rht committed
172
'ipfs object get' is a plumbing command for retrieving DAG nodes.
173 174
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
175

176 177 178 179
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
180
(Specified by the "--encoding" or "-enc" flag)`,
181
	},
182 183

	Arguments: []cmds.Argument{
184
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
185
	},
186
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
187
		n, err := req.InvocContext().GetNode()
188
		if err != nil {
189 190
			res.SetError(err, cmds.ErrNormal)
			return
191
		}
192

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

Jeromy's avatar
Jeromy committed
195
		object, err := core.Resolve(req.Context(), n, fpath)
196
		if err != nil {
197 198
			res.SetError(err, cmds.ErrNormal)
			return
199 200
		}

201 202
		node := &Node{
			Links: make([]Link, len(object.Links)),
203
			Data:  string(object.Data),
204 205 206 207 208 209 210 211 212 213
		}

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

214
		res.SetOutput(node)
215
	},
216
	Type: Node{},
217
	Marshalers: cmds.MarshalerMap{
218
		cmds.EncodingType("protobuf"): func(res cmds.Response) (io.Reader, error) {
219
			node := res.Output().(*Node)
220 221
			// deserialize the Data field as text as this was the standard behaviour
			object, err := deserializeNode(node, "text")
222 223
			if err != nil {
				return nil, err
224
			}
225 226 227 228 229 230

			marshaled, err := object.Marshal()
			if err != nil {
				return nil, err
			}
			return bytes.NewReader(marshaled), nil
231 232 233 234
		},
	},
}

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

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

Jeromy's avatar
Jeromy committed
262
		object, err := core.Resolve(req.Context(), n, fpath)
263
		if err != nil {
264 265
			res.SetError(err, cmds.ErrNormal)
			return
266 267 268 269
		}

		ns, err := object.Stat()
		if err != nil {
270 271
			res.SetError(err, cmds.ErrNormal)
			return
272 273
		}

274
		res.SetOutput(ns)
275 276 277 278
	},
	Type: dag.NodeStat{},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
279
			ns := res.Output().(*dag.NodeStat)
280

281
			buf := new(bytes.Buffer)
282
			w := func(s string, n int) {
283
				fmt.Fprintf(buf, "%s: %d\n", s, n)
284 285 286 287 288 289 290
			}
			w("NumLinks", ns.NumLinks)
			w("BlockSize", ns.BlockSize)
			w("LinksSize", ns.LinksSize)
			w("DataSize", ns.DataSize)
			w("CumulativeSize", ns.CumulativeSize)

291
			return buf, nil
292 293 294 295
		},
	},
}

296
var ObjectPutCmd = &cmds.Command{
297
	Helptext: cmds.HelpText{
rht's avatar
rht committed
298
		Tagline: "Stores input as a DAG object, outputs its key.",
299 300 301 302 303 304
		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.
305 306
It reads from stdin, and the output is a base58 encoded multihash.

Henry's avatar
Henry committed
307 308
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
309
	* "protobuf"
Henry's avatar
Henry committed
310
	* "json" (default)
Dylan Powers's avatar
Dylan Powers committed
311 312 313

Examples:

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

316 317
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
318 319 320 321 322 323 324 325 326 327

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

328
And then run:
Dylan Powers's avatar
Dylan Powers committed
329

330
	$ ipfs object put node.json
331
`,
332
	},
333 334

	Arguments: []cmds.Argument{
335
		cmds.FileArg("data", true, false, "Data to be stored as a DAG object.").EnableStdin(),
Henry's avatar
Henry committed
336 337
	},
	Options: []cmds.Option{
338
		cmds.StringOption("inputenc", "Encoding type of input data. One of: {\"protobuf\", \"json\"}.").Default("json"),
slothbag's avatar
slothbag committed
339
		cmds.StringOption("datafieldenc", "Encoding type of the data field, either \"text\" or \"base64\".").Default("text"),
340
	},
341
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
342
		n, err := req.InvocContext().GetNode()
343
		if err != nil {
344 345
			res.SetError(err, cmds.ErrNormal)
			return
346
		}
347

348 349
		input, err := req.Files().NextFile()
		if err != nil && err != io.EOF {
350 351
			res.SetError(err, cmds.ErrNormal)
			return
352 353
		}

354
		inputenc, _, err := req.Option("inputenc").String()
Henry's avatar
Henry committed
355 356 357 358
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
359

360
		datafieldenc, _, err := req.Option("datafieldenc").String()
361 362 363 364
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
slothbag's avatar
slothbag committed
365

366
		output, err := objectPut(n, input, inputenc, datafieldenc)
367 368 369 370 371
		if err != nil {
			errType := cmds.ErrNormal
			if err == ErrUnknownObjectEnc {
				errType = cmds.ErrClient
			}
372 373
			res.SetError(err, errType)
			return
374 375
		}

376
		res.SetOutput(output)
377
	},
378
	Marshalers: cmds.MarshalerMap{
379
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
380
			object := res.Output().(*Object)
381
			return strings.NewReader("added " + object.Hash + "\n"), nil
382 383
		},
	},
384
	Type: Object{},
385 386
}

387
var ObjectNewCmd = &cmds.Command{
388
	Helptext: cmds.HelpText{
rht's avatar
rht committed
389
		Tagline: "Creates a new object from an ipfs template.",
390 391 392 393 394 395 396 397
		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.
398 399 400

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

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

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

457
	data, err := ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
458 459 460 461 462 463 464 465
	if err != nil {
		return nil, err
	}

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

466
	var dagnode *dag.Node
467 468
	switch getObjectEnc(encoding) {
	case objectEncodingJSON:
469 470 471 472 473 474
		node := new(Node)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}

475 476
		// check that we have data in the Node to add
		// otherwise we will add the empty object without raising an error
477
		if NodeEmpty(node) {
478 479 480
			return nil, ErrEmptyNode
		}

481
		dagnode, err = deserializeNode(node, dataFieldEncoding)
482 483 484
		if err != nil {
			return nil, err
		}
485 486

	case objectEncodingProtobuf:
487
		dagnode, err = dag.DecodeProtobuf(data)
488

489 490 491 492 493 494 495 496 497 498 499 500 501
	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
		}

502
		dagnode, err = deserializeNode(node, dataFieldEncoding)
503 504 505 506
		if err != nil {
			return nil, err
		}

507 508 509 510 511 512 513 514
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

515
	_, err = n.DAG.Add(dagnode)
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
	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"
531
	objectEncodingXML                     = "xml"
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
)

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
551
		Hash:  key.B58String(),
552 553 554 555 556 557 558 559 560 561 562 563 564
		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
}
565 566

// converts the Node object into a real dag.Node
567
func deserializeNode(node *Node, dataFieldEncoding string) (*dag.Node, error) {
568
	dagnode := new(dag.Node)
slothbag's avatar
slothbag committed
569 570
	switch dataFieldEncoding {
	case "text":
571
		dagnode.Data = []byte(node.Data)
slothbag's avatar
slothbag committed
572
	case "base64":
573
		dagnode.Data, _ = base64.StdEncoding.DecodeString(node.Data)
slothbag's avatar
slothbag committed
574 575
	default:
		return nil, fmt.Errorf("Unkown data field encoding")
576 577
	}

578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
	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
}
593 594 595 596

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