object.go 14.3 KB
Newer Older
1
package objectcmd
2 3 4 5

import (
	"bytes"
	"encoding/json"
6
	"encoding/xml"
7
	"errors"
8
	"fmt"
9 10
	"io"
	"io/ioutil"
11
	"strings"
12
	"text/tabwriter"
13

14
	mh "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"
15

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

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

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

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

type Object struct {
	Hash  string
	Links []Link
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

221
		res.SetOutput(node)
222
	},
223
	Type: Node{},
224
	Marshalers: cmds.MarshalerMap{
225
		cmds.EncodingType("protobuf"): func(res cmds.Response) (io.Reader, error) {
226
			node := res.Output().(*Node)
227 228 229
			object, err := deserializeNode(node)
			if err != nil {
				return nil, err
230
			}
231 232 233 234 235 236

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

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

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

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

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

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

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

297
			return buf, nil
298 299 300 301
		},
	},
}

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

Henry's avatar
Henry committed
313 314
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
315
	* "protobuf"
Henry's avatar
Henry committed
316
	* "json" (default)
Dylan Powers's avatar
Dylan Powers committed
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336

Examples:

	echo '{ "Data": "abc" }' | ipfs object put

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:

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

and then run

	ipfs object put node.json
337
`,
338
	},
339 340

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	case objectEncodingProtobuf:
		dagnode, err = dag.Decoded(data)

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

		dagnode, err = deserializeNode(node)
		if err != nil {
			return nil, err
		}

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

	if err != nil {
		return nil, err
	}

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

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{
		Hash:  key.Pretty(),
		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
}
567 568 569 570

// converts the Node object into a real dag.Node
func deserializeNode(node *Node) (*dag.Node, error) {
	dagnode := new(dag.Node)
571
	dagnode.Data = []byte(node.Data)
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
	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
}
587 588 589 590

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