object.go 12.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
	cmdenv "github.com/ipfs/go-ipfs/core/commands/cmdenv"
12 13
	coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface"
	"github.com/ipfs/go-ipfs/core/coreapi/interface/options"
14

15
	cid "gx/ipfs/QmPSQnBKM9g7BaUcZCvswUJVscQ1ipjmwxN5PXCjkp9EQ7/go-cid"
Hector Sanjuan's avatar
Hector Sanjuan committed
16
	ipld "gx/ipfs/QmR7TcHkR9nxkUorfi8XMTAMLUK7GiP64TWWBzY3aacc1o/go-ipld-format"
17
	cmds "gx/ipfs/QmSXUokcP4TJpFfqozT69AVAYRtzXVMUjzQVkYX41R9Svs/go-ipfs-cmds"
Hector Sanjuan's avatar
Hector Sanjuan committed
18
	dag "gx/ipfs/QmSei8kFMfqdJq7Q68d2LMnHbTWKKg2daA29ezUYFAUNgc/go-merkledag"
19
	cmdkit "gx/ipfs/Qmde5VP1qUkyQXKCfmEUA7bP64V2HAptbJ7phuPp7jXWwg/go-ipfs-cmdkit"
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
var ObjectCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
40
	Helptext: cmdkit.HelpText{
41
		Tagline: "Interact with IPFS objects.",
Matt Bell's avatar
Matt Bell committed
42 43 44
		ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
45
	},
46 47

	Subcommands: map[string]*cmds.Command{
48 49 50 51 52
		"data":  ObjectDataCmd,
		"diff":  ObjectDiffCmd,
		"get":   ObjectGetCmd,
		"links": ObjectLinksCmd,
		"new":   ObjectNewCmd,
53
		"patch": ObjectPatchCmd,
54 55
		"put":   ObjectPutCmd,
		"stat":  ObjectStatCmd,
56 57 58
	},
}

59
var ObjectDataCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
60
	Helptext: cmdkit.HelpText{
61
		Tagline: "Output the raw bytes of an IPFS object.",
62
		ShortDescription: `
63 64
'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.
65 66
`,
		LongDescription: `
67 68
'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.
69

70 71
Note that the "--encoding" option does not affect the output, since the output
is the raw data of the object.
72
`,
73
	},
74

Jan Winkelmann's avatar
Jan Winkelmann committed
75 76
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
77
	},
78 79
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		api, err := cmdenv.GetApi(env)
80
		if err != nil {
81
			return err
82
		}
83

84
		path, err := coreiface.ParsePath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
85
		if err != nil {
86
			return err
Jeromy's avatar
Jeromy committed
87 88
		}

89
		data, err := api.Object().Data(req.Context, path)
90
		if err != nil {
91
			return err
92
		}
93

94
		return res.Emit(data)
95 96 97
	},
}

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

Jan Winkelmann's avatar
Jan Winkelmann committed
108 109
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
110
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
111
	Options: []cmdkit.Option{
112
		cmdkit.BoolOption("headers", "v", "Print table headers (Hash, Size, Name)."),
palkeo's avatar
palkeo committed
113
	},
114 115
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		api, err := cmdenv.GetApi(env)
116
		if err != nil {
117
			return err
118 119
		}

120
		path, err := coreiface.ParsePath(req.Arguments[0])
121
		if err != nil {
122
			return err
123 124
		}

125
		rp, err := api.ResolvePath(req.Context, path)
126
		if err != nil {
127
			return err
128
		}
129

130
		links, err := api.Object().Links(req.Context, rp)
131
		if err != nil {
132
			return err
133
		}
134 135 136 137 138 139 140 141 142 143

		outLinks := make([]Link, len(links))
		for i, link := range links {
			outLinks[i] = Link{
				Hash: link.Cid.String(),
				Name: link.Name,
				Size: link.Size,
			}
		}

Steven Allen's avatar
Steven Allen committed
144
		out := &Object{
145 146 147 148
			Hash:  rp.Cid().String(),
			Links: outLinks,
		}

149
		return res.Emit(out)
150
	},
151 152 153 154
	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)
			headers, _ := req.Options["headers"].(bool)
155
			if headers {
156
				fmt.Fprintln(tw, "Hash\tSize\tName")
157
			}
158 159
			for _, link := range out.Links {
				fmt.Fprintf(tw, "%s\t%v\t%s\n", link.Hash, link.Size, link.Name)
160
			}
161 162 163 164
			tw.Flush()

			return nil
		}),
165
	},
keks's avatar
keks committed
166
	Type: &Object{},
167 168
}

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

182 183 184 185
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
186 187 188 189 190 191 192 193 194
(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"
`,
195
	},
196

Jan Winkelmann's avatar
Jan Winkelmann committed
197 198
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
199
	},
200 201 202
	Options: []cmdkit.Option{
		cmdkit.StringOption("data-encoding", "Encoding type of the data field, either \"text\" or \"base64\".").WithDefault("text"),
	},
203 204
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		api, err := cmdenv.GetApi(env)
205
		if err != nil {
206
			return err
207
		}
208

209
		path, err := coreiface.ParsePath(req.Arguments[0])
210
		if err != nil {
211
			return err
212
		}
213

214
		datafieldenc, _ := req.Options["data-encoding"].(string)
215
		if err != nil {
216
			return err
217 218
		}

219
		nd, err := api.Object().Get(req.Context, path)
220
		if err != nil {
221
			return err
222 223
		}

224
		r, err := api.Object().Data(req.Context, path)
225
		if err != nil {
226
			return err
227 228
		}

229 230
		data, err := ioutil.ReadAll(r)
		if err != nil {
231
			return err
232 233 234
		}

		out, err := encodeData(data, datafieldenc)
235
		if err != nil {
236
			return err
237 238
		}

239
		node := &Node{
240 241
			Links: make([]Link, len(nd.Links())),
			Data:  out,
242 243
		}

244
		for i, link := range nd.Links() {
245
			node.Links[i] = Link{
246
				Hash: link.Cid.String(),
247 248 249 250 251
				Name: link.Name,
				Size: link.Size,
			}
		}

252
		return res.Emit(node)
253
	},
254
	Type: Node{},
255 256
	Encoders: cmds.EncoderMap{
		cmds.Protobuf: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *Node) error {
257
			// deserialize the Data field as text as this was the standard behaviour
258
			object, err := deserializeNode(out, "text")
259
			if err != nil {
260
				return nil
261
			}
262 263 264

			marshaled, err := object.Marshal()
			if err != nil {
265
				return err
266
			}
267 268 269 270
			fmt.Fprint(w, marshaled)

			return nil
		}),
271 272 273
	},
}

274
var ObjectStatCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
275
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
276
		Tagline: "Get stats for the DAG node named by <key>.",
277 278 279 280 281 282 283 284 285 286 287 288
		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
`,
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
289 290
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(),
291
	},
292 293
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		api, err := cmdenv.GetApi(env)
294
		if err != nil {
295
			return err
296 297
		}

298
		path, err := coreiface.ParsePath(req.Arguments[0])
299
		if err != nil {
300
			return err
301 302
		}

303
		ns, err := api.Object().Stat(req.Context, path)
304
		if err != nil {
305
			return err
306 307
		}

308 309 310 311 312 313 314 315 316
		oldStat := &ipld.NodeStat{
			Hash:           ns.Cid.String(),
			NumLinks:       ns.NumLinks,
			BlockSize:      ns.BlockSize,
			LinksSize:      ns.LinksSize,
			DataSize:       ns.DataSize,
			CumulativeSize: ns.CumulativeSize,
		}

317
		return res.Emit(oldStat)
318
	},
319
	Type: ipld.NodeStat{},
320 321 322 323
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *ipld.NodeStat) error {
			fw := func(s string, n int) {
				fmt.Fprintf(w, "%s: %d\n", s, n)
Jan Winkelmann's avatar
Jan Winkelmann committed
324
			}
325 326 327 328 329 330 331 332
			fw("NumLinks", out.NumLinks)
			fw("BlockSize", out.BlockSize)
			fw("LinksSize", out.LinksSize)
			fw("DataSize", out.DataSize)
			fw("CumulativeSize", out.CumulativeSize)

			return nil
		}),
333 334 335
	},
}

336
var ObjectPutCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
337
	Helptext: cmdkit.HelpText{
338
		Tagline: "Store input as a DAG object, print its key.",
339 340 341 342 343 344
		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.
345 346
It reads from stdin, and the output is a base58 encoded multihash.

Henry's avatar
Henry committed
347 348
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
349
	* "protobuf"
Henry's avatar
Henry committed
350
	* "json" (default)
Dylan Powers's avatar
Dylan Powers committed
351 352 353

Examples:

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

356 357
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
358 359 360 361 362 363 364 365 366 367

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

368
And then run:
Dylan Powers's avatar
Dylan Powers committed
369

370
	$ ipfs object put node.json
371
`,
372
	},
373

Jan Winkelmann's avatar
Jan Winkelmann committed
374 375
	Arguments: []cmdkit.Argument{
		cmdkit.FileArg("data", true, false, "Data to be stored as a DAG object.").EnableStdin(),
Henry's avatar
Henry committed
376
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
377
	Options: []cmdkit.Option{
378 379
		cmdkit.StringOption("inputenc", "Encoding type of input data. One of: {\"protobuf\", \"json\"}.").WithDefault("json"),
		cmdkit.StringOption("datafieldenc", "Encoding type of the data field, either \"text\" or \"base64\".").WithDefault("text"),
380
		cmdkit.BoolOption("pin", "Pin this object when adding."),
Łukasz Magiera's avatar
Łukasz Magiera committed
381
		cmdkit.BoolOption("quiet", "q", "Write minimal output."),
382
	},
383 384
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		api, err := cmdenv.GetApi(env)
385
		if err != nil {
386
			return err
387
		}
388

389
		input, err := req.Files.NextFile()
390
		if err != nil && err != io.EOF {
391
			return err
392 393
		}

394
		inputenc, _ := req.Options["inputenc"].(string)
Henry's avatar
Henry committed
395
		if err != nil {
396
			return err
Henry's avatar
Henry committed
397
		}
398

399
		datafieldenc, _ := req.Options["datafieldenc"].(string)
400
		if err != nil {
401
			return err
402
		}
slothbag's avatar
slothbag committed
403

404
		dopin, _ := req.Options["pin"].(bool)
Łukasz Magiera's avatar
Łukasz Magiera committed
405
		if err != nil {
406
			return err
Łukasz Magiera's avatar
Łukasz Magiera committed
407 408
		}

409
		p, err := api.Object().Put(req.Context, input,
410 411 412
			options.Object.DataType(datafieldenc),
			options.Object.InputEnc(inputenc),
			options.Object.Pin(dopin))
413
		if err != nil {
414
			return err
415 416
		}

417
		return res.Emit(&Object{Hash: p.Cid().String()})
418
	},
419 420 421
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *Object) error {
			quiet, _ := req.Options["quiet"].(bool)
Łukasz Magiera's avatar
Łukasz Magiera committed
422

423
			o := out.Hash
Łukasz Magiera's avatar
Łukasz Magiera committed
424
			if !quiet {
425
				o = "added " + o
Łukasz Magiera's avatar
Łukasz Magiera committed
426 427
			}

428 429 430 431
			fmt.Fprintln(w, o)

			return nil
		}),
432
	},
433
	Type: Object{},
434 435
}

436
var ObjectNewCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
437
	Helptext: cmdkit.HelpText{
438
		Tagline: "Create a new object from an ipfs template.",
439 440 441 442 443 444 445 446
		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.
447 448 449

Available templates:
	* unixfs-dir
450 451
`,
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
452 453
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("template", false, false, "Template to use. Optional."),
454
	},
455 456
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		api, err := cmdenv.GetApi(env)
457
		if err != nil {
458
			return err
459 460
		}

461
		template := "empty"
462 463
		if len(req.Arguments) == 1 {
			template = req.Arguments[0]
464 465
		}

466
		nd, err := api.Object().New(req.Context, options.Object.Type(template))
467
		if err != nil && err != io.EOF {
468
			return err
469
		}
470

471
		return res.Emit(&Object{Hash: nd.Cid().String()})
472
	},
473 474 475 476 477
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *Object) error {
			fmt.Fprintln(w, out.Hash)
			return nil
		}),
478 479 480 481
	},
	Type: Object{},
}

482
// converts the Node object into a real dag.ProtoNode
Jeromy's avatar
Jeromy committed
483
func deserializeNode(nd *Node, dataFieldEncoding string) (*dag.ProtoNode, error) {
484
	dagnode := new(dag.ProtoNode)
slothbag's avatar
slothbag committed
485 486
	switch dataFieldEncoding {
	case "text":
Jeromy's avatar
Jeromy committed
487
		dagnode.SetData([]byte(nd.Data))
slothbag's avatar
slothbag committed
488
	case "base64":
Steven Allen's avatar
Steven Allen committed
489 490 491 492
		data, err := base64.StdEncoding.DecodeString(nd.Data)
		if err != nil {
			return nil, err
		}
493
		dagnode.SetData(data)
slothbag's avatar
slothbag committed
494
	default:
495
		return nil, ErrDataEncoding
496 497
	}

Steven Allen's avatar
Steven Allen committed
498
	links := make([]*ipld.Link, len(nd.Links))
Jeromy's avatar
Jeromy committed
499
	for i, link := range nd.Links {
500
		c, err := cid.Decode(link.Hash)
501 502 503
		if err != nil {
			return nil, err
		}
Steven Allen's avatar
Steven Allen committed
504
		links[i] = &ipld.Link{
505 506
			Name: link.Name,
			Size: link.Size,
507
			Cid:  c,
508 509
		}
	}
Steven Allen's avatar
Steven Allen committed
510
	dagnode.SetLinks(links)
511 512 513

	return dagnode, nil
}
514

515 516 517 518 519 520 521 522
func encodeData(data []byte, encoding string) (string, error) {
	switch encoding {
	case "text":
		return string(data), nil
	case "base64":
		return base64.StdEncoding.EncodeToString(data), nil
	}

523
	return "", ErrDataEncoding
524
}