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)."),
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, either \"protobuf\" or \"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
		}

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

363 364 365 366 367
		datafieldenc, found, err := req.Option("datafieldenc").String()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
slothbag's avatar
slothbag committed
368

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

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

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

Available templates:
	* unixfs-dir
404 405 406
`,
	},
	Arguments: []cmds.Argument{
407
		cmds.StringArg("template", false, false, "Template to use. Optional."),
408 409
	},
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
410
		n, err := req.InvocContext().GetNode()
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 451 452 453
		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)
	}
}

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

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

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

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

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

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

484
		dagnode, err = deserializeNode(node, dataFieldEncoding)
485 486 487
		if err != nil {
			return nil, err
		}
488 489

	case objectEncodingProtobuf:
490
		dagnode, err = dag.DecodeProtobuf(data)
491

492 493 494 495 496 497 498 499 500 501 502 503 504
	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
		}

505
		dagnode, err = deserializeNode(node, dataFieldEncoding)
506 507 508 509
		if err != nil {
			return nil, err
		}

510 511 512 513 514 515 516 517
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

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

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

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

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

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