pin.go 12.4 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 30
		"add":    addPinCmd,
		"rm":     rmPinCmd,
		"ls":     listPinCmd,
		"update": updatePinCmd,
Brian Tiger Chow's avatar
Brian Tiger Chow committed
31 32 33
	},
}

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

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

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

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

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

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

74 75 76 77 78 79
		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
80
			res.SetOutput(&AddPinOutput{Pins: cidsToStrings(added)})
81 82 83
			return
		}

84 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
		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
113
					out <- &AddPinOutput{Pins: cidsToStrings(val)}
114 115 116 117 118 119 120 121 122
					return
				case <-ticker.C:
					out <- &AddPinOutput{Progress: v.Value()}
				case <-ctx.Done():
					res.SetError(ctx.Err(), cmds.ErrNormal)
					return
				}
			}
		}()
Jeromy's avatar
Jeromy committed
123 124 125
	},
	Marshalers: cmds.MarshalerMap{
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
Jeromy's avatar
Jeromy committed
126
			var added []string
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151

			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
152 153
				return nil, u.ErrCast()
			}
Jeromy's avatar
Jeromy committed
154
			var pintype string
155 156
			rec, found, _ := res.Request().Option("recursive").Bool()
			if rec || !found {
Jeromy's avatar
Jeromy committed
157 158 159 160 161
				pintype = "recursively"
			} else {
				pintype = "directly"
			}

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

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

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

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

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

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

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

225 226
var listPinCmd = &cmds.Command{
	Helptext: cmds.HelpText{
rht's avatar
rht committed
227
		Tagline: "List objects pinned to local storage.",
228
		ShortDescription: `
229
Returns a list of objects that are pinned locally.
230 231 232
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.
233 234
`,
		LongDescription: `
235
Returns a list of objects that are pinned locally.
236 237 238
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
239

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

248 249 250
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.
251

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

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

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

289 290 291 292 293 294
		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
295 296
		}

297 298
		var keys map[string]RefKeyObject

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

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

Jeromy's avatar
Jeromy committed
319 320 321 322 323
			keys, ok := res.Output().(*RefKeyList)
			if !ok {
				return nil, u.ErrCast()
			}
			out := new(bytes.Buffer)
Jeromy's avatar
Jeromy committed
324 325 326 327 328
			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
329 330 331 332
				}
			}
			return out, nil
		},
333 334
	},
}
Jeromy's avatar
Jeromy committed
335

Jeromy's avatar
Jeromy committed
336 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
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
		},
	},
}

413
type RefKeyObject struct {
Jeromy's avatar
Jeromy committed
414
	Type string
415 416
}

Jeromy's avatar
Jeromy committed
417
type RefKeyList struct {
418
	Keys map[string]RefKeyObject
Jeromy's avatar
Jeromy committed
419
}
420 421

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

423 424
	mode, ok := pin.StringToPinMode(typeStr)
	if !ok {
Hector Sanjuan's avatar
Hector Sanjuan committed
425
		return nil, fmt.Errorf("invalid pin mode '%s'", typeStr)
426 427
	}

428 429 430
	keys := make(map[string]RefKeyObject)

	for _, p := range args {
Jeromy's avatar
Jeromy committed
431
		pth, err := path.ParsePath(p)
432 433 434 435
		if err != nil {
			return nil, err
		}

436
		c, err := core.ResolveToCid(ctx, n, pth)
437 438 439 440
		if err != nil {
			return nil, err
		}

Jeromy's avatar
Jeromy committed
441
		pinType, pinned, err := n.Pinning.IsPinnedWithType(c, mode)
442 443 444 445 446
		if err != nil {
			return nil, err
		}

		if !pinned {
Hector Sanjuan's avatar
Hector Sanjuan committed
447
			return nil, fmt.Errorf("path '%s' is not pinned", p)
448 449 450 451 452 453 454
		}

		switch pinType {
		case "direct", "indirect", "recursive", "internal":
		default:
			pinType = "indirect through " + pinType
		}
Jeromy's avatar
Jeromy committed
455
		keys[c.String()] = RefKeyObject{
456 457 458 459 460 461
			Type: pinType,
		}
	}

	return keys, nil
}
462 463 464 465 466

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

	keys := make(map[string]RefKeyObject)

Jeromy's avatar
Jeromy committed
467 468 469
	AddToResultKeys := func(keyList []*cid.Cid, typeStr string) {
		for _, c := range keyList {
			keys[c.String()] = RefKeyObject{
470 471 472 473 474 475 476 477 478
				Type: typeStr,
			}
		}
	}

	if typeStr == "direct" || typeStr == "all" {
		AddToResultKeys(n.Pinning.DirectKeys(), "direct")
	}
	if typeStr == "indirect" || typeStr == "all" {
Jeromy's avatar
Jeromy committed
479
		set := cid.NewSet()
480
		for _, k := range n.Pinning.RecursiveKeys() {
481
			err := dag.EnumerateChildren(n.Context(), n.DAG.GetLinks, k, set.Visit)
482 483 484 485
			if err != nil {
				return nil, err
			}
		}
Jeromy's avatar
Jeromy committed
486
		AddToResultKeys(set.Keys(), "indirect")
487 488 489 490 491 492 493
	}
	if typeStr == "recursive" || typeStr == "all" {
		AddToResultKeys(n.Pinning.RecursiveKeys(), "recursive")
	}

	return keys, nil
}
Jeromy's avatar
Jeromy committed
494 495 496 497 498 499 500 501

func cidsToStrings(cs []*cid.Cid) []string {
	out := make([]string, 0, len(cs))
	for _, c := range cs {
		out = append(out, c.String())
	}
	return out
}