pin.go 10.3 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
	cid "gx/ipfs/QmV5gPoRsjN1Gid3LMdNZTyfCtP2DsvqEbMAmz82RmmiGk/go-cid"
	u "gx/ipfs/QmZuY8aV7zbNXVy6DyN9SmnuH3o9nG852F4aTiSBpts8d1/go-ipfs-util"
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 27 28
	Subcommands: map[string]*cmds.Command{
		"add": addPinCmd,
		"rm":  rmPinCmd,
29
		"ls":  listPinCmd,
Brian Tiger Chow's avatar
Brian Tiger Chow committed
30 31 32
	},
}

Jeromy's avatar
Jeromy committed
33
type PinOutput struct {
Jeromy's avatar
Jeromy committed
34
	Pins []*cid.Cid
Jeromy's avatar
Jeromy committed
35 36
}

37 38 39 40 41
type AddPinOutput struct {
	Pins     []*cid.Cid
	Progress int `json:",omitempty"`
}

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

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

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

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

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
			}
			res.SetOutput(&AddPinOutput{Pins: added})
80 81 82
			return
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

296 297
		var keys map[string]RefKeyObject

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

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

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

335
type RefKeyObject struct {
Jeromy's avatar
Jeromy committed
336
	Type string
337 338
}

Jeromy's avatar
Jeromy committed
339
type RefKeyList struct {
340
	Keys map[string]RefKeyObject
Jeromy's avatar
Jeromy committed
341
}
342 343

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

345 346
	mode, ok := pin.StringToPinMode(typeStr)
	if !ok {
Hector Sanjuan's avatar
Hector Sanjuan committed
347
		return nil, fmt.Errorf("invalid pin mode '%s'", typeStr)
348 349
	}

350 351 352
	keys := make(map[string]RefKeyObject)

	for _, p := range args {
Jeromy's avatar
Jeromy committed
353
		pth, err := path.ParsePath(p)
354 355 356 357
		if err != nil {
			return nil, err
		}

358
		c, err := core.ResolveToCid(ctx, n, pth)
359 360 361 362
		if err != nil {
			return nil, err
		}

Jeromy's avatar
Jeromy committed
363
		pinType, pinned, err := n.Pinning.IsPinnedWithType(c, mode)
364 365 366 367 368
		if err != nil {
			return nil, err
		}

		if !pinned {
Hector Sanjuan's avatar
Hector Sanjuan committed
369
			return nil, fmt.Errorf("path '%s' is not pinned", p)
370 371 372 373 374 375 376
		}

		switch pinType {
		case "direct", "indirect", "recursive", "internal":
		default:
			pinType = "indirect through " + pinType
		}
Jeromy's avatar
Jeromy committed
377
		keys[c.String()] = RefKeyObject{
378 379 380 381 382 383
			Type: pinType,
		}
	}

	return keys, nil
}
384 385 386 387 388

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

	keys := make(map[string]RefKeyObject)

Jeromy's avatar
Jeromy committed
389 390 391
	AddToResultKeys := func(keyList []*cid.Cid, typeStr string) {
		for _, c := range keyList {
			keys[c.String()] = RefKeyObject{
392 393 394 395 396 397 398 399 400
				Type: typeStr,
			}
		}
	}

	if typeStr == "direct" || typeStr == "all" {
		AddToResultKeys(n.Pinning.DirectKeys(), "direct")
	}
	if typeStr == "indirect" || typeStr == "all" {
Jeromy's avatar
Jeromy committed
401
		set := cid.NewSet()
402
		for _, k := range n.Pinning.RecursiveKeys() {
403
			err := dag.EnumerateChildren(n.Context(), n.DAG.GetLinks, k, set.Visit)
404 405 406 407
			if err != nil {
				return nil, err
			}
		}
Jeromy's avatar
Jeromy committed
408
		AddToResultKeys(set.Keys(), "indirect")
409 410 411 412 413 414 415
	}
	if typeStr == "recursive" || typeStr == "all" {
		AddToResultKeys(n.Pinning.RecursiveKeys(), "recursive")
	}

	return keys, nil
}