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
// 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")
26

27
const inputLimit = 2 * 1024 * 1024
28

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: `
68 69
'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.
70 71
`,
		LongDescription: `
72 73
'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.
74

75 76
Note that the "--encoding" option does not affect the output, since the output
is the raw data of the object.
77
`,
78
	},
79 80

	Arguments: []cmds.Argument{
Jeromy's avatar
Jeromy committed
81
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
82
	},
83
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
84
		n, err := req.InvocContext().GetNode()
85
		if err != nil {
86 87
			res.SetError(err, cmds.ErrNormal)
			return
88
		}
89

Jeromy's avatar
Jeromy committed
90
		fpath := path.Path(req.Arguments()[0])
Jeromy's avatar
Jeromy committed
91
		node, err := core.Resolve(req.Context(), n, fpath)
92 93 94 95
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
96
		res.SetOutput(bytes.NewReader(node.Data()))
97 98 99
	},
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

289
			return buf, nil
290 291 292 293
		},
	},
}

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

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

Examples:

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

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

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

326
And then run:
Dylan Powers's avatar
Dylan Powers committed
327

328
	$ ipfs object put node.json
329
`,
330
	},
331 332

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

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

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

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

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

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

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

Available templates:
	* unixfs-dir
399 400 401
`,
	},
	Arguments: []cmds.Argument{
402
		cmds.StringArg("template", false, false, "Template to use. Optional."),
403 404
	},
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
405
		n, err := req.InvocContext().GetNode()
406 407 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
		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":
441
		return ft.EmptyDirNode(), nil
442 443 444 445 446
	default:
		return nil, fmt.Errorf("template '%s' not found", template)
	}
}

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

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

453
	data, err := ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
454 455 456 457 458 459 460 461
	if err != nil {
		return nil, err
	}

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

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

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

477
		dagnode, err = deserializeNode(node, dataFieldEncoding)
478 479 480
		if err != nil {
			return nil, err
		}
481 482

	case objectEncodingProtobuf:
483
		dagnode, err = dag.DecodeProtobuf(data)
484

485 486 487 488 489 490 491 492 493 494 495 496 497
	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
		}

498
		dagnode, err = deserializeNode(node, dataFieldEncoding)
499 500 501 502
		if err != nil {
			return nil, err
		}

503 504 505 506 507 508 509 510
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

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

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
547
		Hash:  key.B58String(),
548 549 550 551 552 553 554 555 556 557 558 559 560
		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
}
561 562

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

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

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