pin.go 14.1 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 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
var verifyPinCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Verify that recursive pins are complete.",
	},
	Run: func(req cmds.Request, res cmds.Response) {
		n, _ := req.InvocContext().GetNode()

		rdr, wtr := io.Pipe()
		out := pinVerify(req.Context(), n)

		go func() {
			defer wtr.Close()
			for r := range out {
				r.Format(wtr)
			}
		}()

		res.SetOutput(rdr)
	},
}

435
type RefKeyObject struct {
Jeromy's avatar
Jeromy committed
436
	Type string
437 438
}

Jeromy's avatar
Jeromy committed
439
type RefKeyList struct {
440
	Keys map[string]RefKeyObject
Jeromy's avatar
Jeromy committed
441
}
442 443

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

445 446
	mode, ok := pin.StringToPinMode(typeStr)
	if !ok {
Hector Sanjuan's avatar
Hector Sanjuan committed
447
		return nil, fmt.Errorf("invalid pin mode '%s'", typeStr)
448 449
	}

450 451 452
	keys := make(map[string]RefKeyObject)

	for _, p := range args {
Jeromy's avatar
Jeromy committed
453
		pth, err := path.ParsePath(p)
454 455 456 457
		if err != nil {
			return nil, err
		}

458
		c, err := core.ResolveToCid(ctx, n, pth)
459 460 461 462
		if err != nil {
			return nil, err
		}

Jeromy's avatar
Jeromy committed
463
		pinType, pinned, err := n.Pinning.IsPinnedWithType(c, mode)
464 465 466 467 468
		if err != nil {
			return nil, err
		}

		if !pinned {
Hector Sanjuan's avatar
Hector Sanjuan committed
469
			return nil, fmt.Errorf("path '%s' is not pinned", p)
470 471 472 473 474 475 476
		}

		switch pinType {
		case "direct", "indirect", "recursive", "internal":
		default:
			pinType = "indirect through " + pinType
		}
Jeromy's avatar
Jeromy committed
477
		keys[c.String()] = RefKeyObject{
478 479 480 481 482 483
			Type: pinType,
		}
	}

	return keys, nil
}
484 485 486 487 488

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

	keys := make(map[string]RefKeyObject)

Jeromy's avatar
Jeromy committed
489 490 491
	AddToResultKeys := func(keyList []*cid.Cid, typeStr string) {
		for _, c := range keyList {
			keys[c.String()] = RefKeyObject{
492 493 494 495 496 497 498 499 500
				Type: typeStr,
			}
		}
	}

	if typeStr == "direct" || typeStr == "all" {
		AddToResultKeys(n.Pinning.DirectKeys(), "direct")
	}
	if typeStr == "indirect" || typeStr == "all" {
Jeromy's avatar
Jeromy committed
501
		set := cid.NewSet()
502
		for _, k := range n.Pinning.RecursiveKeys() {
503
			err := dag.EnumerateChildren(n.Context(), n.DAG.GetLinks, k, set.Visit)
504 505 506 507
			if err != nil {
				return nil, err
			}
		}
Jeromy's avatar
Jeromy committed
508
		AddToResultKeys(set.Keys(), "indirect")
509 510 511 512 513 514 515
	}
	if typeStr == "recursive" || typeStr == "all" {
		AddToResultKeys(n.Pinning.RecursiveKeys(), "recursive")
	}

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

517
type pinStatus struct {
518
	badNodes []badNode
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
}

type badNode struct {
	cid *cid.Cid
	err error
}

type pinVerifyRes struct {
	cid *cid.Cid
	pinStatus
}

func pinVerify(ctx context.Context, n *core.IpfsNode) <-chan pinVerifyRes {
	visited := make(map[string]pinStatus)
	getLinks := n.DAG.GetOfflineLinkService().GetLinks
	recPins := n.Pinning.RecursiveKeys()

	var checkPin func(root *cid.Cid) pinStatus
	checkPin = func(root *cid.Cid) pinStatus {
		key := root.String()
		if status, ok := visited[key]; ok {
			return status
		}

		links, err := getLinks(ctx, root)
		if err != nil {
545
			status := pinStatus{[]badNode{badNode{cid: root, err: err}}}
546 547 548 549 550 551 552
			visited[key] = status
			return status
		}

		status := pinStatus{}
		for _, lnk := range links {
			res := checkPin(lnk.Cid)
553 554
			if len(res.badNodes) > 0 {
				status.badNodes = append(status.badNodes, res.badNodes...)
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
			}
		}

		visited[key] = status
		return status
	}

	out := make(chan pinVerifyRes)
	go func() {
		defer close(out)
		for _, cid := range recPins {
			pinStatus := checkPin(cid)
			out <- pinVerifyRes{cid, pinStatus}
		}
	}()

	return out
}

func (r pinVerifyRes) Format(out io.Writer) {
575
	if len(r.badNodes) == 0 {
576 577 578
		fmt.Fprintf(out, "%s ok\n", r.cid)
	} else {
		fmt.Fprintf(out, "%s broken\n", r.cid)
579
		for _, e := range r.badNodes {
580 581 582 583 584
			fmt.Fprintf(out, "  %s: %s\n", e.cid, e.err)
		}
	}
}

Jeromy's avatar
Jeromy committed
585 586 587 588 589 590 591
func cidsToStrings(cs []*cid.Cid) []string {
	out := make([]string, 0, len(cs))
	for _, c := range cs {
		out = append(out, c.String())
	}
	return out
}