object.go 11.6 KB
Newer Older
1 2 3 4 5 6
package commands

import (
	"bytes"
	"encoding/json"
	"errors"
7
	"fmt"
8 9
	"io"
	"io/ioutil"
10
	"strings"
11

12 13
	mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"

14
	cmds "github.com/jbenet/go-ipfs/commands"
15
	core "github.com/jbenet/go-ipfs/core"
16
	dag "github.com/jbenet/go-ipfs/merkledag"
Jeromy's avatar
Jeromy committed
17
	path "github.com/jbenet/go-ipfs/path"
18 19 20 21 22 23 24
)

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

25 26
type Node struct {
	Links []Link
27
	Data  string
28 29
}

30
var ObjectCmd = &cmds.Command{
31
	Helptext: cmds.HelpText{
Matt Bell's avatar
Matt Bell committed
32 33 34 35 36 37 38 39 40
		Tagline: "Interact with ipfs objects",
		ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
		Synopsis: `
ipfs object get <key>             - Get the DAG node named by <key>
ipfs object put <data> <encoding> - 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
41
ipfs object stat <key>            - Outputs statistics of object
Matt Bell's avatar
Matt Bell committed
42
`,
43
	},
44 45 46 47 48 49

	Subcommands: map[string]*cmds.Command{
		"data":  objectDataCmd,
		"links": objectLinksCmd,
		"get":   objectGetCmd,
		"put":   objectPutCmd,
50
		"stat":  objectStatCmd,
51 52 53 54
	},
}

var objectDataCmd = &cmds.Command{
55 56 57
	Helptext: cmds.HelpText{
		Tagline: "Outputs the raw bytes in an IPFS object",
		ShortDescription: `
58 59 60 61 62 63 64 65
ipfs data is a plumbing command for retreiving the raw bytes stored in
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
`,
		LongDescription: `
ipfs data is a plumbing command for retreiving the raw bytes stored in
a DAG node. It outputs to stdout, and <key> is a base58 encoded
multihash.
66 67 68 69

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

	Arguments: []cmds.Argument{
73
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format").EnableStdin(),
74
	},
75
	Run: func(req cmds.Request, res cmds.Response) {
76 77
		n, err := req.Context().GetNode()
		if err != nil {
78 79
			res.SetError(err, cmds.ErrNormal)
			return
80
		}
81

Jeromy's avatar
Jeromy committed
82 83
		fpath := path.Path(req.Arguments()[0])
		output, err := objectData(n, fpath)
84 85 86 87 88
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(output)
89 90 91 92
	},
}

var objectLinksCmd = &cmds.Command{
93 94 95
	Helptext: cmds.HelpText{
		Tagline: "Outputs the links pointed to by the specified object",
		ShortDescription: `
Matt Bell's avatar
Matt Bell committed
96 97 98
'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.
99 100
`,
	},
101 102

	Arguments: []cmds.Argument{
103
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format").EnableStdin(),
104
	},
105
	Run: func(req cmds.Request, res cmds.Response) {
106 107
		n, err := req.Context().GetNode()
		if err != nil {
108 109
			res.SetError(err, cmds.ErrNormal)
			return
110
		}
111

Jeromy's avatar
Jeromy committed
112 113
		fpath := path.Path(req.Arguments()[0])
		output, err := objectLinks(n, fpath)
114 115 116 117 118
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(output)
119
	},
120
	Marshalers: cmds.MarshalerMap{
121
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
122 123
			object := res.Output().(*Object)
			marshalled := marshalLinks(object.Links)
124
			return strings.NewReader(marshalled), nil
125 126
		},
	},
127
	Type: Object{},
128 129 130
}

var objectGetCmd = &cmds.Command{
131 132 133 134
	Helptext: cmds.HelpText{
		Tagline: "Get and serialize the DAG node named by <key>",
		ShortDescription: `
'ipfs object get' is a plumbing command for retreiving DAG nodes.
135 136
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
137 138 139
`,
		LongDescription: `
'ipfs object get' is a plumbing command for retreiving DAG nodes.
140 141
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
142

143 144 145 146
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
147
(Specified by the "--encoding" or "-enc" flag)`,
148
	},
149 150

	Arguments: []cmds.Argument{
151
		cmds.StringArg("key", true, false, "Key of the object to retrieve (in base58-encoded multihash format)").EnableStdin(),
152
	},
153
	Run: func(req cmds.Request, res cmds.Response) {
154 155
		n, err := req.Context().GetNode()
		if err != nil {
156 157
			res.SetError(err, cmds.ErrNormal)
			return
158
		}
159

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

Jeromy's avatar
Jeromy committed
162
		object, err := objectGet(n, fpath)
163
		if err != nil {
164 165
			res.SetError(err, cmds.ErrNormal)
			return
166 167
		}

168 169
		node := &Node{
			Links: make([]Link, len(object.Links)),
170
			Data:  string(object.Data),
171 172 173 174 175 176 177 178 179 180
		}

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

181
		res.SetOutput(node)
182
	},
183
	Type: Node{},
184
	Marshalers: cmds.MarshalerMap{
185
		cmds.EncodingType("protobuf"): func(res cmds.Response) (io.Reader, error) {
186
			node := res.Output().(*Node)
187 188 189
			object, err := deserializeNode(node)
			if err != nil {
				return nil, err
190
			}
191 192 193 194 195 196

			marshaled, err := object.Marshal()
			if err != nil {
				return nil, err
			}
			return bytes.NewReader(marshaled), nil
197 198 199 200
		},
	},
}

201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
var objectStatCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Get stats for the DAG node named by <key>",
		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{
217
		cmds.StringArg("key", true, false, "Key of the object to retrieve (in base58-encoded multihash format)").EnableStdin(),
218
	},
219
	Run: func(req cmds.Request, res cmds.Response) {
220 221
		n, err := req.Context().GetNode()
		if err != nil {
222 223
			res.SetError(err, cmds.ErrNormal)
			return
224 225
		}

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

Jeromy's avatar
Jeromy committed
228
		object, err := objectGet(n, fpath)
229
		if err != nil {
230 231
			res.SetError(err, cmds.ErrNormal)
			return
232 233 234 235
		}

		ns, err := object.Stat()
		if err != nil {
236 237
			res.SetError(err, cmds.ErrNormal)
			return
238 239
		}

240
		res.SetOutput(ns)
241 242 243 244
	},
	Type: dag.NodeStat{},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
245
			ns := res.Output().(*dag.NodeStat)
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261

			var buf bytes.Buffer
			w := func(s string, n int) {
				buf.Write([]byte(fmt.Sprintf("%s: %d\n", s, n)))
			}
			w("NumLinks", ns.NumLinks)
			w("BlockSize", ns.BlockSize)
			w("LinksSize", ns.LinksSize)
			w("DataSize", ns.DataSize)
			w("CumulativeSize", ns.CumulativeSize)

			return &buf, nil
		},
	},
}

262
var objectPutCmd = &cmds.Command{
263 264 265 266 267 268 269 270
	Helptext: cmds.HelpText{
		Tagline: "Stores input as a DAG object, outputs its key",
		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.
271 272 273 274
It reads from stdin, and the output is a base58 encoded multihash.

Data should be in the format specified by <encoding>.
<encoding> may be one of the following:
Matt Bell's avatar
Matt Bell committed
275 276
	* "protobuf"
	* "json"
277
`,
278
	},
279 280

	Arguments: []cmds.Argument{
Matt Bell's avatar
Matt Bell committed
281
		cmds.FileArg("data", true, false, "Data to be stored as a DAG object"),
282
		cmds.StringArg("encoding", true, false, "Encoding type of <data>, either \"protobuf\" or \"json\""),
283
	},
284
	Run: func(req cmds.Request, res cmds.Response) {
285 286
		n, err := req.Context().GetNode()
		if err != nil {
287 288
			res.SetError(err, cmds.ErrNormal)
			return
289
		}
290

291 292
		input, err := req.Files().NextFile()
		if err != nil && err != io.EOF {
293 294
			res.SetError(err, cmds.ErrNormal)
			return
295 296
		}

297
		encoding := req.Arguments()[0]
298 299 300 301 302 303 304

		output, err := objectPut(n, input, encoding)
		if err != nil {
			errType := cmds.ErrNormal
			if err == ErrUnknownObjectEnc {
				errType = cmds.ErrClient
			}
305 306
			res.SetError(err, errType)
			return
307 308
		}

309
		res.SetOutput(output)
310
	},
311
	Marshalers: cmds.MarshalerMap{
312
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
313
			object := res.Output().(*Object)
314
			return strings.NewReader("added " + object.Hash), nil
315 316
		},
	},
317
	Type: Object{},
318 319 320
}

// objectData takes a key string and writes out the raw bytes of that node (if there is one)
Jeromy's avatar
Jeromy committed
321 322
func objectData(n *core.IpfsNode, fpath path.Path) (io.Reader, error) {
	dagnode, err := n.Resolver.ResolvePath(fpath)
323 324 325 326
	if err != nil {
		return nil, err
	}

Jeromy's avatar
Jeromy committed
327
	log.Debugf("objectData: found dagnode %s (# of bytes: %d - # links: %d)", fpath, len(dagnode.Data), len(dagnode.Links))
328 329 330 331 332

	return bytes.NewReader(dagnode.Data), nil
}

// objectLinks takes a key string and lists the links it points to
Jeromy's avatar
Jeromy committed
333 334
func objectLinks(n *core.IpfsNode, fpath path.Path) (*Object, error) {
	dagnode, err := n.Resolver.ResolvePath(fpath)
335 336 337 338
	if err != nil {
		return nil, err
	}

Jeromy's avatar
Jeromy committed
339
	log.Debugf("objectLinks: found dagnode %s (# of bytes: %d - # links: %d)", fpath, len(dagnode.Data), len(dagnode.Links))
340 341 342 343 344

	return getOutput(dagnode)
}

// objectGet takes a key string from args and a format option and serializes the dagnode to that format
Jeromy's avatar
Jeromy committed
345 346
func objectGet(n *core.IpfsNode, fpath path.Path) (*dag.Node, error) {
	dagnode, err := n.Resolver.ResolvePath(fpath)
347 348 349 350
	if err != nil {
		return nil, err
	}

Jeromy's avatar
Jeromy committed
351
	log.Debugf("objectGet: found dagnode %s (# of bytes: %d - # links: %d)", fpath, len(dagnode.Data), len(dagnode.Links))
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374

	return dagnode, nil
}

// 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) {
	var (
		dagnode *dag.Node
		data    []byte
		err     error
	)

	data, err = ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
	if err != nil {
		return nil, err
	}

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

	switch getObjectEnc(encoding) {
	case objectEncodingJSON:
375 376 377 378 379 380 381 382 383 384
		node := new(Node)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}

		dagnode, err = deserializeNode(node)
		if err != nil {
			return nil, err
		}
385 386 387 388 389 390 391 392 393 394 395 396 397 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 441 442 443 444 445 446

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

	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

	err = addNode(n, dagnode)
	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"
)

func getObjectEnc(o interface{}) objectEncoding {
	v, ok := o.(string)
	if !ok {
		// chosen as default because it's human readable
		log.Warning("option is not a string - falling back to json")
		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
}
447 448 449 450

// converts the Node object into a real dag.Node
func deserializeNode(node *Node) (*dag.Node, error) {
	dagnode := new(dag.Node)
451
	dagnode.Data = []byte(node.Data)
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
	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
}