object.go 13.9 KB
Newer Older
1
package objectcmd
2 3

import (
4
	"encoding/base64"
5
	"errors"
6
	"fmt"
7 8
	"io"
	"io/ioutil"
9
	"text/tabwriter"
10

11
	"github.com/ipfs/go-ipfs/core/commands/cmdenv"
12

13
	humanize "github.com/dustin/go-humanize"
Jakub Sztandera's avatar
Jakub Sztandera committed
14 15 16 17 18
	"github.com/ipfs/go-cid"
	"github.com/ipfs/go-ipfs-cmds"
	ipld "github.com/ipfs/go-ipld-format"
	dag "github.com/ipfs/go-merkledag"
	"github.com/ipfs/interface-go-ipfs-core/options"
19
	path "github.com/ipfs/interface-go-ipfs-core/path"
20 21
)

22 23
type Node struct {
	Links []Link
24
	Data  string
25 26
}

27 28 29 30 31 32
type Link struct {
	Name, Hash string
	Size       uint64
}

type Object struct {
Jeromy's avatar
Jeromy committed
33 34
	Hash  string `json:"Hash,omitempty"`
	Links []Link `json:"Links,omitempty"`
35 36
}

Kejie Zhang's avatar
Kejie Zhang committed
37
var ErrDataEncoding = errors.New("unkown data field encoding")
38

39 40 41 42 43 44 45
const (
	headersOptionName      = "headers"
	encodingOptionName     = "data-encoding"
	inputencOptionName     = "inputenc"
	datafieldencOptionName = "datafieldenc"
	pinOptionName          = "pin"
	quietOptionName        = "quiet"
46
	humanOptionName        = "human"
47 48
)

49
var ObjectCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
50
	Helptext: cmds.HelpText{
51
		Tagline: "Interact with IPFS objects.",
Matt Bell's avatar
Matt Bell committed
52 53 54
		ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
55
	},
56 57

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

Overbool's avatar
Overbool committed
69
// ObjectDataCmd object data command
70
var ObjectDataCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
71
	Helptext: cmds.HelpText{
72
		Tagline: "Output the raw bytes of an IPFS object.",
73
		ShortDescription: `
74 75
'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.
76 77
`,
		LongDescription: `
78 79
'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.
80

81 82
Note that the "--encoding" option does not affect the output, since the output
is the raw data of the object.
83
`,
84
	},
85

Steven Allen's avatar
Steven Allen committed
86 87
	Arguments: []cmds.Argument{
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
88
	},
89
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
90
		api, err := cmdenv.GetApi(env, req)
91
		if err != nil {
92
			return err
93
		}
94

95
		path := path.New(req.Arguments[0])
Jeromy's avatar
Jeromy committed
96

97
		data, err := api.Object().Data(req.Context, path)
98
		if err != nil {
99
			return err
100
		}
101

102
		return res.Emit(data)
103 104 105
	},
}

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

Steven Allen's avatar
Steven Allen committed
117 118
	Arguments: []cmds.Argument{
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
119
	},
Steven Allen's avatar
Steven Allen committed
120 121
	Options: []cmds.Option{
		cmds.BoolOption(headersOptionName, "v", "Print table headers (Hash, Size, Name)."),
palkeo's avatar
palkeo committed
122
	},
123
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
124
		api, err := cmdenv.GetApi(env, req)
125
		if err != nil {
126
			return err
127 128
		}

129 130 131 132 133
		enc, err := cmdenv.GetLowLevelCidEncoder(req)
		if err != nil {
			return err
		}

134
		path := path.New(req.Arguments[0])
135

136
		rp, err := api.ResolvePath(req.Context, path)
137
		if err != nil {
138
			return err
139
		}
140

141
		links, err := api.Object().Links(req.Context, rp)
142
		if err != nil {
143
			return err
144
		}
145 146 147 148

		outLinks := make([]Link, len(links))
		for i, link := range links {
			outLinks[i] = Link{
149
				Hash: enc.Encode(link.Cid),
150 151 152 153 154
				Name: link.Name,
				Size: link.Size,
			}
		}

Steven Allen's avatar
Steven Allen committed
155
		out := &Object{
156
			Hash:  enc.Encode(rp.Cid()),
157 158 159
			Links: outLinks,
		}

160
		return cmds.EmitOnce(res, out)
161
	},
162 163 164
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *Object) error {
			tw := tabwriter.NewWriter(w, 1, 2, 1, ' ', 0)
165
			headers, _ := req.Options[headersOptionName].(bool)
166
			if headers {
167
				fmt.Fprintln(tw, "Hash\tSize\tName")
168
			}
169 170
			for _, link := range out.Links {
				fmt.Fprintf(tw, "%s\t%v\t%s\n", link.Hash, link.Size, link.Name)
171
			}
172 173 174 175
			tw.Flush()

			return nil
		}),
176
	},
keks's avatar
keks committed
177
	Type: &Object{},
178 179
}

Overbool's avatar
Overbool committed
180
// ObjectGetCmd object get command
181
var ObjectGetCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
182
	Helptext: cmds.HelpText{
rht's avatar
rht committed
183
		Tagline: "Get and serialize the DAG node named by <key>.",
184
		ShortDescription: `
rht's avatar
rht committed
185
'ipfs object get' is a plumbing command for retrieving DAG nodes.
186 187
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
188 189
`,
		LongDescription: `
rht's avatar
rht committed
190
'ipfs object get' is a plumbing command for retrieving DAG nodes.
191 192
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
193

194 195 196 197
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
198 199 200 201 202 203 204 205 206
(Specified by the "--encoding" or "--enc" flag)

The encoding of the object's data field can be specifed by using the
--data-encoding flag

Supported values are:
	* "text" (default)
	* "base64"
`,
207
	},
208

Steven Allen's avatar
Steven Allen committed
209 210
	Arguments: []cmds.Argument{
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
211
	},
Steven Allen's avatar
Steven Allen committed
212 213
	Options: []cmds.Option{
		cmds.StringOption(encodingOptionName, "Encoding type of the data field, either \"text\" or \"base64\".").WithDefault("text"),
214
	},
215
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
216
		api, err := cmdenv.GetApi(env, req)
217
		if err != nil {
218
			return err
219
		}
220

221 222 223 224 225
		enc, err := cmdenv.GetLowLevelCidEncoder(req)
		if err != nil {
			return err
		}

226
		path := path.New(req.Arguments[0])
227

228
		datafieldenc, _ := req.Options[encodingOptionName].(string)
229
		if err != nil {
230
			return err
231 232
		}

233
		nd, err := api.Object().Get(req.Context, path)
234
		if err != nil {
235
			return err
236 237
		}

238
		r, err := api.Object().Data(req.Context, path)
239
		if err != nil {
240
			return err
241 242
		}

243 244
		data, err := ioutil.ReadAll(r)
		if err != nil {
245
			return err
246 247 248
		}

		out, err := encodeData(data, datafieldenc)
249
		if err != nil {
250
			return err
251 252
		}

253
		node := &Node{
254 255
			Links: make([]Link, len(nd.Links())),
			Data:  out,
256 257
		}

258
		for i, link := range nd.Links() {
259
			node.Links[i] = Link{
260
				Hash: enc.Encode(link.Cid),
261 262 263 264 265
				Name: link.Name,
				Size: link.Size,
			}
		}

266
		return cmds.EmitOnce(res, node)
267
	},
268
	Type: Node{},
269 270
	Encoders: cmds.EncoderMap{
		cmds.Protobuf: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *Node) error {
271
			// deserialize the Data field as text as this was the standard behaviour
272
			object, err := deserializeNode(out, "text")
273
			if err != nil {
274
				return nil
275
			}
276 277 278

			marshaled, err := object.Marshal()
			if err != nil {
279
				return err
280
			}
Steven Allen's avatar
Steven Allen committed
281 282
			_, err = w.Write(marshaled)
			return err
283
		}),
284 285 286
	},
}

Overbool's avatar
Overbool committed
287
// ObjectStatCmd object stat command
288
var ObjectStatCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
289
	Helptext: cmds.HelpText{
rht's avatar
rht committed
290
		Tagline: "Get stats for the DAG node named by <key>.",
291 292 293 294 295 296 297 298 299 300 301 302
		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
`,
	},

Steven Allen's avatar
Steven Allen committed
303 304
	Arguments: []cmds.Argument{
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
305
	},
Steven Allen's avatar
Steven Allen committed
306 307
	Options: []cmds.Option{
		cmds.BoolOption(humanOptionName, "Print sizes in human readable format (e.g., 1K 234M 2G)"),
308
	},
309
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
310
		api, err := cmdenv.GetApi(env, req)
311
		if err != nil {
312
			return err
313 314
		}

315 316 317 318 319
		enc, err := cmdenv.GetLowLevelCidEncoder(req)
		if err != nil {
			return err
		}

320
		ns, err := api.Object().Stat(req.Context, path.New(req.Arguments[0]))
321
		if err != nil {
322
			return err
323 324
		}

325
		oldStat := &ipld.NodeStat{
326
			Hash:           enc.Encode(ns.Cid),
327 328 329 330 331 332 333
			NumLinks:       ns.NumLinks,
			BlockSize:      ns.BlockSize,
			LinksSize:      ns.LinksSize,
			DataSize:       ns.DataSize,
			CumulativeSize: ns.CumulativeSize,
		}

334
		return cmds.EmitOnce(res, oldStat)
335
	},
336
	Type: ipld.NodeStat{},
337 338
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *ipld.NodeStat) error {
339 340
			wtr := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
			defer wtr.Flush()
341
			fw := func(s string, n int) {
342
				fmt.Fprintf(wtr, "%s:\t%d\n", s, n)
Jan Winkelmann's avatar
Jan Winkelmann committed
343
			}
344
			human, _ := req.Options[humanOptionName].(bool)
345 346 347 348
			fw("NumLinks", out.NumLinks)
			fw("BlockSize", out.BlockSize)
			fw("LinksSize", out.LinksSize)
			fw("DataSize", out.DataSize)
349 350 351 352 353
			if human {
				fmt.Fprintf(wtr, "%s:\t%s\n", "CumulativeSize", humanize.Bytes(uint64(out.CumulativeSize)))
			} else {
				fw("CumulativeSize", out.CumulativeSize)
			}
354 355 356

			return nil
		}),
357 358 359
	},
}

Overbool's avatar
Overbool committed
360
// ObjectPutCmd object put command
361
var ObjectPutCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
362
	Helptext: cmds.HelpText{
363
		Tagline: "Store input as a DAG object, print its key.",
364 365 366 367 368 369
		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.
370 371
It reads from stdin, and the output is a base58 encoded multihash.

Henry's avatar
Henry committed
372 373
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
374
	* "protobuf"
Henry's avatar
Henry committed
375
	* "json" (default)
Dylan Powers's avatar
Dylan Powers committed
376 377 378

Examples:

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

381 382
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
383 384 385 386 387 388 389 390 391 392

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

393
And then run:
Dylan Powers's avatar
Dylan Powers committed
394

395
	$ ipfs object put node.json
396
`,
397
	},
398

Steven Allen's avatar
Steven Allen committed
399 400
	Arguments: []cmds.Argument{
		cmds.FileArg("data", true, false, "Data to be stored as a DAG object.").EnableStdin(),
Henry's avatar
Henry committed
401
	},
Steven Allen's avatar
Steven Allen committed
402 403 404 405 406
	Options: []cmds.Option{
		cmds.StringOption(inputencOptionName, "Encoding type of input data. One of: {\"protobuf\", \"json\"}.").WithDefault("json"),
		cmds.StringOption(datafieldencOptionName, "Encoding type of the data field, either \"text\" or \"base64\".").WithDefault("text"),
		cmds.BoolOption(pinOptionName, "Pin this object when adding."),
		cmds.BoolOption(quietOptionName, "q", "Write minimal output."),
407
	},
408
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
409
		api, err := cmdenv.GetApi(env, req)
410
		if err != nil {
411
			return err
412
		}
413

414 415 416 417 418
		enc, err := cmdenv.GetLowLevelCidEncoder(req)
		if err != nil {
			return err
		}

419 420 421
		file, err := cmdenv.GetFileArg(req.Files.Entries())
		if err != nil {
			return err
422 423
		}

424
		inputenc, _ := req.Options[inputencOptionName].(string)
Henry's avatar
Henry committed
425
		if err != nil {
426
			return err
Henry's avatar
Henry committed
427
		}
428

429
		datafieldenc, _ := req.Options[datafieldencOptionName].(string)
430
		if err != nil {
431
			return err
432
		}
slothbag's avatar
slothbag committed
433

434
		dopin, _ := req.Options[pinOptionName].(bool)
Łukasz Magiera's avatar
Łukasz Magiera committed
435
		if err != nil {
436
			return err
Łukasz Magiera's avatar
Łukasz Magiera committed
437 438
		}

439
		p, err := api.Object().Put(req.Context, file,
440 441 442
			options.Object.DataType(datafieldenc),
			options.Object.InputEnc(inputenc),
			options.Object.Pin(dopin))
443
		if err != nil {
444
			return err
445 446
		}

447
		return cmds.EmitOnce(res, &Object{Hash: enc.Encode(p.Cid())})
448
	},
449 450
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *Object) error {
451
			quiet, _ := req.Options[quietOptionName].(bool)
Łukasz Magiera's avatar
Łukasz Magiera committed
452

453
			o := out.Hash
Łukasz Magiera's avatar
Łukasz Magiera committed
454
			if !quiet {
455
				o = "added " + o
Łukasz Magiera's avatar
Łukasz Magiera committed
456 457
			}

458 459 460 461
			fmt.Fprintln(w, o)

			return nil
		}),
462
	},
463
	Type: Object{},
464 465
}

Overbool's avatar
Overbool committed
466
// ObjectNewCmd object new command
467
var ObjectNewCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
468
	Helptext: cmds.HelpText{
469
		Tagline: "Create a new object from an ipfs template.",
470 471 472 473 474 475 476 477
		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.
478 479 480

Available templates:
	* unixfs-dir
481 482
`,
	},
Steven Allen's avatar
Steven Allen committed
483 484
	Arguments: []cmds.Argument{
		cmds.StringArg("template", false, false, "Template to use. Optional."),
485
	},
486
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
487
		api, err := cmdenv.GetApi(env, req)
488
		if err != nil {
489
			return err
490 491
		}

492 493 494 495 496
		enc, err := cmdenv.GetLowLevelCidEncoder(req)
		if err != nil {
			return err
		}

497
		template := "empty"
498 499
		if len(req.Arguments) == 1 {
			template = req.Arguments[0]
500 501
		}

502
		nd, err := api.Object().New(req.Context, options.Object.Type(template))
503
		if err != nil && err != io.EOF {
504
			return err
505
		}
506

507
		return cmds.EmitOnce(res, &Object{Hash: enc.Encode(nd.Cid())})
508
	},
509 510 511 512 513
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *Object) error {
			fmt.Fprintln(w, out.Hash)
			return nil
		}),
514 515 516 517
	},
	Type: Object{},
}

518
// converts the Node object into a real dag.ProtoNode
Jeromy's avatar
Jeromy committed
519
func deserializeNode(nd *Node, dataFieldEncoding string) (*dag.ProtoNode, error) {
520
	dagnode := new(dag.ProtoNode)
slothbag's avatar
slothbag committed
521 522
	switch dataFieldEncoding {
	case "text":
Jeromy's avatar
Jeromy committed
523
		dagnode.SetData([]byte(nd.Data))
slothbag's avatar
slothbag committed
524
	case "base64":
Steven Allen's avatar
Steven Allen committed
525 526 527 528
		data, err := base64.StdEncoding.DecodeString(nd.Data)
		if err != nil {
			return nil, err
		}
529
		dagnode.SetData(data)
slothbag's avatar
slothbag committed
530
	default:
531
		return nil, ErrDataEncoding
532 533
	}

Steven Allen's avatar
Steven Allen committed
534
	links := make([]*ipld.Link, len(nd.Links))
Jeromy's avatar
Jeromy committed
535
	for i, link := range nd.Links {
536
		c, err := cid.Decode(link.Hash)
537 538 539
		if err != nil {
			return nil, err
		}
Steven Allen's avatar
Steven Allen committed
540
		links[i] = &ipld.Link{
541 542
			Name: link.Name,
			Size: link.Size,
543
			Cid:  c,
544 545
		}
	}
Steven Allen's avatar
Steven Allen committed
546
	dagnode.SetLinks(links)
547 548 549

	return dagnode, nil
}
550

551 552 553 554 555 556 557 558
func encodeData(data []byte, encoding string) (string, error) {
	switch encoding {
	case "text":
		return string(data), nil
	case "base64":
		return base64.StdEncoding.EncodeToString(data), nil
	}

559
	return "", ErrDataEncoding
560
}