pin.go 15.5 KB
Newer Older
1 2 3
package commands

import (
Jeromy's avatar
Jeromy committed
4
	"bytes"
5
	"fmt"
Jeromy's avatar
Jeromy committed
6
	"io"
7
	"time"
8

9
	cmds "github.com/ipfs/go-ipfs/commands"
10
	core "github.com/ipfs/go-ipfs/core"
11
	corerepo "github.com/ipfs/go-ipfs/core/corerepo"
Jeromy's avatar
Jeromy committed
12
	dag "github.com/ipfs/go-ipfs/merkledag"
13
	path "github.com/ipfs/go-ipfs/path"
14
	pin "github.com/ipfs/go-ipfs/pin"
Jeromy's avatar
Jeromy committed
15

16
	context "context"
17 18
	u "gx/ipfs/QmWbjfz3u6HkAdPh34dgPchGbQjob6LXLhAeCGii2TX69n/go-ipfs-util"
	cid "gx/ipfs/QmYhQaCYEcaPPjxJX7YcPcVKkQfRy6sJ7B3XmGFk82XYdQ/go-cid"
19 20
)

21
var PinCmd = &cmds.Command{
22
	Helptext: cmds.HelpText{
rht's avatar
rht committed
23
		Tagline: "Pin (and unpin) objects to local storage.",
24
	},
25

Brian Tiger Chow's avatar
Brian Tiger Chow committed
26
	Subcommands: map[string]*cmds.Command{
Jeromy's avatar
Jeromy committed
27 28 29
		"add":    addPinCmd,
		"rm":     rmPinCmd,
		"ls":     listPinCmd,
30
		"verify": verifyPinCmd,
Jeromy's avatar
Jeromy committed
31
		"update": updatePinCmd,
Brian Tiger Chow's avatar
Brian Tiger Chow committed
32 33 34
	},
}

Jeromy's avatar
Jeromy committed
35
type PinOutput struct {
Jeromy's avatar
Jeromy committed
36
	Pins []string
Jeromy's avatar
Jeromy committed
37 38
}

39
type AddPinOutput struct {
Jeromy's avatar
Jeromy committed
40
	Pins     []string
41 42 43
	Progress int `json:",omitempty"`
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
44
var addPinCmd = &cmds.Command{
45
	Helptext: cmds.HelpText{
46
		Tagline:          "Pin objects to local storage.",
Richard Littauer's avatar
Richard Littauer committed
47
		ShortDescription: "Stores an IPFS object(s) from a given path locally to disk.",
48
	},
49

50
	Arguments: []cmds.Argument{
Jeromy's avatar
Jeromy committed
51
		cmds.StringArg("ipfs-path", true, true, "Path to object(s) to be pinned.").EnableStdin(),
52 53
	},
	Options: []cmds.Option{
54
		cmds.BoolOption("recursive", "r", "Recursively pin the object linked to by the specified object(s).").Default(true),
55
		cmds.BoolOption("progress", "Show progress"),
56
	},
57
	Type: AddPinOutput{},
58
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
59
		n, err := req.InvocContext().GetNode()
60
		if err != nil {
61 62
			res.SetError(err, cmds.ErrNormal)
			return
63
		}
64

65
		defer n.Blockstore.PinLock().Unlock()
Jeromy's avatar
Jeromy committed
66

67
		// set recursive flag
68
		recursive, _, err := req.Option("recursive").Bool()
69
		if err != nil {
70 71
			res.SetError(err, cmds.ErrNormal)
			return
72
		}
73
		showProgress, _, _ := req.Option("progress").Bool()
74

75 76 77 78 79 80
		if !showProgress {
			added, err := corerepo.Pin(n, req.Context(), req.Arguments(), recursive)
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
Jeromy's avatar
Jeromy committed
81
			res.SetOutput(&AddPinOutput{Pins: cidsToStrings(added)})
82 83 84
			return
		}

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
		v := new(dag.ProgressTracker)
		ctx := v.DeriveContext(req.Context())

		ch := make(chan []*cid.Cid)
		go func() {
			defer close(ch)
			added, err := corerepo.Pin(n, ctx, req.Arguments(), recursive)
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
			ch <- added
		}()
		out := make(chan interface{})
		res.SetOutput((<-chan interface{})(out))
		go func() {
			ticker := time.NewTicker(500 * time.Millisecond)
			defer ticker.Stop()
			defer close(out)
			for {
				select {
				case val, ok := <-ch:
					if !ok {
						// error already set just return
						return
					}
					if pv := v.Value(); pv != 0 {
						out <- &AddPinOutput{Progress: v.Value()}
					}
Jeromy's avatar
Jeromy committed
114
					out <- &AddPinOutput{Pins: cidsToStrings(val)}
115 116 117 118 119 120 121 122 123
					return
				case <-ticker.C:
					out <- &AddPinOutput{Progress: v.Value()}
				case <-ctx.Done():
					res.SetError(ctx.Err(), cmds.ErrNormal)
					return
				}
			}
		}()
Jeromy's avatar
Jeromy committed
124 125 126
	},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
Jeromy's avatar
Jeromy committed
127
			var added []string
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152

			switch out := res.Output().(type) {
			case *AddPinOutput:
				added = out.Pins
			case <-chan interface{}:
				progressLine := false
				for r0 := range out {
					r := r0.(*AddPinOutput)
					if r.Pins != nil {
						added = r.Pins
					} else {
						if progressLine {
							fmt.Fprintf(res.Stderr(), "\r")
						}
						fmt.Fprintf(res.Stderr(), "Fetched/Processed %d nodes", r.Progress)
						progressLine = true
					}
				}
				if progressLine {
					fmt.Fprintf(res.Stderr(), "\n")
				}
				if res.Error() != nil {
					return nil, res.Error()
				}
			default:
Jeromy's avatar
Jeromy committed
153 154
				return nil, u.ErrCast()
			}
Jeromy's avatar
Jeromy committed
155
			var pintype string
156 157
			rec, found, _ := res.Request().Option("recursive").Bool()
			if rec || !found {
Jeromy's avatar
Jeromy committed
158 159 160 161 162
				pintype = "recursively"
			} else {
				pintype = "directly"
			}

163
			buf := new(bytes.Buffer)
164
			for _, k := range added {
165
				fmt.Fprintf(buf, "pinned %s %s\n", k, pintype)
Jeromy's avatar
Jeromy committed
166
			}
167
			return buf, nil
Jeromy's avatar
Jeromy committed
168
		},
169 170 171
	},
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
172
var rmPinCmd = &cmds.Command{
173
	Helptext: cmds.HelpText{
174
		Tagline: "Remove pinned objects from local storage.",
175
		ShortDescription: `
176
Removes the pin from the given object allowing it to be garbage
Richard Littauer's avatar
Richard Littauer committed
177
collected if needed. (By default, recursively. Use -r=false for direct pins.)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
178
`,
179
	},
180

181
	Arguments: []cmds.Argument{
Jeromy's avatar
Jeromy committed
182
		cmds.StringArg("ipfs-path", true, true, "Path to object(s) to be unpinned.").EnableStdin(),
183 184
	},
	Options: []cmds.Option{
185
		cmds.BoolOption("recursive", "r", "Recursively unpin the object linked to by the specified object(s).").Default(true),
186
	},
Jeromy's avatar
Jeromy committed
187
	Type: PinOutput{},
188
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
189
		n, err := req.InvocContext().GetNode()
190
		if err != nil {
191 192
			res.SetError(err, cmds.ErrNormal)
			return
193
		}
194 195

		// set recursive flag
196
		recursive, _, err := req.Option("recursive").Bool()
197
		if err != nil {
198 199
			res.SetError(err, cmds.ErrNormal)
			return
200
		}
201

202 203 204 205 206
		removed, err := corerepo.Unpin(n, req.Context(), req.Arguments(), recursive)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
207

Jeromy's avatar
Jeromy committed
208
		res.SetOutput(&PinOutput{cidsToStrings(removed)})
Jeromy's avatar
Jeromy committed
209 210 211
	},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
212
			added, ok := res.Output().(*PinOutput)
Jeromy's avatar
Jeromy committed
213 214 215 216
			if !ok {
				return nil, u.ErrCast()
			}

217 218 219
			buf := new(bytes.Buffer)
			for _, k := range added.Pins {
				fmt.Fprintf(buf, "unpinned %s\n", k)
Jeromy's avatar
Jeromy committed
220
			}
221
			return buf, nil
Jeromy's avatar
Jeromy committed
222
		},
223 224
	},
}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
225

226 227
var listPinCmd = &cmds.Command{
	Helptext: cmds.HelpText{
rht's avatar
rht committed
228
		Tagline: "List objects pinned to local storage.",
229
		ShortDescription: `
230
Returns a list of objects that are pinned locally.
231 232 233
By default, all pinned objects are returned, but the '--type' flag or
arguments can restrict that to a specific pin type or to some specific objects
respectively.
234 235
`,
		LongDescription: `
236
Returns a list of objects that are pinned locally.
237 238 239
By default, all pinned objects are returned, but the '--type' flag or
arguments can restrict that to a specific pin type or to some specific objects
respectively.
Jeromy's avatar
Jeromy committed
240

241 242
Use --type=<type> to specify the type of pinned keys to list.
Valid values are:
Jeromy's avatar
Jeromy committed
243
    * "direct": pin that specific object.
244 245
    * "recursive": pin that specific object, and indirectly pin all its
    	descendants
Jeromy's avatar
Jeromy committed
246 247 248
    * "indirect": pinned indirectly by an ancestor (like a refcount)
    * "all"

249 250 251
With arguments, the command fails if any of the arguments is not a pinned
object. And if --type=<type> is additionally used, the command will also fail
if any of the arguments is not of the specified type.
252

253 254 255 256
Example:
	$ echo "hello" | ipfs add -q
	QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN
	$ ipfs pin ls
257
	QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN recursive
258 259
	# now remove the pin, and repin it directly
	$ ipfs pin rm QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN
260
	unpinned QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN
261
	$ ipfs pin add -r=false QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN
262
	pinned QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN directly
263
	$ ipfs pin ls --type=direct
264 265 266
	QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN direct
	$ ipfs pin ls QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN
	QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN direct
267 268 269
`,
	},

270
	Arguments: []cmds.Argument{
271
		cmds.StringArg("ipfs-path", false, true, "Path to object(s) to be listed."),
272
	},
273
	Options: []cmds.Option{
274
		cmds.StringOption("type", "t", "The type of pinned keys to list. Can be \"direct\", \"indirect\", \"recursive\", or \"all\".").Default("all"),
275
		cmds.BoolOption("quiet", "q", "Write just hashes of objects.").Default(false),
276
	},
277
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
278
		n, err := req.InvocContext().GetNode()
279
		if err != nil {
280 281
			res.SetError(err, cmds.ErrNormal)
			return
282 283
		}

284
		typeStr, _, err := req.Option("type").String()
285
		if err != nil {
286 287
			res.SetError(err, cmds.ErrNormal)
			return
288
		}
289

290 291 292 293 294 295
		switch typeStr {
		case "all", "direct", "indirect", "recursive":
		default:
			err = fmt.Errorf("Invalid type '%s', must be one of {direct, indirect, recursive, all}", typeStr)
			res.SetError(err, cmds.ErrClient)
			return
296 297
		}

298 299
		var keys map[string]RefKeyObject

300
		if len(req.Arguments()) > 0 {
301 302 303
			keys, err = pinLsKeys(req.Arguments(), typeStr, req.Context(), n)
		} else {
			keys, err = pinLsAll(typeStr, req.Context(), n)
304
		}
305

306 307 308 309
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
		} else {
			res.SetOutput(&RefKeyList{Keys: keys})
310
		}
311
	},
Jeromy's avatar
Jeromy committed
312
	Type: RefKeyList{},
313
	Marshalers: cmds.MarshalerMap{
Jeromy's avatar
Jeromy committed
314
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
315 316 317 318 319
			quiet, _, err := res.Request().Option("quiet").Bool()
			if err != nil {
				return nil, err
			}

Jeromy's avatar
Jeromy committed
320 321 322 323 324
			keys, ok := res.Output().(*RefKeyList)
			if !ok {
				return nil, u.ErrCast()
			}
			out := new(bytes.Buffer)
Jeromy's avatar
Jeromy committed
325 326 327 328 329
			for k, v := range keys.Keys {
				if quiet {
					fmt.Fprintf(out, "%s\n", k)
				} else {
					fmt.Fprintf(out, "%s %s\n", k, v.Type)
Jeromy's avatar
Jeromy committed
330 331 332 333
				}
			}
			return out, nil
		},
334 335
	},
}
Jeromy's avatar
Jeromy committed
336

Jeromy's avatar
Jeromy committed
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 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
var updatePinCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Update a recursive pin",
		ShortDescription: `
Updates one pin to another, making sure that all objects in the new pin are
local.  Then removes the old pin. This is an optimized version of adding the
new pin and removing the old one.
`,
	},

	Arguments: []cmds.Argument{
		cmds.StringArg("from-path", true, false, "Path to old object."),
		cmds.StringArg("to-path", true, false, "Path to new object to be pinned."),
	},
	Options: []cmds.Option{
		cmds.BoolOption("unpin", "Remove the old pin.").Default(true),
	},
	Type: PinOutput{},
	Run: func(req cmds.Request, res cmds.Response) {
		n, err := req.InvocContext().GetNode()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		unpin, _, err := req.Option("unpin").Bool()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		from, err := path.ParsePath(req.Arguments()[0])
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		to, err := path.ParsePath(req.Arguments()[1])
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		fromc, err := core.ResolveToCid(req.Context(), n, from)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		toc, err := core.ResolveToCid(req.Context(), n, to)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		err = n.Pinning.Update(req.Context(), fromc, toc, unpin)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		res.SetOutput(&PinOutput{Pins: []string{from.String(), to.String()}})
	},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
			added, ok := res.Output().(*PinOutput)
			if !ok {
				return nil, u.ErrCast()
			}

			buf := new(bytes.Buffer)
			fmt.Fprintf(buf, "updated %s to %s\n", added.Pins[0], added.Pins[1])
			return buf, nil
		},
	},
}

414 415 416 417
var verifyPinCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Verify that recursive pins are complete.",
	},
418 419 420 421
	Options: []cmds.Option{
		cmds.BoolOption("verbose", "Also write the hashes of non-broken pins."),
		cmds.BoolOption("quiet", "q", "Write just hashes of broken pins."),
	},
422
	Run: func(req cmds.Request, res cmds.Response) {
423 424 425 426 427 428 429 430
		n, err := req.InvocContext().GetNode()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		verbose, _, _ := res.Request().Option("verbose").Bool()
		quiet, _, _ := res.Request().Option("quiet").Bool()
431

Kevin Atkinson's avatar
Kevin Atkinson committed
432 433 434
		if verbose && quiet {
			res.SetError(fmt.Errorf("The --verbose and --quiet options can not be used at the same time"), cmds.ErrNormal)
		}
435

Kevin Atkinson's avatar
Kevin Atkinson committed
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
		opts := pinVerifyOpts{
			explain:   !quiet,
			includeOk: verbose,
		}
		out := pinVerify(req.Context(), n, opts)

		res.SetOutput(out)
	},
	Type: PinVerifyRes{},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
			quiet, _, _ := res.Request().Option("quiet").Bool()

			outChan, ok := res.Output().(<-chan interface{})
			if !ok {
				return nil, u.ErrCast()
452 453
			}

Kevin Atkinson's avatar
Kevin Atkinson committed
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
			rdr, wtr := io.Pipe()
			go func() {
				defer wtr.Close()
				for r0 := range outChan {
					r := r0.(*PinVerifyRes)
					if quiet && !r.Ok {
						fmt.Fprintf(wtr, "%s\n", r.Cid)
					} else if !quiet {
						r.Format(wtr)
					}
				}
			}()

			return rdr, nil
		},
469 470 471
	},
}

472
type RefKeyObject struct {
Jeromy's avatar
Jeromy committed
473
	Type string
474 475
}

Jeromy's avatar
Jeromy committed
476
type RefKeyList struct {
477
	Keys map[string]RefKeyObject
Jeromy's avatar
Jeromy committed
478
}
479 480

func pinLsKeys(args []string, typeStr string, ctx context.Context, n *core.IpfsNode) (map[string]RefKeyObject, error) {
481

482 483
	mode, ok := pin.StringToPinMode(typeStr)
	if !ok {
Hector Sanjuan's avatar
Hector Sanjuan committed
484
		return nil, fmt.Errorf("invalid pin mode '%s'", typeStr)
485 486
	}

487 488 489
	keys := make(map[string]RefKeyObject)

	for _, p := range args {
Jeromy's avatar
Jeromy committed
490
		pth, err := path.ParsePath(p)
491 492 493 494
		if err != nil {
			return nil, err
		}

495
		c, err := core.ResolveToCid(ctx, n, pth)
496 497 498 499
		if err != nil {
			return nil, err
		}

Jeromy's avatar
Jeromy committed
500
		pinType, pinned, err := n.Pinning.IsPinnedWithType(c, mode)
501 502 503 504 505
		if err != nil {
			return nil, err
		}

		if !pinned {
Hector Sanjuan's avatar
Hector Sanjuan committed
506
			return nil, fmt.Errorf("path '%s' is not pinned", p)
507 508 509 510 511 512 513
		}

		switch pinType {
		case "direct", "indirect", "recursive", "internal":
		default:
			pinType = "indirect through " + pinType
		}
Jeromy's avatar
Jeromy committed
514
		keys[c.String()] = RefKeyObject{
515 516 517 518 519 520
			Type: pinType,
		}
	}

	return keys, nil
}
521 522 523 524 525

func pinLsAll(typeStr string, ctx context.Context, n *core.IpfsNode) (map[string]RefKeyObject, error) {

	keys := make(map[string]RefKeyObject)

Jeromy's avatar
Jeromy committed
526 527 528
	AddToResultKeys := func(keyList []*cid.Cid, typeStr string) {
		for _, c := range keyList {
			keys[c.String()] = RefKeyObject{
529 530 531 532 533 534 535 536 537
				Type: typeStr,
			}
		}
	}

	if typeStr == "direct" || typeStr == "all" {
		AddToResultKeys(n.Pinning.DirectKeys(), "direct")
	}
	if typeStr == "indirect" || typeStr == "all" {
Jeromy's avatar
Jeromy committed
538
		set := cid.NewSet()
539
		for _, k := range n.Pinning.RecursiveKeys() {
540
			err := dag.EnumerateChildren(n.Context(), n.DAG.GetLinks, k, set.Visit)
541 542 543 544
			if err != nil {
				return nil, err
			}
		}
Jeromy's avatar
Jeromy committed
545
		AddToResultKeys(set.Keys(), "indirect")
546 547 548 549 550 551 552
	}
	if typeStr == "recursive" || typeStr == "all" {
		AddToResultKeys(n.Pinning.RecursiveKeys(), "recursive")
	}

	return keys, nil
}
Jeromy's avatar
Jeromy committed
553

Kevin Atkinson's avatar
Kevin Atkinson committed
554 555 556 557
// PinVerifyRes is the result returned for each pin checked in "pin verify"
type PinVerifyRes struct {
	Cid string
	PinStatus
558 559
}

Kevin Atkinson's avatar
Kevin Atkinson committed
560 561 562 563
// PinStatus is part of PinVerifyRes, do not use directly
type PinStatus struct {
	Ok       bool
	BadNodes []BadNode `json:",omitempty"`
564 565
}

Kevin Atkinson's avatar
Kevin Atkinson committed
566 567 568 569
// BadNode is used in PinVerifyRes
type BadNode struct {
	Cid string
	Err string
570 571
}

Kevin Atkinson's avatar
Kevin Atkinson committed
572 573 574 575 576 577 578
type pinVerifyOpts struct {
	explain   bool
	includeOk bool
}

func pinVerify(ctx context.Context, n *core.IpfsNode, opts pinVerifyOpts) <-chan interface{} {
	visited := make(map[string]PinStatus)
579 580 581
	getLinks := n.DAG.GetOfflineLinkService().GetLinks
	recPins := n.Pinning.RecursiveKeys()

Kevin Atkinson's avatar
Kevin Atkinson committed
582 583
	var checkPin func(root *cid.Cid) PinStatus
	checkPin = func(root *cid.Cid) PinStatus {
584 585 586 587 588 589 590
		key := root.String()
		if status, ok := visited[key]; ok {
			return status
		}

		links, err := getLinks(ctx, root)
		if err != nil {
Kevin Atkinson's avatar
Kevin Atkinson committed
591 592 593 594
			status := PinStatus{Ok: false}
			if opts.explain {
				status.BadNodes = []BadNode{BadNode{Cid: key, Err: err.Error()}}
			}
595 596 597 598
			visited[key] = status
			return status
		}

Kevin Atkinson's avatar
Kevin Atkinson committed
599
		status := PinStatus{Ok: true}
600 601
		for _, lnk := range links {
			res := checkPin(lnk.Cid)
Kevin Atkinson's avatar
Kevin Atkinson committed
602 603 604
			if !res.Ok {
				status.Ok = false
				status.BadNodes = append(status.BadNodes, res.BadNodes...)
605 606 607 608 609 610 611
			}
		}

		visited[key] = status
		return status
	}

Kevin Atkinson's avatar
Kevin Atkinson committed
612
	out := make(chan interface{})
613 614 615 616
	go func() {
		defer close(out)
		for _, cid := range recPins {
			pinStatus := checkPin(cid)
Kevin Atkinson's avatar
Kevin Atkinson committed
617 618 619
			if !pinStatus.Ok || opts.includeOk {
				out <- &PinVerifyRes{cid.String(), pinStatus}
			}
620 621 622 623 624 625
		}
	}()

	return out
}

Kevin Atkinson's avatar
Kevin Atkinson committed
626 627 628 629
// Format formats PinVerifyRes
func (r PinVerifyRes) Format(out io.Writer) {
	if r.Ok {
		fmt.Fprintf(out, "%s ok\n", r.Cid)
630
	} else {
Kevin Atkinson's avatar
Kevin Atkinson committed
631 632 633
		fmt.Fprintf(out, "%s broken\n", r.Cid)
		for _, e := range r.BadNodes {
			fmt.Fprintf(out, "  %s: %s\n", e.Cid, e.Err)
634 635 636 637
		}
	}
}

Jeromy's avatar
Jeromy committed
638 639 640 641 642 643 644
func cidsToStrings(cs []*cid.Cid) []string {
	out := make([]string, 0, len(cs))
	for _, c := range cs {
		out = append(out, c.String())
	}
	return out
}