files.go 29.2 KB
Newer Older
Jeromy's avatar
Jeromy committed
1 2 3
package commands

import (
Jeromy's avatar
Jeromy committed
4
	"context"
Jeromy's avatar
Jeromy committed
5 6 7 8 9
	"errors"
	"fmt"
	"io"
	"os"
	gopath "path"
Lucas Molas's avatar
Lucas Molas committed
10
	"sort"
Jeromy's avatar
Jeromy committed
11 12
	"strings"

13 14
	"github.com/ipfs/go-ipfs/core"
	"github.com/ipfs/go-ipfs/core/commands/cmdenv"
15

Jakub Sztandera's avatar
Jakub Sztandera committed
16 17 18 19
	"github.com/dustin/go-humanize"
	bservice "github.com/ipfs/go-blockservice"
	cid "github.com/ipfs/go-cid"
	cidenc "github.com/ipfs/go-cidutil/cidenc"
20 21
	cmds "github.com/ipfs/go-ipfs-cmds"
	offline "github.com/ipfs/go-ipfs-exchange-offline"
Jakub Sztandera's avatar
Jakub Sztandera committed
22 23 24 25 26
	ipld "github.com/ipfs/go-ipld-format"
	logging "github.com/ipfs/go-log"
	dag "github.com/ipfs/go-merkledag"
	"github.com/ipfs/go-mfs"
	ft "github.com/ipfs/go-unixfs"
27
	iface "github.com/ipfs/interface-go-ipfs-core"
28
	path "github.com/ipfs/interface-go-ipfs-core/path"
Jakub Sztandera's avatar
Jakub Sztandera committed
29
	mh "github.com/multiformats/go-multihash"
Jeromy's avatar
Jeromy committed
30 31
)

32
var flog = logging.Logger("cmds/files")
Jeromy's avatar
Jeromy committed
33

Michael Muré's avatar
Michael Muré committed
34
// FilesCmd is the 'ipfs files' command
Jeromy's avatar
Jeromy committed
35
var FilesCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
36
	Helptext: cmds.HelpText{
37
		Tagline: "Interact with unixfs files.",
Jeromy's avatar
Jeromy committed
38
		ShortDescription: `
@RubenKelevra's avatar
@RubenKelevra committed
39
Files is an API for manipulating IPFS objects as if they were a Unix
40
filesystem.
Jeromy's avatar
Jeromy committed
41

42
The files facility interacts with MFS (Mutable File System). MFS acts as a
@RubenKelevra's avatar
@RubenKelevra committed
43
single, dynamic filesystem mount. MFS has a root CID that is transparently
44 45 46 47
updated when a change happens (and can be checked with "ipfs files stat /").

All files and folders within MFS are respected and will not be cleaned up
during garbage collections. MFS is independent from the list of pinned items
48
("ipfs pin ls"). Calls to "ipfs pin add" and "ipfs pin rm" will add and remove
Steven Allen's avatar
Steven Allen committed
49
pins independently of MFS. If MFS content that was
50 51 52 53 54 55
additionally pinned is removed by calling "ipfs files rm", it will still
remain pinned.

Content added with "ipfs add" (which by default also becomes pinned), is not
added to MFS. Any content can be put into MFS with the command "ipfs files cp
/ipfs/<cid> /some/path/".
56 57


58
NOTE:
59 60 61 62 63
Most of the subcommands of 'ipfs files' accept the '--flush' flag. It defaults
to true. Use caution when setting this flag to false. It will improve
performance for large numbers of file operations, but it does so at the cost
of consistency guarantees. If the daemon is unexpectedly killed before running
'ipfs files flush' on the files in question, then data may be lost. This also
@RubenKelevra's avatar
@RubenKelevra committed
64
applies to run 'ipfs repo gc' concurrently with '--flush=false'
65
operations.
Jeromy's avatar
Jeromy committed
66 67
`,
	},
Steven Allen's avatar
Steven Allen committed
68 69
	Options: []cmds.Option{
		cmds.BoolOption(filesFlushOptionName, "f", "Flush target and ancestors after write.").WithDefault(true),
Jeromy's avatar
Jeromy committed
70
	},
Jeromy's avatar
Jeromy committed
71
	Subcommands: map[string]*cmds.Command{
Overbool's avatar
Overbool committed
72
		"read":  filesReadCmd,
73
		"write": filesWriteCmd,
Overbool's avatar
Overbool committed
74 75 76 77
		"mv":    filesMvCmd,
		"cp":    filesCpCmd,
		"ls":    filesLsCmd,
		"mkdir": filesMkdirCmd,
78
		"stat":  filesStatCmd,
Overbool's avatar
Overbool committed
79 80 81
		"rm":    filesRmCmd,
		"flush": filesFlushCmd,
		"chcid": filesChcidCmd,
Jeromy's avatar
Jeromy committed
82 83 84
	},
}

Kejie Zhang's avatar
Kejie Zhang committed
85 86 87 88 89
const (
	filesCidVersionOptionName = "cid-version"
	filesHashOptionName       = "hash"
)

Steven Allen's avatar
Steven Allen committed
90 91
var cidVersionOption = cmds.IntOption(filesCidVersionOptionName, "cid-ver", "Cid version to use. (experimental)")
var hashOption = cmds.StringOption(filesHashOptionName, "Hash function to use. Will set Cid version to 1 if used. (experimental)")
92

Michael Muré's avatar
Michael Muré committed
93
var errFormat = errors.New("format was set by multiple options. Only one format option is allowed")
94

Michael Muré's avatar
Michael Muré committed
95
type statOutput struct {
96 97 98 99 100
	Hash           string
	Size           uint64
	CumulativeSize uint64
	Blocks         int
	Type           string
Michael Muré's avatar
Michael Muré committed
101
	WithLocality   bool   `json:",omitempty"`
102 103 104 105
	Local          bool   `json:",omitempty"`
	SizeLocal      uint64 `json:",omitempty"`
}

Kejie Zhang's avatar
Kejie Zhang committed
106 107
const (
	defaultStatFormat = `<hash>
keks's avatar
keks committed
108 109 110 111
Size: <size>
CumulativeSize: <cumulsize>
ChildBlocks: <childs>
Type: <type>`
Kejie Zhang's avatar
Kejie Zhang committed
112 113 114 115
	filesFormatOptionName    = "format"
	filesSizeOptionName      = "size"
	filesWithLocalOptionName = "with-local"
)
keks's avatar
keks committed
116

117
var filesStatCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
118
	Helptext: cmds.HelpText{
rht's avatar
rht committed
119
		Tagline: "Display file status.",
Jeromy's avatar
Jeromy committed
120 121
	},

Steven Allen's avatar
Steven Allen committed
122 123
	Arguments: []cmds.Argument{
		cmds.StringArg("path", true, false, "Path to node to stat."),
Jeromy's avatar
Jeromy committed
124
	},
Steven Allen's avatar
Steven Allen committed
125 126
	Options: []cmds.Option{
		cmds.StringOption(filesFormatOptionName, "Print statistics in given format. Allowed tokens: "+
keks's avatar
keks committed
127
			"<hash> <size> <cumulsize> <type> <childs>. Conflicts with other format options.").WithDefault(defaultStatFormat),
Steven Allen's avatar
Steven Allen committed
128 129 130
		cmds.BoolOption(filesHashOptionName, "Print only hash. Implies '--format=<hash>'. Conflicts with other format options."),
		cmds.BoolOption(filesSizeOptionName, "Print only size. Implies '--format=<cumulsize>'. Conflicts with other format options."),
		cmds.BoolOption(filesWithLocalOptionName, "Compute the amount of the dag that is local, and if possible the total size"),
131
	},
keks's avatar
keks committed
132
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
133 134 135

		_, err := statGetFormatOptions(req)
		if err != nil {
Steven Allen's avatar
Steven Allen committed
136
			return cmds.Errorf(cmds.ErrClient, err.Error())
137 138
		}

139
		node, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
140
		if err != nil {
keks's avatar
keks committed
141
			return err
Jeromy's avatar
Jeromy committed
142 143
		}

144
		api, err := cmdenv.GetApi(env, req)
145 146 147 148
		if err != nil {
			return err
		}

149
		path, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
150
		if err != nil {
keks's avatar
keks committed
151
			return err
Jeromy's avatar
Jeromy committed
152 153
		}

Kejie Zhang's avatar
Kejie Zhang committed
154
		withLocal, _ := req.Options[filesWithLocalOptionName].(bool)
155

156 157 158 159 160
		enc, err := cmdenv.GetCidEncoder(req)
		if err != nil {
			return err
		}

161 162 163 164 165 166 167 168 169 170 171
		var dagserv ipld.DAGService
		if withLocal {
			// an offline DAGService will not fetch from the network
			dagserv = dag.NewDAGService(bservice.New(
				node.Blockstore,
				offline.Exchange(node.Blockstore),
			))
		} else {
			dagserv = node.DAG
		}

172
		nd, err := getNodeFromPath(req.Context, node, api, path)
Jeromy's avatar
Jeromy committed
173
		if err != nil {
keks's avatar
keks committed
174
			return err
Jeromy's avatar
Jeromy committed
175 176
		}

177
		o, err := statNode(nd, enc)
Jeromy's avatar
Jeromy committed
178
		if err != nil {
keks's avatar
keks committed
179
			return err
Jeromy's avatar
Jeromy committed
180 181
		}

182
		if !withLocal {
keks's avatar
keks committed
183
			return cmds.EmitOnce(res, o)
184 185 186
		}

		local, sizeLocal, err := walkBlock(req.Context, dagserv, nd)
187 188 189
		if err != nil {
			return err
		}
190 191 192 193 194

		o.WithLocality = true
		o.Local = local
		o.SizeLocal = sizeLocal

keks's avatar
keks committed
195
		return cmds.EmitOnce(res, o)
Jeromy's avatar
Jeromy committed
196
	},
197
	Encoders: cmds.EncoderMap{
198
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *statOutput) error {
199
			s, _ := statGetFormatOptions(req)
200 201 202 203 204 205
			s = strings.Replace(s, "<hash>", out.Hash, -1)
			s = strings.Replace(s, "<size>", fmt.Sprintf("%d", out.Size), -1)
			s = strings.Replace(s, "<cumulsize>", fmt.Sprintf("%d", out.CumulativeSize), -1)
			s = strings.Replace(s, "<childs>", fmt.Sprintf("%d", out.Blocks), -1)
			s = strings.Replace(s, "<type>", out.Type, -1)

206 207 208 209 210 211 212 213 214 215 216
			fmt.Fprintln(w, s)

			if out.WithLocality {
				fmt.Fprintf(w, "Local: %s of %s (%.2f%%)\n",
					humanize.Bytes(out.SizeLocal),
					humanize.Bytes(out.CumulativeSize),
					100.0*float64(out.SizeLocal)/float64(out.CumulativeSize),
				)
			}

			return nil
217
		}),
Jeromy's avatar
Jeromy committed
218
	},
Michael Muré's avatar
Michael Muré committed
219
	Type: statOutput{},
Jeromy's avatar
Jeromy committed
220 221
}

222 223 224 225
func moreThanOne(a, b, c bool) bool {
	return a && b || b && c || a && c
}

226
func statGetFormatOptions(req *cmds.Request) (string, error) {
227

Kejie Zhang's avatar
Kejie Zhang committed
228 229 230
	hash, _ := req.Options[filesHashOptionName].(bool)
	size, _ := req.Options[filesSizeOptionName].(bool)
	format, _ := req.Options[filesFormatOptionName].(string)
231

keks's avatar
keks committed
232
	if moreThanOne(hash, size, format != defaultStatFormat) {
Michael Muré's avatar
Michael Muré committed
233
		return "", errFormat
234 235 236
	}

	if hash {
237 238 239 240 241
		return "<hash>", nil
	} else if size {
		return "<cumulsize>", nil
	} else {
		return format, nil
242 243 244
	}
}

245
func statNode(nd ipld.Node, enc cidenc.Encoder) (*statOutput, error) {
Jeromy's avatar
Jeromy committed
246
	c := nd.Cid()
Jeromy's avatar
Jeromy committed
247 248 249 250 251 252

	cumulsize, err := nd.Size()
	if err != nil {
		return nil, err
	}

253 254
	switch n := nd.(type) {
	case *dag.ProtoNode:
Overbool's avatar
Overbool committed
255
		d, err := ft.FSNodeFromBytes(n.Data())
256 257 258 259 260
		if err != nil {
			return nil, err
		}

		var ndtype string
Overbool's avatar
Overbool committed
261
		switch d.Type() {
262
		case ft.TDirectory, ft.THAMTShard:
263
			ndtype = "directory"
264
		case ft.TFile, ft.TMetadata, ft.TRaw:
265 266
			ndtype = "file"
		default:
Overbool's avatar
Overbool committed
267
			return nil, fmt.Errorf("unrecognized node type: %s", d.Type())
268 269
		}

Michael Muré's avatar
Michael Muré committed
270
		return &statOutput{
271
			Hash:           enc.Encode(c),
272
			Blocks:         len(nd.Links()),
Overbool's avatar
Overbool committed
273
			Size:           d.FileSize(),
274 275 276 277
			CumulativeSize: cumulsize,
			Type:           ndtype,
		}, nil
	case *dag.RawNode:
Michael Muré's avatar
Michael Muré committed
278
		return &statOutput{
279
			Hash:           enc.Encode(c),
280 281 282 283 284
			Blocks:         0,
			Size:           cumulsize,
			CumulativeSize: cumulsize,
			Type:           "file",
		}, nil
Jeromy's avatar
Jeromy committed
285
	default:
286
		return nil, fmt.Errorf("not unixfs node (proto or raw)")
Jeromy's avatar
Jeromy committed
287
	}
Jeromy's avatar
Jeromy committed
288 289
}

290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
func walkBlock(ctx context.Context, dagserv ipld.DAGService, nd ipld.Node) (bool, uint64, error) {
	// Start with the block data size
	sizeLocal := uint64(len(nd.RawData()))

	local := true

	for _, link := range nd.Links() {
		child, err := dagserv.Get(ctx, link.Cid)

		if err == ipld.ErrNotFound {
			local = false
			continue
		}

		if err != nil {
			return local, sizeLocal, err
		}

		childLocal, childLocalSize, err := walkBlock(ctx, dagserv, child)

		if err != nil {
			return local, sizeLocal, err
		}

		// Recursively add the child size
		local = local && childLocal
		sizeLocal += childLocalSize
	}

	return local, sizeLocal, nil
}

Overbool's avatar
Overbool committed
322
var filesCpCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
323
	Helptext: cmds.HelpText{
324
		Tagline: "Copy any IPFS files and directories into MFS (or copy within MFS).",
325
		ShortDescription: `
326 327
"ipfs files cp" can be used to copy any IPFS file or directory (usually in the
form /ipfs/<CID>, but also any resolvable path), into the Mutable File System
328 329 330 331 332 333 334 335
(MFS).

It can also be used to copy files within MFS, but in the case when an
IPFS-path matches an existing MFS path, the IPFS path wins.

In order to add content to MFS from disk, you can use "ipfs add" to obtain the
IPFS Content Identifier and then "ipfs files cp" to copy it into MFS:

336
$ ipfs add --quieter --pin=false <your file>
337 338 339 340
# ...
# ... outputs the root CID at the end
$ ipfs cp /ipfs/<CID> /your/desired/mfs/path
`,
Jeromy's avatar
Jeromy committed
341
	},
Steven Allen's avatar
Steven Allen committed
342
	Arguments: []cmds.Argument{
343 344
		cmds.StringArg("source", true, false, "Source IPFS or MFS path to copy."),
		cmds.StringArg("dest", true, false, "Destination within MFS."),
Jeromy's avatar
Jeromy committed
345
	},
Overbool's avatar
Overbool committed
346 347
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		nd, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
348
		if err != nil {
Overbool's avatar
Overbool committed
349
			return err
Jeromy's avatar
Jeromy committed
350 351
		}

352
		api, err := cmdenv.GetApi(env, req)
353
		if err != nil {
Overbool's avatar
Overbool committed
354
			return err
355 356
		}

357
		flush, _ := req.Options[filesFlushOptionName].(bool)
358

Overbool's avatar
Overbool committed
359
		src, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
360
		if err != nil {
Overbool's avatar
Overbool committed
361
			return err
Jeromy's avatar
Jeromy committed
362
		}
363 364
		src = strings.TrimRight(src, "/")

Overbool's avatar
Overbool committed
365
		dst, err := checkPath(req.Arguments[1])
Jeromy's avatar
Jeromy committed
366
		if err != nil {
Overbool's avatar
Overbool committed
367
			return err
Jeromy's avatar
Jeromy committed
368
		}
Jeromy's avatar
Jeromy committed
369

370 371 372 373
		if dst[len(dst)-1] == '/' {
			dst += gopath.Base(src)
		}

Overbool's avatar
Overbool committed
374
		node, err := getNodeFromPath(req.Context, nd, api, src)
Jeromy's avatar
Jeromy committed
375
		if err != nil {
Overbool's avatar
Overbool committed
376
			return fmt.Errorf("cp: cannot get node from path %s: %s", src, err)
Jeromy's avatar
Jeromy committed
377 378
		}

Overbool's avatar
Overbool committed
379
		err = mfs.PutNode(nd.FilesRoot, dst, node)
Jeromy's avatar
Jeromy committed
380
		if err != nil {
Overbool's avatar
Overbool committed
381
			return fmt.Errorf("cp: cannot put node in path %s: %s", dst, err)
Jeromy's avatar
Jeromy committed
382
		}
383 384

		if flush {
385
			_, err := mfs.FlushPath(req.Context, nd.FilesRoot, dst)
386
			if err != nil {
Overbool's avatar
Overbool committed
387
				return fmt.Errorf("cp: cannot flush the created file %s: %s", dst, err)
388 389
			}
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
390

Overbool's avatar
Overbool committed
391
		return nil
Jeromy's avatar
Jeromy committed
392 393 394
	},
}

395
func getNodeFromPath(ctx context.Context, node *core.IpfsNode, api iface.CoreAPI, p string) (ipld.Node, error) {
Jeromy's avatar
Jeromy committed
396 397
	switch {
	case strings.HasPrefix(p, "/ipfs/"):
398
		return api.ResolveNode(ctx, path.New(p))
Jeromy's avatar
Jeromy committed
399 400 401 402 403 404 405 406 407 408
	default:
		fsn, err := mfs.Lookup(node.FilesRoot, p)
		if err != nil {
			return nil, err
		}

		return fsn.GetNode()
	}
}

Michael Muré's avatar
Michael Muré committed
409
type filesLsOutput struct {
Jeromy's avatar
Jeromy committed
410 411 412
	Entries []mfs.NodeListing
}

Kejie Zhang's avatar
Kejie Zhang committed
413
const (
414
	longOptionName     = "long"
Kejie Zhang's avatar
Kejie Zhang committed
415 416 417
	dontSortOptionName = "U"
)

Overbool's avatar
Overbool committed
418
var filesLsCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
419
	Helptext: cmds.HelpText{
Chaitanya's avatar
Chaitanya committed
420
		Tagline: "List directories in the local mutable namespace.",
Jeromy's avatar
Jeromy committed
421
		ShortDescription: `
Chaitanya's avatar
Chaitanya committed
422
List directories in the local mutable namespace (works on both IPFS and MFS paths).
Jeromy's avatar
Jeromy committed
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438

Examples:

    $ ipfs files ls /welcome/docs/
    about
    contact
    help
    quick-start
    readme
    security-notes

    $ ipfs files ls /myfiles/a/b/c/d
    foo
    bar
`,
	},
Steven Allen's avatar
Steven Allen committed
439 440
	Arguments: []cmds.Argument{
		cmds.StringArg("path", false, false, "Path to show listing for. Defaults to '/'."),
Jeromy's avatar
Jeromy committed
441
	},
Steven Allen's avatar
Steven Allen committed
442
	Options: []cmds.Option{
443
		cmds.BoolOption(longOptionName, "l", "Use long listing format."),
Steven Allen's avatar
Steven Allen committed
444
		cmds.BoolOption(dontSortOptionName, "Do not sort; list entries in directory order."),
Jeromy's avatar
Jeromy committed
445
	},
Overbool's avatar
Overbool committed
446
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
447 448
		var arg string

Overbool's avatar
Overbool committed
449
		if len(req.Arguments) == 0 {
450 451
			arg = "/"
		} else {
Overbool's avatar
Overbool committed
452
			arg = req.Arguments[0]
453 454 455
		}

		path, err := checkPath(arg)
Jeromy's avatar
Jeromy committed
456
		if err != nil {
Overbool's avatar
Overbool committed
457
			return err
Jeromy's avatar
Jeromy committed
458 459
		}

Overbool's avatar
Overbool committed
460
		nd, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
461
		if err != nil {
Overbool's avatar
Overbool committed
462
			return err
Jeromy's avatar
Jeromy committed
463 464 465 466
		}

		fsn, err := mfs.Lookup(nd.FilesRoot, path)
		if err != nil {
Overbool's avatar
Overbool committed
467
			return err
Jeromy's avatar
Jeromy committed
468 469
		}

Overbool's avatar
Overbool committed
470
		long, _ := req.Options[longOptionName].(bool)
Jeromy's avatar
Jeromy committed
471

472 473 474 475 476
		enc, err := cmdenv.GetCidEncoder(req)
		if err != nil {
			return err
		}

Jeromy's avatar
Jeromy committed
477 478
		switch fsn := fsn.(type) {
		case *mfs.Directory:
Jeromy's avatar
Jeromy committed
479 480
			if !long {
				var output []mfs.NodeListing
Overbool's avatar
Overbool committed
481
				names, err := fsn.ListNames(req.Context)
482
				if err != nil {
Overbool's avatar
Overbool committed
483
					return err
484 485 486
				}

				for _, name := range names {
Jeromy's avatar
Jeromy committed
487
					output = append(output, mfs.NodeListing{
Jeromy's avatar
Jeromy committed
488
						Name: name,
Jeromy's avatar
Jeromy committed
489 490
					})
				}
491
				return cmds.EmitOnce(res, &filesLsOutput{output})
Jeromy's avatar
Jeromy committed
492
			}
493 494 495 496
			listing, err := fsn.List(req.Context)
			if err != nil {
				return err
			}
497
			return cmds.EmitOnce(res, &filesLsOutput{listing})
Jeromy's avatar
Jeromy committed
498
		case *mfs.File:
rht's avatar
rht committed
499
			_, name := gopath.Split(path)
Overbool's avatar
Overbool committed
500
			out := &filesLsOutput{[]mfs.NodeListing{{Name: name}}}
501 502 503 504 505
			if long {
				out.Entries[0].Type = int(fsn.Type())

				size, err := fsn.Size()
				if err != nil {
Overbool's avatar
Overbool committed
506
					return err
507 508 509 510 511
				}
				out.Entries[0].Size = size

				nd, err := fsn.GetNode()
				if err != nil {
Overbool's avatar
Overbool committed
512
					return err
513
				}
514
				out.Entries[0].Hash = enc.Encode(nd.Cid())
515
			}
516
			return cmds.EmitOnce(res, out)
Jeromy's avatar
Jeromy committed
517
		default:
Overbool's avatar
Overbool committed
518
			return errors.New("unrecognized type")
Jeromy's avatar
Jeromy committed
519 520
		}
	},
Overbool's avatar
Overbool committed
521 522 523
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *filesLsOutput) error {
			noSort, _ := req.Options[dontSortOptionName].(bool)
Lucas Molas's avatar
Lucas Molas committed
524 525 526 527 528 529
			if !noSort {
				sort.Slice(out.Entries, func(i, j int) bool {
					return strings.Compare(out.Entries[i].Name, out.Entries[j].Name) < 0
				})
			}

Overbool's avatar
Overbool committed
530
			long, _ := req.Options[longOptionName].(bool)
Jeromy's avatar
Jeromy committed
531 532
			for _, o := range out.Entries {
				if long {
Overbool's avatar
Overbool committed
533 534 535
					if o.Type == int(mfs.TDir) {
						o.Name += "/"
					}
Overbool's avatar
Overbool committed
536
					fmt.Fprintf(w, "%s\t%s\t%d\n", o.Name, o.Hash, o.Size)
Jeromy's avatar
Jeromy committed
537
				} else {
Overbool's avatar
Overbool committed
538
					fmt.Fprintf(w, "%s\n", o.Name)
Jeromy's avatar
Jeromy committed
539 540
				}
			}
Overbool's avatar
Overbool committed
541 542 543

			return nil
		}),
Jeromy's avatar
Jeromy committed
544
	},
Michael Muré's avatar
Michael Muré committed
545
	Type: filesLsOutput{},
Jeromy's avatar
Jeromy committed
546 547
}

Kejie Zhang's avatar
Kejie Zhang committed
548 549 550 551 552
const (
	filesOffsetOptionName = "offset"
	filesCountOptionName  = "count"
)

Overbool's avatar
Overbool committed
553
var filesReadCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
554
	Helptext: cmds.HelpText{
@RubenKelevra's avatar
@RubenKelevra committed
555
		Tagline: "Read a file in a given MFS.",
Jeromy's avatar
Jeromy committed
556
		ShortDescription: `
557
Read a specified number of bytes from a file at a given offset. By default,
@RubenKelevra's avatar
@RubenKelevra committed
558
it will read the entire file similar to the Unix cat.
Jeromy's avatar
Jeromy committed
559 560 561 562 563

Examples:

    $ ipfs files read /test/hello
    hello
Jeromy's avatar
Jeromy committed
564
        `,
Jeromy's avatar
Jeromy committed
565 566
	},

Steven Allen's avatar
Steven Allen committed
567 568
	Arguments: []cmds.Argument{
		cmds.StringArg("path", true, false, "Path to file to be read."),
Jeromy's avatar
Jeromy committed
569
	},
Steven Allen's avatar
Steven Allen committed
570 571 572
	Options: []cmds.Option{
		cmds.Int64Option(filesOffsetOptionName, "o", "Byte offset to begin reading from."),
		cmds.Int64Option(filesCountOptionName, "n", "Maximum number of bytes to read."),
Jeromy's avatar
Jeromy committed
573
	},
Overbool's avatar
Overbool committed
574 575
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		nd, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
576
		if err != nil {
Overbool's avatar
Overbool committed
577
			return err
Jeromy's avatar
Jeromy committed
578 579
		}

Overbool's avatar
Overbool committed
580
		path, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
581
		if err != nil {
Overbool's avatar
Overbool committed
582
			return err
Jeromy's avatar
Jeromy committed
583 584
		}

Overbool's avatar
Overbool committed
585
		fsn, err := mfs.Lookup(nd.FilesRoot, path)
Jeromy's avatar
Jeromy committed
586
		if err != nil {
Overbool's avatar
Overbool committed
587
			return err
Jeromy's avatar
Jeromy committed
588 589 590 591
		}

		fi, ok := fsn.(*mfs.File)
		if !ok {
Overbool's avatar
Overbool committed
592
			return fmt.Errorf("%s was not a file", path)
Jeromy's avatar
Jeromy committed
593 594
		}

Steven Allen's avatar
Steven Allen committed
595
		rfd, err := fi.Open(mfs.Flags{Read: true})
596
		if err != nil {
Overbool's avatar
Overbool committed
597
			return err
598 599 600 601
		}

		defer rfd.Close()

Overbool's avatar
Overbool committed
602
		offset, _ := req.Options[offsetOptionName].(int64)
Jeromy's avatar
Jeromy committed
603
		if offset < 0 {
Overbool's avatar
Overbool committed
604
			return fmt.Errorf("cannot specify negative offset")
Jeromy's avatar
Jeromy committed
605 606
		}

607
		filen, err := rfd.Size()
Jeromy's avatar
Jeromy committed
608
		if err != nil {
Overbool's avatar
Overbool committed
609
			return err
Jeromy's avatar
Jeromy committed
610 611 612
		}

		if int64(offset) > filen {
Overbool's avatar
Overbool committed
613
			return fmt.Errorf("offset was past end of file (%d > %d)", offset, filen)
Jeromy's avatar
Jeromy committed
614
		}
Jeromy's avatar
Jeromy committed
615

616
		_, err = rfd.Seek(int64(offset), io.SeekStart)
Jeromy's avatar
Jeromy committed
617
		if err != nil {
Overbool's avatar
Overbool committed
618
			return err
Jeromy's avatar
Jeromy committed
619
		}
620

Overbool's avatar
Overbool committed
621 622
		var r io.Reader = &contextReaderWrapper{R: rfd, ctx: req.Context}
		count, found := req.Options[filesCountOptionName].(int64)
Jeromy's avatar
Jeromy committed
623 624
		if found {
			if count < 0 {
Overbool's avatar
Overbool committed
625
				return fmt.Errorf("cannot specify negative 'count'")
Jeromy's avatar
Jeromy committed
626
			}
627
			r = io.LimitReader(r, int64(count))
Jeromy's avatar
Jeromy committed
628
		}
Overbool's avatar
Overbool committed
629
		return res.Emit(r)
Jeromy's avatar
Jeromy committed
630 631 632
	},
}

Jeromy's avatar
Jeromy committed
633 634 635 636 637 638 639 640 641 642 643 644 645
type contextReader interface {
	CtxReadFull(context.Context, []byte) (int, error)
}

type contextReaderWrapper struct {
	R   contextReader
	ctx context.Context
}

func (crw *contextReaderWrapper) Read(b []byte) (int, error) {
	return crw.R.CtxReadFull(crw.ctx, b)
}

Overbool's avatar
Overbool committed
646
var filesMvCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
647
	Helptext: cmds.HelpText{
rht's avatar
rht committed
648
		Tagline: "Move files.",
Jeromy's avatar
Jeromy committed
649
		ShortDescription: `
@RubenKelevra's avatar
@RubenKelevra committed
650
Move files around. Just like the traditional Unix mv.
Jeromy's avatar
Jeromy committed
651 652 653 654 655

Example:

    $ ipfs files mv /myfs/a/b/c /myfs/foo/newc

Jeromy's avatar
Jeromy committed
656
`,
Jeromy's avatar
Jeromy committed
657 658
	},

Steven Allen's avatar
Steven Allen committed
659 660 661
	Arguments: []cmds.Argument{
		cmds.StringArg("source", true, false, "Source file to move."),
		cmds.StringArg("dest", true, false, "Destination path for file to be moved to."),
Jeromy's avatar
Jeromy committed
662
	},
Overbool's avatar
Overbool committed
663 664
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		nd, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
665
		if err != nil {
Overbool's avatar
Overbool committed
666
			return err
Jeromy's avatar
Jeromy committed
667 668
		}

669 670
		flush, _ := req.Options[filesFlushOptionName].(bool)

Overbool's avatar
Overbool committed
671
		src, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
672
		if err != nil {
Overbool's avatar
Overbool committed
673
			return err
Jeromy's avatar
Jeromy committed
674
		}
Overbool's avatar
Overbool committed
675
		dst, err := checkPath(req.Arguments[1])
Jeromy's avatar
Jeromy committed
676
		if err != nil {
Overbool's avatar
Overbool committed
677
			return err
Jeromy's avatar
Jeromy committed
678
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
679

680 681
		err = mfs.Mv(nd.FilesRoot, src, dst)
		if err == nil && flush {
682
			_, err = mfs.FlushPath(req.Context, nd.FilesRoot, "/")
683 684
		}
		return err
Jeromy's avatar
Jeromy committed
685 686 687
	},
}

Kejie Zhang's avatar
Kejie Zhang committed
688 689 690 691 692 693 694 695
const (
	filesCreateOptionName    = "create"
	filesParentsOptionName   = "parents"
	filesTruncateOptionName  = "truncate"
	filesRawLeavesOptionName = "raw-leaves"
	filesFlushOptionName     = "flush"
)

696
var filesWriteCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
697
	Helptext: cmds.HelpText{
rht's avatar
rht committed
698
		Tagline: "Write to a mutable file in a given filesystem.",
Jeromy's avatar
Jeromy committed
699 700
		ShortDescription: `
Write data to a file in a given filesystem. This command allows you to specify
701 702
a beginning offset to write to. The entire length of the input will be
written.
Jeromy's avatar
Jeromy committed
703

Jeromy's avatar
Jeromy committed
704
If the '--create' option is specified, the file will be created if it does not
@RubenKelevra's avatar
@RubenKelevra committed
705
exist. Nonexistent intermediate directories will not be created unless the
Alan Shaw's avatar
Alan Shaw committed
706
'--parents' option is specified.
Jeromy's avatar
Jeromy committed
707

708
Newly created files will have the same CID version and hash function of the
Alan Shaw's avatar
Alan Shaw committed
709
parent directory unless the '--cid-version' and '--hash' options are used.
710 711

Newly created leaves will be in the legacy format (Protobuf) if the
Alan Shaw's avatar
Alan Shaw committed
712
CID version is 0, or raw if the CID version is non-zero.  Use of the
Alan Shaw's avatar
Alan Shaw committed
713
'--raw-leaves' option will override this behavior.
714

@RubenKelevra's avatar
@RubenKelevra committed
715
If the '--flush' option is set to false, changes will not be propagated to the
716 717 718
merkledag root. This can make operations much faster when doing a large number
of writes to a deeper directory structure.

719
EXAMPLE:
Jeromy's avatar
Jeromy committed
720

Alan Shaw's avatar
Alan Shaw committed
721
    echo "hello world" | ipfs files write --create --parents /myfs/a/b/file
Jeromy's avatar
Jeromy committed
722
    echo "hello world" | ipfs files write --truncate /myfs/a/b/file
723

724
WARNING:
725

726 727 728
Usage of the '--flush=false' option does not guarantee data durability until
the tree has been flushed. This can be accomplished by running 'ipfs files
stat' on the file or any of its ancestors.
Jeromy's avatar
Jeromy committed
729
`,
Jeromy's avatar
Jeromy committed
730
	},
Steven Allen's avatar
Steven Allen committed
731 732 733 734 735 736 737 738 739 740 741
	Arguments: []cmds.Argument{
		cmds.StringArg("path", true, false, "Path to write to."),
		cmds.FileArg("data", true, false, "Data to write.").EnableStdin(),
	},
	Options: []cmds.Option{
		cmds.Int64Option(filesOffsetOptionName, "o", "Byte offset to begin writing at."),
		cmds.BoolOption(filesCreateOptionName, "e", "Create the file if it does not exist."),
		cmds.BoolOption(filesParentsOptionName, "p", "Make parent directories as needed."),
		cmds.BoolOption(filesTruncateOptionName, "t", "Truncate the file to size zero before writing."),
		cmds.Int64Option(filesCountOptionName, "n", "Maximum number of bytes to read."),
		cmds.BoolOption(filesRawLeavesOptionName, "Use raw blocks for newly created leaf nodes. (experimental)"),
742 743
		cidVersionOption,
		hashOption,
Jeromy's avatar
Jeromy committed
744
	},
keks's avatar
keks committed
745
	Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) (retErr error) {
746
		path, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
747
		if err != nil {
keks's avatar
keks committed
748
			return err
Jeromy's avatar
Jeromy committed
749 750
		}

Kejie Zhang's avatar
Kejie Zhang committed
751 752 753 754 755
		create, _ := req.Options[filesCreateOptionName].(bool)
		mkParents, _ := req.Options[filesParentsOptionName].(bool)
		trunc, _ := req.Options[filesTruncateOptionName].(bool)
		flush, _ := req.Options[filesFlushOptionName].(bool)
		rawLeaves, rawLeavesDef := req.Options[filesRawLeavesOptionName].(bool)
756

757
		prefix, err := getPrefixNew(req)
758
		if err != nil {
keks's avatar
keks committed
759
			return err
760
		}
Jeromy's avatar
Jeromy committed
761

762
		nd, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
763
		if err != nil {
keks's avatar
keks committed
764
			return err
Jeromy's avatar
Jeromy committed
765 766
		}

767
		offset, _ := req.Options[filesOffsetOptionName].(int64)
Jeromy's avatar
Jeromy committed
768
		if offset < 0 {
keks's avatar
keks committed
769
			return fmt.Errorf("cannot have negative write offset")
Jeromy's avatar
Jeromy committed
770 771
		}

772 773 774
		if mkParents {
			err := ensureContainingDirectoryExists(nd.FilesRoot, path, prefix)
			if err != nil {
keks's avatar
keks committed
775
				return err
776 777 778
			}
		}

779
		fi, err := getFileHandle(nd.FilesRoot, path, create, prefix)
Jeromy's avatar
Jeromy committed
780
		if err != nil {
keks's avatar
keks committed
781
			return err
Jeromy's avatar
Jeromy committed
782
		}
783 784 785
		if rawLeavesDef {
			fi.RawLeaves = rawLeaves
		}
786

Steven Allen's avatar
Steven Allen committed
787
		wfd, err := fi.Open(mfs.Flags{Write: true, Sync: flush})
788
		if err != nil {
keks's avatar
keks committed
789
			return err
790
		}
Jeromy's avatar
Jeromy committed
791

792 793 794
		defer func() {
			err := wfd.Close()
			if err != nil {
keks's avatar
keks committed
795 796 797
				if retErr == nil {
					retErr = err
				} else {
798
					flog.Error("files: error closing file mfs file descriptor", err)
keks's avatar
keks committed
799
				}
800 801
			}
		}()
802

Jeromy's avatar
Jeromy committed
803
		if trunc {
804
			if err := wfd.Truncate(0); err != nil {
keks's avatar
keks committed
805
				return err
Jeromy's avatar
Jeromy committed
806 807 808
			}
		}

809
		count, countfound := req.Options[filesCountOptionName].(int64)
Jeromy's avatar
Jeromy committed
810
		if countfound && count < 0 {
keks's avatar
keks committed
811
			return fmt.Errorf("cannot have negative byte count")
Jeromy's avatar
Jeromy committed
812
		}
Jeromy's avatar
Jeromy committed
813

814
		_, err = wfd.Seek(int64(offset), io.SeekStart)
Jeromy's avatar
Jeromy committed
815
		if err != nil {
816
			flog.Error("seekfail: ", err)
keks's avatar
keks committed
817
			return err
Jeromy's avatar
Jeromy committed
818 819
		}

820 821 822 823
		var r io.Reader
		r, err = cmdenv.GetFileArg(req.Files.Entries())
		if err != nil {
			return err
Jeromy's avatar
Jeromy committed
824
		}
zramsay's avatar
zramsay committed
825 826 827 828
		if countfound {
			r = io.LimitReader(r, int64(count))
		}

Jan Winkelmann's avatar
Jan Winkelmann committed
829
		_, err = io.Copy(wfd, r)
keks's avatar
keks committed
830
		return err
Jeromy's avatar
Jeromy committed
831 832 833
	},
}

Overbool's avatar
Overbool committed
834
var filesMkdirCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
835
	Helptext: cmds.HelpText{
rht's avatar
rht committed
836
		Tagline: "Make directories.",
Jeromy's avatar
Jeromy committed
837 838 839
		ShortDescription: `
Create the directory if it does not already exist.

840 841 842
The directory will have the same CID version and hash function of the
parent directory unless the --cid-version and --hash options are used.

843
NOTE: All paths must be absolute.
Jeromy's avatar
Jeromy committed
844 845 846

Examples:

847 848
    $ ipfs files mkdir /test/newdir
    $ ipfs files mkdir -p /test/does/not/exist/yet
Jeromy's avatar
Jeromy committed
849 850 851
`,
	},

Steven Allen's avatar
Steven Allen committed
852 853
	Arguments: []cmds.Argument{
		cmds.StringArg("path", true, false, "Path to dir to make."),
Jeromy's avatar
Jeromy committed
854
	},
Steven Allen's avatar
Steven Allen committed
855 856
	Options: []cmds.Option{
		cmds.BoolOption(filesParentsOptionName, "p", "No error if existing, make parent directories as needed."),
857 858
		cidVersionOption,
		hashOption,
Jeromy's avatar
Jeromy committed
859
	},
Overbool's avatar
Overbool committed
860 861
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		n, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
862
		if err != nil {
Overbool's avatar
Overbool committed
863
			return err
Jeromy's avatar
Jeromy committed
864 865
		}

Overbool's avatar
Overbool committed
866 867
		dashp, _ := req.Options[filesParentsOptionName].(bool)
		dirtomake, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
868
		if err != nil {
Overbool's avatar
Overbool committed
869
			return err
Jeromy's avatar
Jeromy committed
870 871
		}

Overbool's avatar
Overbool committed
872
		flush, _ := req.Options[filesFlushOptionName].(bool)
Jeromy's avatar
Jeromy committed
873

874 875
		prefix, err := getPrefix(req)
		if err != nil {
Overbool's avatar
Overbool committed
876
			return err
877 878 879
		}
		root := n.FilesRoot

880
		err = mfs.Mkdir(root, dirtomake, mfs.MkdirOpts{
881 882 883
			Mkparents:  dashp,
			Flush:      flush,
			CidBuilder: prefix,
884
		})
keks's avatar
keks committed
885

Overbool's avatar
Overbool committed
886
		return err
Jeromy's avatar
Jeromy committed
887 888 889
	},
}

890 891 892 893
type flushRes struct {
	Cid string
}

Overbool's avatar
Overbool committed
894
var filesFlushCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
895
	Helptext: cmds.HelpText{
896
		Tagline: "Flush a given path's data to disk.",
Jeromy's avatar
Jeromy committed
897
		ShortDescription: `
@RubenKelevra's avatar
@RubenKelevra committed
898
Flush a given path to the disk. This is only useful when other commands
Jeromy's avatar
Jeromy committed
899 900 901
are run with the '--flush=false'.
`,
	},
Steven Allen's avatar
Steven Allen committed
902 903
	Arguments: []cmds.Argument{
		cmds.StringArg("path", false, false, "Path to flush. Default: '/'."),
Jeromy's avatar
Jeromy committed
904
	},
Overbool's avatar
Overbool committed
905 906
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		nd, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
907
		if err != nil {
Overbool's avatar
Overbool committed
908
			return err
Jeromy's avatar
Jeromy committed
909 910
		}

911 912 913 914 915
		enc, err := cmdenv.GetCidEncoder(req)
		if err != nil {
			return err
		}

Jeromy's avatar
Jeromy committed
916
		path := "/"
Overbool's avatar
Overbool committed
917 918
		if len(req.Arguments) > 0 {
			path = req.Arguments[0]
Jeromy's avatar
Jeromy committed
919
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
920

921 922 923 924 925 926
		n, err := mfs.FlushPath(req.Context, nd.FilesRoot, path)
		if err != nil {
			return err
		}

		return cmds.EmitOnce(res, &flushRes{enc.Encode(n.Cid())})
Jeromy's avatar
Jeromy committed
927
	},
928
	Type: flushRes{},
Jeromy's avatar
Jeromy committed
929 930
}

Overbool's avatar
Overbool committed
931
var filesChcidCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
932
	Helptext: cmds.HelpText{
933
		Tagline: "Change the cid version or hash function of the root node of a given path.",
Kevin Atkinson's avatar
Kevin Atkinson committed
934
		ShortDescription: `
935
Change the cid version or hash function of the root node of a given path.
Kevin Atkinson's avatar
Kevin Atkinson committed
936 937
`,
	},
Steven Allen's avatar
Steven Allen committed
938 939
	Arguments: []cmds.Argument{
		cmds.StringArg("path", false, false, "Path to change. Default: '/'."),
Kevin Atkinson's avatar
Kevin Atkinson committed
940
	},
Steven Allen's avatar
Steven Allen committed
941
	Options: []cmds.Option{
942 943 944
		cidVersionOption,
		hashOption,
	},
Overbool's avatar
Overbool committed
945 946
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		nd, err := cmdenv.GetNode(env)
Kevin Atkinson's avatar
Kevin Atkinson committed
947
		if err != nil {
Overbool's avatar
Overbool committed
948
			return err
Kevin Atkinson's avatar
Kevin Atkinson committed
949 950 951
		}

		path := "/"
Overbool's avatar
Overbool committed
952 953
		if len(req.Arguments) > 0 {
			path = req.Arguments[0]
Kevin Atkinson's avatar
Kevin Atkinson committed
954 955
		}

Overbool's avatar
Overbool committed
956
		flush, _ := req.Options[filesFlushOptionName].(bool)
Kevin Atkinson's avatar
Kevin Atkinson committed
957 958 959

		prefix, err := getPrefix(req)
		if err != nil {
Overbool's avatar
Overbool committed
960
			return err
Kevin Atkinson's avatar
Kevin Atkinson committed
961
		}
keks's avatar
keks committed
962

963 964
		err = updatePath(nd.FilesRoot, path, prefix)
		if err == nil && flush {
965
			_, err = mfs.FlushPath(req.Context, nd.FilesRoot, path)
966 967
		}
		return err
Kevin Atkinson's avatar
Kevin Atkinson committed
968 969 970
	},
}

971
func updatePath(rt *mfs.Root, pth string, builder cid.Builder) error {
972
	if builder == nil {
Kevin Atkinson's avatar
Kevin Atkinson committed
973 974 975 976 977 978 979 980 981 982
		return nil
	}

	nd, err := mfs.Lookup(rt, pth)
	if err != nil {
		return err
	}

	switch n := nd.(type) {
	case *mfs.Directory:
983
		n.SetCidBuilder(builder)
Kevin Atkinson's avatar
Kevin Atkinson committed
984 985 986 987 988 989 990
	default:
		return fmt.Errorf("can only update directories")
	}

	return nil
}

Overbool's avatar
Overbool committed
991
var filesRmCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
992
	Helptext: cmds.HelpText{
rht's avatar
rht committed
993
		Tagline: "Remove a file.",
Jeromy's avatar
Jeromy committed
994
		ShortDescription: `
995
Remove files or directories.
Jeromy's avatar
Jeromy committed
996 997 998 999 1000 1001 1002 1003

    $ ipfs files rm /foo
    $ ipfs files ls /bar
    cat
    dog
    fish
    $ ipfs files rm -r /bar
`,
Jeromy's avatar
Jeromy committed
1004 1005
	},

Steven Allen's avatar
Steven Allen committed
1006 1007
	Arguments: []cmds.Argument{
		cmds.StringArg("path", true, true, "File to remove."),
Jeromy's avatar
Jeromy committed
1008
	},
Steven Allen's avatar
Steven Allen committed
1009 1010 1011
	Options: []cmds.Option{
		cmds.BoolOption(recursiveOptionName, "r", "Recursively remove directories."),
		cmds.BoolOption(forceOptionName, "Forcibly remove target at path; implies -r for directories"),
Jeromy's avatar
Jeromy committed
1012
	},
Overbool's avatar
Overbool committed
1013
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1014
		nd, err := cmdenv.GetNode(env)
Jeromy's avatar
Jeromy committed
1015
		if err != nil {
Overbool's avatar
Overbool committed
1016
			return err
Jeromy's avatar
Jeromy committed
1017 1018
		}

Overbool's avatar
Overbool committed
1019
		path, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
1020
		if err != nil {
Overbool's avatar
Overbool committed
1021
			return err
Jeromy's avatar
Jeromy committed
1022 1023 1024
		}

		if path == "/" {
Overbool's avatar
Overbool committed
1025
			return fmt.Errorf("cannot delete root")
Jeromy's avatar
Jeromy committed
1026 1027 1028 1029 1030 1031 1032
		}

		// 'rm a/b/c/' will fail unless we trim the slash at the end
		if path[len(path)-1] == '/' {
			path = path[:len(path)-1]
		}

1033 1034 1035 1036
		// if '--force' specified, it will remove anything else,
		// including file, directory, corrupted node, etc
		force, _ := req.Options[forceOptionName].(bool)

Jeromy's avatar
Jeromy committed
1037
		dir, name := gopath.Split(path)
1038 1039

		pdir, err := getParentDir(nd.FilesRoot, dir)
Jeromy's avatar
Jeromy committed
1040
		if err != nil {
Oli Evans's avatar
Oli Evans committed
1041 1042
			if force && err == os.ErrNotExist {
				return nil
1043
			}
Overbool's avatar
Overbool committed
1044
			return fmt.Errorf("parent lookup: %s", err)
Jeromy's avatar
Jeromy committed
1045 1046
		}

1047
		if force {
Jeromy's avatar
Jeromy committed
1048 1049
			err := pdir.Unlink(name)
			if err != nil {
Oli Evans's avatar
Oli Evans committed
1050
				if err == os.ErrNotExist {
1051 1052
					return nil
				}
Oli Evans's avatar
Oli Evans committed
1053
				return err
Jeromy's avatar
Jeromy committed
1054
			}
Overbool's avatar
Overbool committed
1055
			return pdir.Flush()
Jeromy's avatar
Jeromy committed
1056 1057
		}

1058 1059 1060
		// get child node by name, when the node is corrupted and nonexistent,
		// it will return specific error.
		child, err := pdir.Child(name)
Jeromy's avatar
Jeromy committed
1061
		if err != nil {
Overbool's avatar
Overbool committed
1062
			return err
Jeromy's avatar
Jeromy committed
1063 1064
		}

1065
		dashr, _ := req.Options[recursiveOptionName].(bool)
1066 1067

		switch child.(type) {
Jeromy's avatar
Jeromy committed
1068
		case *mfs.Directory:
1069
			if !dashr {
Overbool's avatar
Overbool committed
1070
				return fmt.Errorf("%s is a directory, use -r to remove directories", path)
Jeromy's avatar
Jeromy committed
1071
			}
1072
		}
Jeromy's avatar
Jeromy committed
1073

1074 1075
		err = pdir.Unlink(name)
		if err != nil {
Overbool's avatar
Overbool committed
1076
			return err
Jeromy's avatar
Jeromy committed
1077
		}
1078

Overbool's avatar
Overbool committed
1079
		return pdir.Flush()
Jeromy's avatar
Jeromy committed
1080 1081 1082
	},
}

1083
func getPrefixNew(req *cmds.Request) (cid.Builder, error) {
Kejie Zhang's avatar
Kejie Zhang committed
1084 1085
	cidVer, cidVerSet := req.Options[filesCidVersionOptionName].(int)
	hashFunStr, hashFunSet := req.Options[filesHashOptionName].(string)
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111

	if !cidVerSet && !hashFunSet {
		return nil, nil
	}

	if hashFunSet && cidVer == 0 {
		cidVer = 1
	}

	prefix, err := dag.PrefixForCidVersion(cidVer)
	if err != nil {
		return nil, err
	}

	if hashFunSet {
		hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
		if !ok {
			return nil, fmt.Errorf("unrecognized hash function: %s", strings.ToLower(hashFunStr))
		}
		prefix.MhType = hashFunCode
		prefix.MhLength = -1
	}

	return &prefix, nil
}

Overbool's avatar
Overbool committed
1112 1113 1114
func getPrefix(req *cmds.Request) (cid.Builder, error) {
	cidVer, cidVerSet := req.Options[filesCidVersionOptionName].(int)
	hashFunStr, hashFunSet := req.Options[filesHashOptionName].(string)
1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127

	if !cidVerSet && !hashFunSet {
		return nil, nil
	}

	if hashFunSet && cidVer == 0 {
		cidVer = 1
	}

	prefix, err := dag.PrefixForCidVersion(cidVer)
	if err != nil {
		return nil, err
	}
Jeromy's avatar
Jeromy committed
1128

1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140
	if hashFunSet {
		hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
		if !ok {
			return nil, fmt.Errorf("unrecognized hash function: %s", strings.ToLower(hashFunStr))
		}
		prefix.MhType = hashFunCode
		prefix.MhLength = -1
	}

	return &prefix, nil
}

1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153
func ensureContainingDirectoryExists(r *mfs.Root, path string, builder cid.Builder) error {
	dirtomake := gopath.Dir(path)

	if dirtomake == "/" {
		return nil
	}

	return mfs.Mkdir(r, dirtomake, mfs.MkdirOpts{
		Mkparents:  true,
		CidBuilder: builder,
	})
}

1154
func getFileHandle(r *mfs.Root, path string, create bool, builder cid.Builder) (*mfs.File, error) {
Jeromy's avatar
Jeromy committed
1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168
	target, err := mfs.Lookup(r, path)
	switch err {
	case nil:
		fi, ok := target.(*mfs.File)
		if !ok {
			return nil, fmt.Errorf("%s was not a file", path)
		}
		return fi, nil

	case os.ErrNotExist:
		if !create {
			return nil, err
		}

Dimitris Apostolou's avatar
Dimitris Apostolou committed
1169
		// if create is specified and the file doesn't exist, we create the file
Jeromy's avatar
Jeromy committed
1170
		dirname, fname := gopath.Split(path)
1171
		pdir, err := getParentDir(r, dirname)
Jeromy's avatar
Jeromy committed
1172 1173 1174
		if err != nil {
			return nil, err
		}
1175

1176 1177
		if builder == nil {
			builder = pdir.GetCidBuilder()
1178
		}
Jeromy's avatar
Jeromy committed
1179

1180
		nd := dag.NodeWithData(ft.FilePBData(nil, 0))
1181
		nd.SetCidBuilder(builder)
Jeromy's avatar
Jeromy committed
1182 1183 1184 1185 1186 1187 1188 1189 1190 1191
		err = pdir.AddChild(fname, nd)
		if err != nil {
			return nil, err
		}

		fsn, err := pdir.Child(fname)
		if err != nil {
			return nil, err
		}

Jeromy's avatar
Jeromy committed
1192 1193
		fi, ok := fsn.(*mfs.File)
		if !ok {
Dimitris Apostolou's avatar
Dimitris Apostolou committed
1194
			return nil, errors.New("expected *mfs.File, didn't get it. This is likely a race condition")
Jeromy's avatar
Jeromy committed
1195 1196
		}
		return fi, nil
Jeromy's avatar
Jeromy committed
1197 1198 1199 1200 1201

	default:
		return nil, err
	}
}
Jeromy's avatar
Jeromy committed
1202 1203 1204

func checkPath(p string) (string, error) {
	if len(p) == 0 {
Michael Muré's avatar
Michael Muré committed
1205
		return "", fmt.Errorf("paths must not be empty")
Jeromy's avatar
Jeromy committed
1206 1207 1208
	}

	if p[0] != '/' {
Michael Muré's avatar
Michael Muré committed
1209
		return "", fmt.Errorf("paths must start with a leading slash")
Jeromy's avatar
Jeromy committed
1210 1211 1212 1213 1214 1215 1216 1217
	}

	cleaned := gopath.Clean(p)
	if p[len(p)-1] == '/' && p != "/" {
		cleaned += "/"
	}
	return cleaned, nil
}
1218 1219 1220 1221 1222 1223 1224 1225 1226

func getParentDir(root *mfs.Root, dir string) (*mfs.Directory, error) {
	parent, err := mfs.Lookup(root, dir)
	if err != nil {
		return nil, err
	}

	pdir, ok := parent.(*mfs.Directory)
	if !ok {
Dimitris Apostolou's avatar
Dimitris Apostolou committed
1227
		return nil, errors.New("expected *mfs.Directory, didn't get it. This is likely a race condition")
1228 1229 1230
	}
	return pdir, nil
}