object.go 15 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: `
51
ipfs object data <key>      - Outputs raw bytes in an object
Richard Littauer's avatar
Richard Littauer committed
52
ipfs object get <key>       - Get the DAG node named by <key>
53 54 55
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
Richard Littauer's avatar
Richard Littauer committed
56 57
ipfs object put <data>      - Stores input, outputs its key
ipfs object stat <key>      - Outputs statistics of object
Matt Bell's avatar
Matt Bell committed
58
`,
59
	},
60 61

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

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

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

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

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

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

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

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

171
var ObjectGetCmd = &cmds.Command{
172
	Helptext: cmds.HelpText{
rht's avatar
rht committed
173
		Tagline: "Get and serialize the DAG node named by <key>.",
174
		ShortDescription: `
rht's avatar
rht committed
175
'ipfs object get' is a plumbing command for retrieving DAG nodes.
176 177
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
178 179
`,
		LongDescription: `
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 185 186 187
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
188
(Specified by the "--encoding" or "-enc" flag)`,
189
	},
190 191

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

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

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

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

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

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

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

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

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

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

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

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

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

299
			return buf, nil
300 301 302 303
		},
	},
}

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

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

Examples:

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

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

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

336
And then run:
Dylan Powers's avatar
Dylan Powers committed
337

338
	$ ipfs object put node.json
339
`,
340
	},
341 342

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	case objectEncodingProtobuf:
498
		dagnode, err = dag.DecodeProtobuf(data)
499

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

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

518 519 520 521 522 523 524 525
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

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

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
562
		Hash:  key.B58String(),
563 564 565 566 567 568 569 570 571 572 573 574 575
		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
}
576 577

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

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

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