object.go 14 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: `
75
'ipfs object data' is a plumbing command for retreiving the raw bytes stored in
76 77 78 79
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
`,
		LongDescription: `
80
'ipfs object data' is a plumbing command for retreiving 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: `
Matt Bell's avatar
Matt Bell committed
113 114 115
'ipfs object links' is a plumbing command for retreiving the links from
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
	},
122
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
123
		n, err := req.InvocContext().GetNode()
124
		if err != nil {
125 126
			res.SetError(err, cmds.ErrNormal)
			return
127
		}
128

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
			fmt.Fprintln(w, "Hash\tSize\tName\t")
			for _, link := range object.Links {
				fmt.Fprintf(w, "%s\t%v\t%s\t\n", link.Hash, link.Size, link.Name)
			}
151
			w.Flush()
152
			return buf, nil
153 154
		},
	},
155
	Type: Object{},
156 157
}

158
var ObjectGetCmd = &cmds.Command{
159
	Helptext: cmds.HelpText{
rht's avatar
rht committed
160
		Tagline: "Get and serialize the DAG node named by <key>.",
161 162
		ShortDescription: `
'ipfs object get' is a plumbing command for retreiving DAG nodes.
163 164
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
165 166 167
`,
		LongDescription: `
'ipfs object get' is a plumbing command for retreiving 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 172 173 174
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
175
(Specified by the "--encoding" or "-enc" flag)`,
176
	},
177 178

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

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

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

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

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

209
		res.SetOutput(node)
210
	},
211
	Type: Node{},
212
	Marshalers: cmds.MarshalerMap{
213
		cmds.EncodingType("protobuf"): func(res cmds.Response) (io.Reader, error) {
214
			node := res.Output().(*Node)
215 216 217
			object, err := deserializeNode(node)
			if err != nil {
				return nil, err
218
			}
219 220 221 222 223 224

			marshaled, err := object.Marshal()
			if err != nil {
				return nil, err
			}
			return bytes.NewReader(marshaled), nil
225 226 227 228
		},
	},
}

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

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

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

		ns, err := object.Stat()
		if err != nil {
264 265
			res.SetError(err, cmds.ErrNormal)
			return
266 267
		}

268
		res.SetOutput(ns)
269 270 271 272
	},
	Type: dag.NodeStat{},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
273
			ns := res.Output().(*dag.NodeStat)
274

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

285
			return buf, nil
286 287 288 289
		},
	},
}

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

Henry's avatar
Henry committed
301 302
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
303
	* "protobuf"
Henry's avatar
Henry committed
304
	* "json" (default)
Dylan Powers's avatar
Dylan Powers committed
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324

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
325
`,
326
	},
327 328

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

341 342
		input, err := req.Files().NextFile()
		if err != nil && err != io.EOF {
343 344
			res.SetError(err, cmds.ErrNormal)
			return
345 346
		}

Henry's avatar
Henry committed
347 348 349 350 351 352 353 354
		inputenc, found, err := req.Option("inputenc").String()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		if !found {
			inputenc = "json"
		}
355

Henry's avatar
Henry committed
356
		output, err := objectPut(n, input, inputenc)
357 358 359 360 361
		if err != nil {
			errType := cmds.ErrNormal
			if err == ErrUnknownObjectEnc {
				errType = cmds.ErrClient
			}
362 363
			res.SetError(err, errType)
			return
364 365
		}

366
		res.SetOutput(output)
367
	},
368
	Marshalers: cmds.MarshalerMap{
369
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
370
			object := res.Output().(*Object)
371
			return strings.NewReader("added " + object.Hash + "\n"), nil
372 373
		},
	},
374
	Type: Object{},
375 376
}

377
var ObjectNewCmd = &cmds.Command{
378
	Helptext: cmds.HelpText{
rht's avatar
rht committed
379
		Tagline: "Creates a new object from an ipfs template.",
380 381 382 383 384 385 386 387
		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.
388 389 390

Available templates:
	* unixfs-dir
391 392 393 394 395 396
`,
	},
	Arguments: []cmds.Argument{
		cmds.StringArg("template", false, false, "optional template to use"),
	},
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
397
		n, err := req.InvocContext().GetNode()
398 399 400 401 402 403 404 405 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":
		nd := new(dag.Node)
		nd.Data = ft.FolderPBData()
		return nd, nil
	default:
		return nil, fmt.Errorf("template '%s' not found", template)
	}
}

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

444 445 446
// 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) {

447
	data, err := ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
448 449 450 451 452 453 454 455
	if err != nil {
		return nil, err
	}

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

456
	var dagnode *dag.Node
457 458
	switch getObjectEnc(encoding) {
	case objectEncodingJSON:
459 460 461 462 463 464
		node := new(Node)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}

465 466
		// check that we have data in the Node to add
		// otherwise we will add the empty object without raising an error
467
		if NodeEmpty(node) {
468 469 470
			return nil, ErrEmptyNode
		}

471 472 473 474
		dagnode, err = deserializeNode(node)
		if err != nil {
			return nil, err
		}
475 476 477 478

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

479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
	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
		}

497 498 499 500 501 502 503 504
	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

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

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
}
555 556 557 558

// converts the Node object into a real dag.Node
func deserializeNode(node *Node) (*dag.Node, error) {
	dagnode := new(dag.Node)
559
	dagnode.Data = []byte(node.Data)
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
	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
}
575 576 577 578

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