object.go 17.9 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
	"text/tabwriter"
12
	"time"
13

14
	mh "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"
15
	context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
16

17
	key "github.com/ipfs/go-ipfs/blocks/key"
18 19 20 21
	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"
22 23
	ft "github.com/ipfs/go-ipfs/unixfs"
	u "github.com/ipfs/go-ipfs/util"
24 25 26 27 28 29 30
)

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

31 32
type Node struct {
	Links []Link
33
	Data  string
34 35
}

36 37 38 39 40 41 42 43 44 45
type Link struct {
	Name, Hash string
	Size       uint64
}

type Object struct {
	Hash  string
	Links []Link
}

46
var ObjectCmd = &cmds.Command{
47
	Helptext: cmds.HelpText{
Matt Bell's avatar
Matt Bell committed
48 49 50 51 52
		Tagline: "Interact with ipfs objects",
		ShortDescription: `
'ipfs object' is a plumbing command used to manipulate DAG objects
directly.`,
		Synopsis: `
53 54 55 56 57 58 59
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
60
`,
61
	},
62 63 64 65 66 67

	Subcommands: map[string]*cmds.Command{
		"data":  objectDataCmd,
		"links": objectLinksCmd,
		"get":   objectGetCmd,
		"put":   objectPutCmd,
68
		"stat":  objectStatCmd,
69 70
		"new":   objectNewCmd,
		"patch": objectPatchCmd,
71 72 73 74
	},
}

var objectDataCmd = &cmds.Command{
75 76 77
	Helptext: cmds.HelpText{
		Tagline: "Outputs the raw bytes in an IPFS object",
		ShortDescription: `
78 79 80 81 82 83 84 85
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.
86 87 88 89

Note that the "--encoding" option does not affect the output, since the
output is the raw data of the object.
`,
90
	},
91 92

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

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

var objectLinksCmd = &cmds.Command{
113 114 115
	Helptext: cmds.HelpText{
		Tagline: "Outputs the links pointed to by the specified object",
		ShortDescription: `
Matt Bell's avatar
Matt Bell committed
116 117 118
'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.
119 120
`,
	},
121 122

	Arguments: []cmds.Argument{
123
		cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format").EnableStdin(),
124
	},
125
	Run: func(req cmds.Request, res cmds.Response) {
126 127
		n, err := req.Context().GetNode()
		if err != nil {
128 129
			res.SetError(err, cmds.ErrNormal)
			return
130
		}
131

Jeromy's avatar
Jeromy committed
132
		fpath := path.Path(req.Arguments()[0])
133 134 135 136 137 138
		node, err := core.Resolve(req.Context().Context, n, fpath)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		output, err := getOutput(node)
139 140 141 142 143
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(output)
144
	},
145
	Marshalers: cmds.MarshalerMap{
146
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
147
			object := res.Output().(*Object)
148 149
			buf := new(bytes.Buffer)
			w := tabwriter.NewWriter(buf, 1, 2, 1, ' ', 0)
150 151 152 153
			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)
			}
154
			w.Flush()
155
			return buf, nil
156 157
		},
	},
158
	Type: Object{},
159 160 161
}

var objectGetCmd = &cmds.Command{
162 163 164 165
	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.
166 167
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
168 169 170
`,
		LongDescription: `
'ipfs object get' is a plumbing command for retreiving DAG nodes.
171 172
It serializes the DAG node to the format specified by the "--encoding"
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
173

174 175 176 177
This command outputs data in the following encodings:
  * "protobuf"
  * "json"
  * "xml"
178
(Specified by the "--encoding" or "-enc" flag)`,
179
	},
180 181

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

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

193
		object, err := core.Resolve(req.Context().Context, n, fpath)
194
		if err != nil {
195 196
			res.SetError(err, cmds.ErrNormal)
			return
197 198
		}

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

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

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

			marshaled, err := object.Marshal()
			if err != nil {
				return nil, err
			}
			return bytes.NewReader(marshaled), nil
228 229 230 231
		},
	},
}

232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
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{
248
		cmds.StringArg("key", true, false, "Key of the object to retrieve (in base58-encoded multihash format)").EnableStdin(),
249
	},
250
	Run: func(req cmds.Request, res cmds.Response) {
251 252
		n, err := req.Context().GetNode()
		if err != nil {
253 254
			res.SetError(err, cmds.ErrNormal)
			return
255 256
		}

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

259
		object, err := core.Resolve(req.Context().Context, n, fpath)
260
		if err != nil {
261 262
			res.SetError(err, cmds.ErrNormal)
			return
263 264 265 266
		}

		ns, err := object.Stat()
		if err != nil {
267 268
			res.SetError(err, cmds.ErrNormal)
			return
269 270
		}

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

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

288
			return buf, nil
289 290 291 292
		},
	},
}

293
var objectPutCmd = &cmds.Command{
294 295 296 297 298 299 300 301
	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.
302 303
It reads from stdin, and the output is a base58 encoded multihash.

Henry's avatar
Henry committed
304 305
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
306
	* "protobuf"
Henry's avatar
Henry committed
307
	* "json" (default)
308
`,
309
	},
310 311

	Arguments: []cmds.Argument{
Henry's avatar
Henry committed
312 313 314 315
		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\""),
316
	},
317
	Run: func(req cmds.Request, res cmds.Response) {
318 319
		n, err := req.Context().GetNode()
		if err != nil {
320 321
			res.SetError(err, cmds.ErrNormal)
			return
322
		}
323

324 325
		input, err := req.Files().NextFile()
		if err != nil && err != io.EOF {
326 327
			res.SetError(err, cmds.ErrNormal)
			return
328 329
		}

Henry's avatar
Henry committed
330 331 332 333 334 335 336 337
		inputenc, found, err := req.Option("inputenc").String()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		if !found {
			inputenc = "json"
		}
338

Henry's avatar
Henry committed
339
		output, err := objectPut(n, input, inputenc)
340 341 342 343 344
		if err != nil {
			errType := cmds.ErrNormal
			if err == ErrUnknownObjectEnc {
				errType = cmds.ErrClient
			}
345 346
			res.SetError(err, errType)
			return
347 348
		}

349
		res.SetOutput(output)
350
	},
351
	Marshalers: cmds.MarshalerMap{
352
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
353
			object := res.Output().(*Object)
354
			return strings.NewReader("added " + object.Hash), nil
355 356
		},
	},
357
	Type: Object{},
358 359
}

360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 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
var objectNewCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "creates a new object from an ipfs template",
		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.
`,
	},
	Arguments: []cmds.Argument{
		cmds.StringArg("template", false, false, "optional template to use"),
	},
	Run: func(req cmds.Request, res cmds.Response) {
		n, err := req.Context().GetNode()
		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{},
}

var objectPatchCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Create a new merkledag object based on an existing one",
		ShortDescription: `
'ipfs patch <root> [add-link|rm-link] <args>' is a plumbing command used to
build custom DAG objects. It adds and removes links from objects, creating a new
object as a result. This is the merkle-dag version of modifying an object.

Examples:

    EMPTY_DIR=$(ipfs object new unixfs-dir)
    BAR=$(echo "bar" | ipfs add -q)
    ipfs patch $EMPTY_DIR add-link foo $BAR

This takes an empty directory, and adds a link named foo under it, pointing to
a file containing 'bar', and returns the hash of the new object.

    ipfs patch $FOO_BAR rm-link foo

This removes the link named foo from the hash in $FOO_BAR and returns the
resulting object hash.
`,
	},
	Options: []cmds.Option{},
	Arguments: []cmds.Argument{
		cmds.StringArg("root", true, false, "the hash of the node to modify"),
		cmds.StringArg("command", true, false, "the operation to perform"),
		cmds.StringArg("args", true, true, "extra arguments").EnableStdin(),
	},
439
	Type: Object{},
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
	Run: func(req cmds.Request, res cmds.Response) {
		nd, err := req.Context().GetNode()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		rhash := key.B58KeyDecode(req.Arguments()[0])
		if rhash == "" {
			res.SetError(fmt.Errorf("incorrectly formatted root hash"), cmds.ErrNormal)
			return
		}

		ctx, cancel := context.WithTimeout(req.Context().Context, time.Second*30)
		rnode, err := nd.DAG.Get(ctx, rhash)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			cancel()
			return
		}
		cancel()

		action := req.Arguments()[1]

		switch action {
		case "add-link":
			k, err := addLinkCaller(req, rnode)
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
471
			res.SetOutput(&Object{Hash: k.B58String()})
472 473 474 475 476 477
		case "rm-link":
			k, err := rmLinkCaller(req, rnode)
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
478
			res.SetOutput(&Object{Hash: k.B58String()})
479 480 481 482 483 484
		case "set-data":
			k, err := setDataCaller(req, rnode)
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
485
			res.SetOutput(&Object{Hash: k.B58String()})
486 487 488 489 490 491
		case "append-data":
			k, err := appendDataCaller(req, rnode)
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
492
			res.SetOutput(&Object{Hash: k.B58String()})
493 494 495 496 497 498 499
		default:
			res.SetError(fmt.Errorf("unrecognized subcommand"), cmds.ErrNormal)
			return
		}
	},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
500
			o, ok := res.Output().(*Object)
501 502 503 504
			if !ok {
				return nil, u.ErrCast()
			}

505
			return strings.NewReader(o.Hash + "\n"), nil
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 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 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627
		},
	},
}

func appendDataCaller(req cmds.Request, root *dag.Node) (key.Key, error) {
	if len(req.Arguments()) < 3 {
		return "", fmt.Errorf("not enough arguments for set-data")
	}

	nd, err := req.Context().GetNode()
	if err != nil {
		return "", err
	}

	root.Data = append(root.Data, []byte(req.Arguments()[2])...)

	newkey, err := nd.DAG.Add(root)
	if err != nil {
		return "", err
	}

	return newkey, nil
}

func setDataCaller(req cmds.Request, root *dag.Node) (key.Key, error) {
	if len(req.Arguments()) < 3 {
		return "", fmt.Errorf("not enough arguments for set-data")
	}

	nd, err := req.Context().GetNode()
	if err != nil {
		return "", err
	}

	root.Data = []byte(req.Arguments()[2])

	newkey, err := nd.DAG.Add(root)
	if err != nil {
		return "", err
	}

	return newkey, nil
}

func rmLinkCaller(req cmds.Request, root *dag.Node) (key.Key, error) {
	if len(req.Arguments()) < 3 {
		return "", fmt.Errorf("not enough arguments for rm-link")
	}

	nd, err := req.Context().GetNode()
	if err != nil {
		return "", err
	}

	name := req.Arguments()[2]

	err = root.RemoveNodeLink(name)
	if err != nil {
		return "", err
	}

	newkey, err := nd.DAG.Add(root)
	if err != nil {
		return "", err
	}

	return newkey, nil
}

func addLinkCaller(req cmds.Request, root *dag.Node) (key.Key, error) {
	if len(req.Arguments()) < 4 {
		return "", fmt.Errorf("not enough arguments for add-link")
	}

	nd, err := req.Context().GetNode()
	if err != nil {
		return "", err
	}

	name := req.Arguments()[2]
	childk := key.B58KeyDecode(req.Arguments()[3])

	newkey, err := addLink(req.Context().Context, nd.DAG, root, name, childk)
	if err != nil {
		return "", err
	}

	return newkey, nil
}

func addLink(ctx context.Context, ds dag.DAGService, root *dag.Node, childname string, childk key.Key) (key.Key, error) {
	ctx, cancel := context.WithTimeout(ctx, time.Second*30)
	childnd, err := ds.Get(ctx, childk)
	if err != nil {
		cancel()
		return "", err
	}
	cancel()

	err = root.AddNodeLinkClean(childname, childnd)
	if err != nil {
		return "", err
	}

	newkey, err := ds.Add(root)
	if err != nil {
		return "", err
	}
	return newkey, nil
}

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)
	}
}

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

631 632 633
// 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) {

634
	data, err := ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
635 636 637 638 639 640 641 642
	if err != nil {
		return nil, err
	}

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

643
	var dagnode *dag.Node
644 645
	switch getObjectEnc(encoding) {
	case objectEncodingJSON:
646 647 648 649 650 651
		node := new(Node)
		err = json.Unmarshal(data, node)
		if err != nil {
			return nil, err
		}

652 653 654 655 656 657
		// check that we have data in the Node to add
		// otherwise we will add the empty object without raising an error
		if node.Data == "" && len(node.Links) == 0 {
			return nil, ErrEmptyNode
		}

658 659 660 661
		dagnode, err = deserializeNode(node)
		if err != nil {
			return nil, err
		}
662 663 664 665 666 667 668 669 670 671 672 673

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

	default:
		return nil, ErrUnknownObjectEnc
	}

	if err != nil {
		return nil, err
	}

674
	_, err = n.DAG.Add(dagnode)
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
	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
}
724 725 726 727

// converts the Node object into a real dag.Node
func deserializeNode(node *Node) (*dag.Node, error) {
	dagnode := new(dag.Node)
728
	dagnode.Data = []byte(node.Data)
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743
	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
}