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

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

Michael Muré's avatar
Michael Muré committed
14 15
	oldcmds "github.com/ipfs/go-ipfs/commands"
	lgc "github.com/ipfs/go-ipfs/commands/legacy"
Jeromy's avatar
Jeromy committed
16
	core "github.com/ipfs/go-ipfs/core"
Jan Winkelmann's avatar
Jan Winkelmann committed
17
	e "github.com/ipfs/go-ipfs/core/commands/e"
Jeromy's avatar
Jeromy committed
18
	mfs "github.com/ipfs/go-ipfs/mfs"
Steven Allen's avatar
Steven Allen committed
19 20 21
	path "gx/ipfs/QmPqCBrmkm7jNfYi7xFS7mUZsrN6DEumBMrxLnL7axNJx1/go-path"
	resolver "gx/ipfs/QmPqCBrmkm7jNfYi7xFS7mUZsrN6DEumBMrxLnL7axNJx1/go-path/resolver"
	dag "gx/ipfs/QmXkZeJmx4c3ddjw81DQMUpM1e5LjAack5idzZYWUb2qAJ/go-merkledag"
22 23
	ft "gx/ipfs/Qmdqe1sKBpz6W8xFDptGfmzgCPQ5CXNuQPhZeELqMowgsQ/go-unixfs"
	uio "gx/ipfs/Qmdqe1sKBpz6W8xFDptGfmzgCPQ5CXNuQPhZeELqMowgsQ/go-unixfs/io"
Steven Allen's avatar
Steven Allen committed
24
	bservice "gx/ipfs/QmeZMtdkNG7u2CohGSL8mzAdZY2c3B1coYE91wvbzip1pF/go-blockservice"
Jeromy's avatar
Jeromy committed
25

Michael Muré's avatar
Michael Muré committed
26
	humanize "gx/ipfs/QmPSBJL4momYnE7DcUyk2DVhD6rH488ZmHBGLbxNdhU44K/go-humanize"
27
	cmdkit "gx/ipfs/QmPVqQHEfLpqK7JLCsUkyam7rhuV3MAeZ9gueQQCrBwCta/go-ipfs-cmdkit"
28
	mh "gx/ipfs/QmPnFwZ2JXKnXgMw8CdBPxn7FWh6LLdjUjxV1fKHuJnkr8/go-multihash"
Steven Allen's avatar
Steven Allen committed
29
	logging "gx/ipfs/QmRREK2CAZ5Re2Bd9zZFG6FeYDppUWt5cMgsoUEp3ktgSr/go-log"
30
	cmds "gx/ipfs/QmUQb3xtNzkQCgTj2NjaqcJZNv2nfSSub2QAdy9DtQMRBT/go-ipfs-cmds"
Steven Allen's avatar
Steven Allen committed
31
	offline "gx/ipfs/QmWURzU3XRY4wYBsu2LHukKKHp5skkYB1K357nzpbEvRY4/go-ipfs-exchange-offline"
Steven Allen's avatar
Steven Allen committed
32 33
	cid "gx/ipfs/QmYVNvtQkeZ6AKSwDrjQTs432QtL6umrrK41EBq3cu7iSP/go-cid"
	ipld "gx/ipfs/QmZtNq8dArGfnpCZfx2pUNY7UcjGhVp5qqwQ4hH6mpTMRQ/go-ipld-format"
Jeromy's avatar
Jeromy committed
34 35
)

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

Michael Muré's avatar
Michael Muré committed
38
// FilesCmd is the 'ipfs files' command
Jeromy's avatar
Jeromy committed
39
var FilesCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
40
	Helptext: cmdkit.HelpText{
41
		Tagline: "Interact with unixfs files.",
Jeromy's avatar
Jeromy committed
42
		ShortDescription: `
43 44
Files is an API for manipulating IPFS objects as if they were a unix
filesystem.
Jeromy's avatar
Jeromy committed
45

46
NOTE:
47 48 49 50 51 52 53
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
applies to running 'ipfs repo gc' concurrently with '--flush=false'
operations.
Jeromy's avatar
Jeromy committed
54 55
`,
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
56
	Options: []cmdkit.Option{
Michael Muré's avatar
Michael Muré committed
57
		cmdkit.BoolOption("f", "flush", "Flush target and ancestors after write.").WithDefault(true),
Jeromy's avatar
Jeromy committed
58
	},
Jeromy's avatar
Jeromy committed
59
	Subcommands: map[string]*cmds.Command{
Michael Muré's avatar
Michael Muré committed
60
		"read":  lgc.NewCommand(filesReadCmd),
61
		"write": filesWriteCmd,
Michael Muré's avatar
Michael Muré committed
62 63 64 65
		"mv":    lgc.NewCommand(filesMvCmd),
		"cp":    lgc.NewCommand(filesCpCmd),
		"ls":    lgc.NewCommand(filesLsCmd),
		"mkdir": lgc.NewCommand(filesMkdirCmd),
66
		"stat":  filesStatCmd,
Michael Muré's avatar
Michael Muré committed
67 68 69
		"rm":    lgc.NewCommand(filesRmCmd),
		"flush": lgc.NewCommand(filesFlushCmd),
		"chcid": lgc.NewCommand(filesChcidCmd),
Jeromy's avatar
Jeromy committed
70 71 72
	},
}

Jan Winkelmann's avatar
Jan Winkelmann committed
73 74
var cidVersionOption = cmdkit.IntOption("cid-version", "cid-ver", "Cid version to use. (experimental)")
var hashOption = cmdkit.StringOption("hash", "Hash function to use. Will set Cid version to 1 if used. (experimental)")
75

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

Michael Muré's avatar
Michael Muré committed
78
type statOutput struct {
79 80 81 82 83
	Hash           string
	Size           uint64
	CumulativeSize uint64
	Blocks         int
	Type           string
Michael Muré's avatar
Michael Muré committed
84
	WithLocality   bool   `json:",omitempty"`
85 86 87 88
	Local          bool   `json:",omitempty"`
	SizeLocal      uint64 `json:",omitempty"`
}

keks's avatar
keks committed
89 90 91 92 93 94
const defaultStatFormat = `<hash>
Size: <size>
CumulativeSize: <cumulsize>
ChildBlocks: <childs>
Type: <type>`

95
var filesStatCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
96
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
97
		Tagline: "Display file status.",
Jeromy's avatar
Jeromy committed
98 99
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
100 101
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("path", true, false, "Path to node to stat."),
Jeromy's avatar
Jeromy committed
102
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
103 104
	Options: []cmdkit.Option{
		cmdkit.StringOption("format", "Print statistics in given format. Allowed tokens: "+
keks's avatar
keks committed
105
			"<hash> <size> <cumulsize> <type> <childs>. Conflicts with other format options.").WithDefault(defaultStatFormat),
106 107
		cmdkit.BoolOption("hash", "Print only hash. Implies '--format=<hash>'. Conflicts with other format options."),
		cmdkit.BoolOption("size", "Print only size. Implies '--format=<cumulsize>'. Conflicts with other format options."),
108
		cmdkit.BoolOption("with-local", "Compute the amount of the dag that is local, and if possible the total size"),
109
	},
110
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) {
111 112 113

		_, err := statGetFormatOptions(req)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
114
			res.SetError(err, cmdkit.ErrClient)
115 116
		}

117
		node, err := GetNode(env)
Jeromy's avatar
Jeromy committed
118
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
119
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
120 121 122
			return
		}

123
		path, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
124
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
125
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
126 127 128
			return
		}

129 130 131 132 133 134 135 136 137 138 139 140 141 142
		withLocal, _ := req.Options["with-local"].(bool)

		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
		}

		nd, err := getNodeFromPath(req.Context, node, dagserv, path)
Jeromy's avatar
Jeromy committed
143
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
144
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
145 146 147
			return
		}

148
		o, err := statNode(nd)
Jeromy's avatar
Jeromy committed
149
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
150
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
151 152 153
			return
		}

154
		if !withLocal {
155
			cmds.EmitOnce(res, o)
156 157 158 159 160 161 162 163 164
			return
		}

		local, sizeLocal, err := walkBlock(req.Context, dagserv, nd)

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

165
		cmds.EmitOnce(res, o)
Jeromy's avatar
Jeromy committed
166
	},
167 168
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeEncoder(func(req *cmds.Request, w io.Writer, v interface{}) error {
Michael Muré's avatar
Michael Muré committed
169
			out, ok := v.(*statOutput)
Jan Winkelmann's avatar
Jan Winkelmann committed
170
			if !ok {
171
				return e.TypeErr(out, v)
Jan Winkelmann's avatar
Jan Winkelmann committed
172
			}
173

174
			s, _ := statGetFormatOptions(req)
175 176 177 178 179 180
			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)

181 182 183 184 185 186 187 188 189 190 191
			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
192
		}),
Jeromy's avatar
Jeromy committed
193
	},
Michael Muré's avatar
Michael Muré committed
194
	Type: statOutput{},
Jeromy's avatar
Jeromy committed
195 196
}

197 198 199 200
func moreThanOne(a, b, c bool) bool {
	return a && b || b && c || a && c
}

201
func statGetFormatOptions(req *cmds.Request) (string, error) {
202

203 204 205
	hash, _ := req.Options["hash"].(bool)
	size, _ := req.Options["size"].(bool)
	format, _ := req.Options["format"].(string)
206

keks's avatar
keks committed
207
	if moreThanOne(hash, size, format != defaultStatFormat) {
Michael Muré's avatar
Michael Muré committed
208
		return "", errFormat
209 210 211
	}

	if hash {
212 213 214 215 216
		return "<hash>", nil
	} else if size {
		return "<cumulsize>", nil
	} else {
		return format, nil
217 218 219
	}
}

Michael Muré's avatar
Michael Muré committed
220
func statNode(nd ipld.Node) (*statOutput, error) {
Jeromy's avatar
Jeromy committed
221
	c := nd.Cid()
Jeromy's avatar
Jeromy committed
222 223 224 225 226 227

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

228 229 230 231 232 233 234 235
	switch n := nd.(type) {
	case *dag.ProtoNode:
		d, err := ft.FromBytes(n.Data())
		if err != nil {
			return nil, err
		}

		var ndtype string
236 237
		switch d.GetType() {
		case ft.TDirectory, ft.THAMTShard:
238
			ndtype = "directory"
239
		case ft.TFile, ft.TMetadata, ft.TRaw:
240 241
			ndtype = "file"
		default:
242
			return nil, fmt.Errorf("unrecognized node type: %s", d.GetType())
243 244
		}

Michael Muré's avatar
Michael Muré committed
245
		return &statOutput{
246 247 248 249 250 251 252
			Hash:           c.String(),
			Blocks:         len(nd.Links()),
			Size:           d.GetFilesize(),
			CumulativeSize: cumulsize,
			Type:           ndtype,
		}, nil
	case *dag.RawNode:
Michael Muré's avatar
Michael Muré committed
253
		return &statOutput{
254 255 256 257 258 259
			Hash:           c.String(),
			Blocks:         0,
			Size:           cumulsize,
			CumulativeSize: cumulsize,
			Type:           "file",
		}, nil
Jeromy's avatar
Jeromy committed
260
	default:
261
		return nil, fmt.Errorf("not unixfs node (proto or raw)")
Jeromy's avatar
Jeromy committed
262
	}
Jeromy's avatar
Jeromy committed
263 264
}

265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
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
}

Michael Muré's avatar
Michael Muré committed
297
var filesCpCmd = &oldcmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
298
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
299
		Tagline: "Copy files into mfs.",
Jeromy's avatar
Jeromy committed
300
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
301 302 303
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("source", true, false, "Source object to copy."),
		cmdkit.StringArg("dest", true, false, "Destination to copy object to."),
Jeromy's avatar
Jeromy committed
304
	},
Michael Muré's avatar
Michael Muré committed
305
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Jeromy's avatar
Jeromy committed
306 307
		node, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
308
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
309 310 311
			return
		}

312
		flush, _, _ := req.Option("flush").Bool()
313

Jeromy's avatar
Jeromy committed
314 315
		src, err := checkPath(req.Arguments()[0])
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
316
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
317 318
			return
		}
319 320
		src = strings.TrimRight(src, "/")

Jeromy's avatar
Jeromy committed
321 322
		dst, err := checkPath(req.Arguments()[1])
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
323
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
324 325
			return
		}
Jeromy's avatar
Jeromy committed
326

327 328 329 330
		if dst[len(dst)-1] == '/' {
			dst += gopath.Base(src)
		}

331
		nd, err := getNodeFromPath(req.Context(), node, node.DAG, src)
Jeromy's avatar
Jeromy committed
332
		if err != nil {
333
			res.SetError(fmt.Errorf("cp: cannot get node from path %s: %s", src, err), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
334
			return
Jeromy's avatar
Jeromy committed
335 336 337 338
		}

		err = mfs.PutNode(node.FilesRoot, dst, nd)
		if err != nil {
339
			res.SetError(fmt.Errorf("cp: cannot put node in path %s: %s", dst, err), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
340 341
			return
		}
342 343 344 345

		if flush {
			err := mfs.FlushPath(node.FilesRoot, dst)
			if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
346
				res.SetError(err, cmdkit.ErrNormal)
347 348 349
				return
			}
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
350 351

		res.SetOutput(nil)
Jeromy's avatar
Jeromy committed
352 353 354
	},
}

355
func getNodeFromPath(ctx context.Context, node *core.IpfsNode, dagservice ipld.DAGService, p string) (ipld.Node, error) {
Jeromy's avatar
Jeromy committed
356 357 358 359 360 361 362
	switch {
	case strings.HasPrefix(p, "/ipfs/"):
		np, err := path.ParsePath(p)
		if err != nil {
			return nil, err
		}

363
		resolver := &resolver.Resolver{
364
			DAG:         dagservice,
365 366 367
			ResolveOnce: uio.ResolveUnixfsOnce,
		}

368
		return core.Resolve(ctx, node.Namesys, resolver, np)
Jeromy's avatar
Jeromy committed
369 370 371 372 373 374 375 376 377 378
	default:
		fsn, err := mfs.Lookup(node.FilesRoot, p)
		if err != nil {
			return nil, err
		}

		return fsn.GetNode()
	}
}

Michael Muré's avatar
Michael Muré committed
379
type filesLsOutput struct {
Jeromy's avatar
Jeromy committed
380 381 382
	Entries []mfs.NodeListing
}

Michael Muré's avatar
Michael Muré committed
383
var filesLsCmd = &oldcmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
384
	Helptext: cmdkit.HelpText{
385
		Tagline: "List directories in the local mutable namespace.",
Jeromy's avatar
Jeromy committed
386
		ShortDescription: `
387
List directories in the local mutable namespace.
Jeromy's avatar
Jeromy committed
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403

Examples:

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

    $ ipfs files ls /myfiles/a/b/c/d
    foo
    bar
`,
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
404 405
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("path", false, false, "Path to show listing for. Defaults to '/'."),
Jeromy's avatar
Jeromy committed
406
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
407 408
	Options: []cmdkit.Option{
		cmdkit.BoolOption("l", "Use long listing format."),
Lucas Molas's avatar
Lucas Molas committed
409
		cmdkit.BoolOption("U", "Do not sort; list entries in directory order."),
Jeromy's avatar
Jeromy committed
410
	},
Michael Muré's avatar
Michael Muré committed
411
	Run: func(req oldcmds.Request, res oldcmds.Response) {
412 413 414 415 416 417 418 419 420
		var arg string

		if len(req.Arguments()) == 0 {
			arg = "/"
		} else {
			arg = req.Arguments()[0]
		}

		path, err := checkPath(arg)
Jeromy's avatar
Jeromy committed
421
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
422
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
423 424 425
			return
		}

Jeromy's avatar
Jeromy committed
426 427
		nd, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
428
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
429 430 431 432 433
			return
		}

		fsn, err := mfs.Lookup(nd.FilesRoot, path)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
434
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
435 436 437
			return
		}

Jeromy's avatar
Jeromy committed
438 439
		long, _, _ := req.Option("l").Bool()

Jeromy's avatar
Jeromy committed
440 441
		switch fsn := fsn.(type) {
		case *mfs.Directory:
Jeromy's avatar
Jeromy committed
442 443
			if !long {
				var output []mfs.NodeListing
Jeromy's avatar
Jeromy committed
444
				names, err := fsn.ListNames(req.Context())
445
				if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
446
					res.SetError(err, cmdkit.ErrNormal)
447 448 449 450
					return
				}

				for _, name := range names {
Jeromy's avatar
Jeromy committed
451
					output = append(output, mfs.NodeListing{
Jeromy's avatar
Jeromy committed
452
						Name: name,
Jeromy's avatar
Jeromy committed
453 454
					})
				}
Michael Muré's avatar
Michael Muré committed
455
				res.SetOutput(&filesLsOutput{output})
Jeromy's avatar
Jeromy committed
456
			} else {
Jeromy's avatar
Jeromy committed
457
				listing, err := fsn.List(req.Context())
Jeromy's avatar
Jeromy committed
458
				if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
459
					res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
460 461
					return
				}
Michael Muré's avatar
Michael Muré committed
462
				res.SetOutput(&filesLsOutput{listing})
Jeromy's avatar
Jeromy committed
463 464 465
			}
			return
		case *mfs.File:
rht's avatar
rht committed
466
			_, name := gopath.Split(path)
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
			out := &filesLsOutput{[]mfs.NodeListing{mfs.NodeListing{Name: name}}}
			if long {
				out.Entries[0].Type = int(fsn.Type())

				size, err := fsn.Size()
				if err != nil {
					res.SetError(err, cmdkit.ErrNormal)
					return
				}
				out.Entries[0].Size = size

				nd, err := fsn.GetNode()
				if err != nil {
					res.SetError(err, cmdkit.ErrNormal)
					return
				}
				out.Entries[0].Hash = nd.Cid().String()
			}
Jeromy's avatar
Jeromy committed
485 486 487
			res.SetOutput(out)
			return
		default:
Jan Winkelmann's avatar
Jan Winkelmann committed
488
			res.SetError(errors.New("unrecognized type"), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
489 490
		}
	},
Michael Muré's avatar
Michael Muré committed
491 492
	Marshalers: oldcmds.MarshalerMap{
		oldcmds.Text: func(res oldcmds.Response) (io.Reader, error) {
Jan Winkelmann's avatar
Jan Winkelmann committed
493 494 495 496 497
			v, err := unwrapOutput(res.Output())
			if err != nil {
				return nil, err
			}

Michael Muré's avatar
Michael Muré committed
498
			out, ok := v.(*filesLsOutput)
Jan Winkelmann's avatar
Jan Winkelmann committed
499 500 501 502
			if !ok {
				return nil, e.TypeErr(out, v)
			}

Jeromy's avatar
Jeromy committed
503 504
			buf := new(bytes.Buffer)

Lucas Molas's avatar
Lucas Molas committed
505 506 507 508 509 510 511 512
			noSort, _, _ := res.Request().Option("U").Bool()
			if !noSort {
				sort.Slice(out.Entries, func(i, j int) bool {
					return strings.Compare(out.Entries[i].Name, out.Entries[j].Name) < 0
				})
			}

			long, _, _ := res.Request().Option("l").Bool()
Jeromy's avatar
Jeromy committed
513 514 515 516 517 518 519 520 521 522
			for _, o := range out.Entries {
				if long {
					fmt.Fprintf(buf, "%s\t%s\t%d\n", o.Name, o.Hash, o.Size)
				} else {
					fmt.Fprintf(buf, "%s\n", o.Name)
				}
			}
			return buf, nil
		},
	},
Michael Muré's avatar
Michael Muré committed
523
	Type: filesLsOutput{},
Jeromy's avatar
Jeromy committed
524 525
}

Michael Muré's avatar
Michael Muré committed
526
var filesReadCmd = &oldcmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
527
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
528
		Tagline: "Read a file in a given mfs.",
Jeromy's avatar
Jeromy committed
529
		ShortDescription: `
530 531
Read a specified number of bytes from a file at a given offset. By default,
will read the entire file similar to unix cat.
Jeromy's avatar
Jeromy committed
532 533 534 535 536

Examples:

    $ ipfs files read /test/hello
    hello
Jeromy's avatar
Jeromy committed
537
        `,
Jeromy's avatar
Jeromy committed
538 539
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
540 541
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("path", true, false, "Path to file to be read."),
Jeromy's avatar
Jeromy committed
542
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
543 544 545
	Options: []cmdkit.Option{
		cmdkit.IntOption("offset", "o", "Byte offset to begin reading from."),
		cmdkit.IntOption("count", "n", "Maximum number of bytes to read."),
Jeromy's avatar
Jeromy committed
546
	},
Michael Muré's avatar
Michael Muré committed
547
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Jeromy's avatar
Jeromy committed
548 549
		n, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
550
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
551 552 553
			return
		}

Jeromy's avatar
Jeromy committed
554 555
		path, err := checkPath(req.Arguments()[0])
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
556
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
557 558 559
			return
		}

Jeromy's avatar
Jeromy committed
560 561
		fsn, err := mfs.Lookup(n.FilesRoot, path)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
562
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
563 564 565 566 567
			return
		}

		fi, ok := fsn.(*mfs.File)
		if !ok {
Michael Muré's avatar
Michael Muré committed
568
			res.SetError(fmt.Errorf("%s was not a file", path), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
569 570 571
			return
		}

572 573
		rfd, err := fi.Open(mfs.OpenReadOnly, false)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
574
			res.SetError(err, cmdkit.ErrNormal)
575 576 577 578 579
			return
		}

		defer rfd.Close()

Jeromy's avatar
Jeromy committed
580 581
		offset, _, err := req.Option("offset").Int()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
582
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
583 584 585
			return
		}
		if offset < 0 {
Michael Muré's avatar
Michael Muré committed
586
			res.SetError(fmt.Errorf("cannot specify negative offset"), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
587 588 589
			return
		}

590
		filen, err := rfd.Size()
Jeromy's avatar
Jeromy committed
591
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
592
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
593 594 595 596
			return
		}

		if int64(offset) > filen {
Michael Muré's avatar
Michael Muré committed
597
			res.SetError(fmt.Errorf("offset was past end of file (%d > %d)", offset, filen), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
598 599
			return
		}
Jeromy's avatar
Jeromy committed
600

601
		_, err = rfd.Seek(int64(offset), io.SeekStart)
Jeromy's avatar
Jeromy committed
602
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
603
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
604 605
			return
		}
606 607

		var r io.Reader = &contextReaderWrapper{R: rfd, ctx: req.Context()}
Jeromy's avatar
Jeromy committed
608
		count, found, err := req.Option("count").Int()
Jeromy's avatar
Jeromy committed
609
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
610
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
611 612 613 614
			return
		}
		if found {
			if count < 0 {
Michael Muré's avatar
Michael Muré committed
615
				res.SetError(fmt.Errorf("cannot specify negative 'count'"), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
616 617
				return
			}
618
			r = io.LimitReader(r, int64(count))
Jeromy's avatar
Jeromy committed
619 620 621 622 623 624
		}

		res.SetOutput(r)
	},
}

Jeromy's avatar
Jeromy committed
625 626 627 628 629 630 631 632 633 634 635 636 637
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)
}

Michael Muré's avatar
Michael Muré committed
638
var filesMvCmd = &oldcmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
639
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
640
		Tagline: "Move files.",
Jeromy's avatar
Jeromy committed
641 642 643 644 645 646 647
		ShortDescription: `
Move files around. Just like traditional unix mv.

Example:

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

Jeromy's avatar
Jeromy committed
648
`,
Jeromy's avatar
Jeromy committed
649 650
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
651 652 653
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("source", true, false, "Source file to move."),
		cmdkit.StringArg("dest", true, false, "Destination path for file to be moved to."),
Jeromy's avatar
Jeromy committed
654
	},
Michael Muré's avatar
Michael Muré committed
655
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Jeromy's avatar
Jeromy committed
656 657
		n, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
658
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
659 660 661
			return
		}

Jeromy's avatar
Jeromy committed
662 663
		src, err := checkPath(req.Arguments()[0])
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
664
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
665 666 667 668
			return
		}
		dst, err := checkPath(req.Arguments()[1])
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
669
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
670 671
			return
		}
Jeromy's avatar
Jeromy committed
672 673 674

		err = mfs.Mv(n.FilesRoot, src, dst)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
675
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
676 677
			return
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
678 679

		res.SetOutput(nil)
Jeromy's avatar
Jeromy committed
680 681 682
	},
}

683
var filesWriteCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
684
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
685
		Tagline: "Write to a mutable file in a given filesystem.",
Jeromy's avatar
Jeromy committed
686 687
		ShortDescription: `
Write data to a file in a given filesystem. This command allows you to specify
688 689
a beginning offset to write to. The entire length of the input will be
written.
Jeromy's avatar
Jeromy committed
690

Jeromy's avatar
Jeromy committed
691
If the '--create' option is specified, the file will be created if it does not
Jeromy's avatar
Jeromy committed
692 693
exist. Nonexistant intermediate directories will not be created.

694 695 696 697 698 699 700
Newly created files will have the same CID version and hash function of the
parent directory unless the --cid-version and --hash options are used.

Newly created leaves will be in the legacy format (Protobuf) if the
CID version is 0, or raw is the CID version is non-zero.  Use of the
--raw-leaves option will override this behavior.

701 702 703 704
If the '--flush' option is set to false, changes will not be propogated to the
merkledag root. This can make operations much faster when doing a large number
of writes to a deeper directory structure.

705
EXAMPLE:
Jeromy's avatar
Jeromy committed
706

Jeromy's avatar
Jeromy committed
707 708
    echo "hello world" | ipfs files write --create /myfs/a/b/file
    echo "hello world" | ipfs files write --truncate /myfs/a/b/file
709

710
WARNING:
711

712 713 714
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
715
`,
Jeromy's avatar
Jeromy committed
716
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
717 718 719
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("path", true, false, "Path to write to."),
		cmdkit.FileArg("data", true, false, "Data to write.").EnableStdin(),
Jeromy's avatar
Jeromy committed
720
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
721 722 723 724 725 726
	Options: []cmdkit.Option{
		cmdkit.IntOption("offset", "o", "Byte offset to begin writing at."),
		cmdkit.BoolOption("create", "e", "Create the file if it does not exist."),
		cmdkit.BoolOption("truncate", "t", "Truncate the file to size zero before writing."),
		cmdkit.IntOption("count", "n", "Maximum number of bytes to read."),
		cmdkit.BoolOption("raw-leaves", "Use raw blocks for newly created leaf nodes. (experimental)"),
727 728
		cidVersionOption,
		hashOption,
Jeromy's avatar
Jeromy committed
729
	},
730 731
	Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) {
		path, err := checkPath(req.Arguments[0])
Jeromy's avatar
Jeromy committed
732
		if err != nil {
733
			re.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
734 735 736
			return
		}

737 738 739 740
		create, _ := req.Options["create"].(bool)
		trunc, _ := req.Options["truncate"].(bool)
		flush, _ := req.Options["flush"].(bool)
		rawLeaves, rawLeavesDef := req.Options["raw-leaves"].(bool)
741

742
		prefix, err := getPrefixNew(req)
743
		if err != nil {
744
			re.SetError(err, cmdkit.ErrNormal)
745 746
			return
		}
Jeromy's avatar
Jeromy committed
747

748
		nd, err := GetNode(env)
Jeromy's avatar
Jeromy committed
749
		if err != nil {
750
			re.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
751 752 753
			return
		}

754
		offset, _ := req.Options["offset"].(int)
Jeromy's avatar
Jeromy committed
755
		if offset < 0 {
756
			re.SetError(fmt.Errorf("cannot have negative write offset"), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
757 758 759
			return
		}

760
		fi, err := getFileHandle(nd.FilesRoot, path, create, prefix)
Jeromy's avatar
Jeromy committed
761
		if err != nil {
762
			re.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
763 764
			return
		}
765 766 767
		if rawLeavesDef {
			fi.RawLeaves = rawLeaves
		}
768

769 770
		wfd, err := fi.Open(mfs.OpenWriteOnly, flush)
		if err != nil {
771
			re.SetError(err, cmdkit.ErrNormal)
772
			return
773
		}
Jeromy's avatar
Jeromy committed
774

775 776 777
		defer func() {
			err := wfd.Close()
			if err != nil {
778
				re.SetError(err, cmdkit.ErrNormal)
779 780
			}
		}()
781

Jeromy's avatar
Jeromy committed
782
		if trunc {
783
			if err := wfd.Truncate(0); err != nil {
784
				re.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
785 786 787 788
				return
			}
		}

789
		count, countfound := req.Options["count"].(int)
Jeromy's avatar
Jeromy committed
790
		if countfound && count < 0 {
791
			re.SetError(fmt.Errorf("cannot have negative byte count"), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
792 793
			return
		}
Jeromy's avatar
Jeromy committed
794

795
		_, err = wfd.Seek(int64(offset), io.SeekStart)
Jeromy's avatar
Jeromy committed
796
		if err != nil {
797
			flog.Error("seekfail: ", err)
798
			re.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
799 800 801
			return
		}

802
		input, err := req.Files.NextFile()
Jeromy's avatar
Jeromy committed
803
		if err != nil {
804
			re.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
805 806 807
			return
		}

zramsay's avatar
zramsay committed
808 809 810 811 812
		var r io.Reader = input
		if countfound {
			r = io.LimitReader(r, int64(count))
		}

Jan Winkelmann's avatar
Jan Winkelmann committed
813
		_, err = io.Copy(wfd, r)
Jeromy's avatar
Jeromy committed
814
		if err != nil {
815
			re.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
816 817 818 819 820
			return
		}
	},
}

Michael Muré's avatar
Michael Muré committed
821
var filesMkdirCmd = &oldcmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
822
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
823
		Tagline: "Make directories.",
Jeromy's avatar
Jeromy committed
824 825 826
		ShortDescription: `
Create the directory if it does not already exist.

827 828 829
The directory will have the same CID version and hash function of the
parent directory unless the --cid-version and --hash options are used.

830
NOTE: All paths must be absolute.
Jeromy's avatar
Jeromy committed
831 832 833

Examples:

834 835
    $ ipfs files mkdir /test/newdir
    $ ipfs files mkdir -p /test/does/not/exist/yet
Jeromy's avatar
Jeromy committed
836 837 838
`,
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
839 840
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("path", true, false, "Path to dir to make."),
Jeromy's avatar
Jeromy committed
841
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
842 843
	Options: []cmdkit.Option{
		cmdkit.BoolOption("parents", "p", "No error if existing, make parent directories as needed."),
844 845
		cidVersionOption,
		hashOption,
Jeromy's avatar
Jeromy committed
846
	},
Michael Muré's avatar
Michael Muré committed
847
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Jeromy's avatar
Jeromy committed
848 849
		n, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
850
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
851 852 853 854
			return
		}

		dashp, _, _ := req.Option("parents").Bool()
Jeromy's avatar
Jeromy committed
855 856
		dirtomake, err := checkPath(req.Arguments()[0])
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
857
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
858 859 860
			return
		}

861
		flush, _, _ := req.Option("flush").Bool()
Jeromy's avatar
Jeromy committed
862

863 864
		prefix, err := getPrefix(req)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
865
			res.SetError(err, cmdkit.ErrNormal)
866 867 868 869
			return
		}
		root := n.FilesRoot

870 871 872 873 874
		err = mfs.Mkdir(root, dirtomake, mfs.MkdirOpts{
			Mkparents: dashp,
			Flush:     flush,
			Prefix:    prefix,
		})
875
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
876
			res.SetError(err, cmdkit.ErrNormal)
877
			return
Jeromy's avatar
Jeromy committed
878
		}
keks's avatar
keks committed
879

Jan Winkelmann's avatar
Jan Winkelmann committed
880
		res.SetOutput(nil)
Jeromy's avatar
Jeromy committed
881 882 883
	},
}

Michael Muré's avatar
Michael Muré committed
884
var filesFlushCmd = &oldcmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
885
	Helptext: cmdkit.HelpText{
886
		Tagline: "Flush a given path's data to disk.",
Jeromy's avatar
Jeromy committed
887
		ShortDescription: `
888
Flush a given path to disk. This is only useful when other commands
Jeromy's avatar
Jeromy committed
889 890 891
are run with the '--flush=false'.
`,
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
892 893
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("path", false, false, "Path to flush. Default: '/'."),
Jeromy's avatar
Jeromy committed
894
	},
Michael Muré's avatar
Michael Muré committed
895
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Jeromy's avatar
Jeromy committed
896 897
		nd, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
898
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
899 900 901 902 903 904 905 906 907 908
			return
		}

		path := "/"
		if len(req.Arguments()) > 0 {
			path = req.Arguments()[0]
		}

		err = mfs.FlushPath(nd.FilesRoot, path)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
909
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
910 911
			return
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
912 913

		res.SetOutput(nil)
Jeromy's avatar
Jeromy committed
914 915 916
	},
}

Michael Muré's avatar
Michael Muré committed
917
var filesChcidCmd = &oldcmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
918
	Helptext: cmdkit.HelpText{
919
		Tagline: "Change the cid version or hash function of the root node of a given path.",
Kevin Atkinson's avatar
Kevin Atkinson committed
920
		ShortDescription: `
921
Change the cid version or hash function of the root node of a given path.
Kevin Atkinson's avatar
Kevin Atkinson committed
922 923
`,
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
924 925
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("path", false, false, "Path to change. Default: '/'."),
Kevin Atkinson's avatar
Kevin Atkinson committed
926
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
927
	Options: []cmdkit.Option{
928 929 930
		cidVersionOption,
		hashOption,
	},
Michael Muré's avatar
Michael Muré committed
931
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Kevin Atkinson's avatar
Kevin Atkinson committed
932 933
		nd, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
934
			res.SetError(err, cmdkit.ErrNormal)
Kevin Atkinson's avatar
Kevin Atkinson committed
935 936 937 938 939 940 941 942 943 944 945 946
			return
		}

		path := "/"
		if len(req.Arguments()) > 0 {
			path = req.Arguments()[0]
		}

		flush, _, _ := req.Option("flush").Bool()

		prefix, err := getPrefix(req)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
947
			res.SetError(err, cmdkit.ErrNormal)
Kevin Atkinson's avatar
Kevin Atkinson committed
948 949 950 951 952
			return
		}

		err = updatePath(nd.FilesRoot, path, prefix, flush)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
953
			res.SetError(err, cmdkit.ErrNormal)
Kevin Atkinson's avatar
Kevin Atkinson committed
954 955
			return
		}
keks's avatar
keks committed
956 957

		res.SetOutput(nil)
Kevin Atkinson's avatar
Kevin Atkinson committed
958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984
	},
}

func updatePath(rt *mfs.Root, pth string, prefix *cid.Prefix, flush bool) error {
	if prefix == nil {
		return nil
	}

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

	switch n := nd.(type) {
	case *mfs.Directory:
		n.SetPrefix(prefix)
	default:
		return fmt.Errorf("can only update directories")
	}

	if flush {
		nd.Flush()
	}

	return nil
}

Michael Muré's avatar
Michael Muré committed
985
var filesRmCmd = &oldcmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
986
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
987
		Tagline: "Remove a file.",
Jeromy's avatar
Jeromy committed
988
		ShortDescription: `
989
Remove files or directories.
Jeromy's avatar
Jeromy committed
990 991 992 993 994 995 996 997

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

Jan Winkelmann's avatar
Jan Winkelmann committed
1000 1001
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("path", true, true, "File to remove."),
Jeromy's avatar
Jeromy committed
1002
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
1003 1004
	Options: []cmdkit.Option{
		cmdkit.BoolOption("recursive", "r", "Recursively remove directories."),
Jeromy's avatar
Jeromy committed
1005
	},
Michael Muré's avatar
Michael Muré committed
1006
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Jan Winkelmann's avatar
Jan Winkelmann committed
1007 1008
		defer res.SetOutput(nil)

Jeromy's avatar
Jeromy committed
1009 1010
		nd, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
1011
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1012 1013 1014
			return
		}

Jeromy's avatar
Jeromy committed
1015 1016
		path, err := checkPath(req.Arguments()[0])
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
1017
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1018 1019 1020 1021
			return
		}

		if path == "/" {
Jan Winkelmann's avatar
Jan Winkelmann committed
1022
			res.SetError(fmt.Errorf("cannot delete root"), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1023 1024 1025 1026 1027 1028 1029 1030
			return
		}

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

Jeromy's avatar
Jeromy committed
1031 1032 1033
		dir, name := gopath.Split(path)
		parent, err := mfs.Lookup(nd.FilesRoot, dir)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
1034
			res.SetError(fmt.Errorf("parent lookup: %s", err), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1035 1036 1037 1038 1039
			return
		}

		pdir, ok := parent.(*mfs.Directory)
		if !ok {
Michael Muré's avatar
Michael Muré committed
1040
			res.SetError(fmt.Errorf("no such file or directory: %s", path), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1041 1042 1043
			return
		}

Jeromy's avatar
Jeromy committed
1044 1045
		dashr, _, _ := req.Option("r").Bool()

Jeromy's avatar
Jeromy committed
1046 1047 1048 1049 1050
		var success bool
		defer func() {
			if success {
				err := pdir.Flush()
				if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
1051
					res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1052 1053 1054 1055 1056
					return
				}
			}
		}()

Jeromy's avatar
Jeromy committed
1057 1058 1059 1060
		// if '-r' specified, don't check file type (in bad scenarios, the block may not exist)
		if dashr {
			err := pdir.Unlink(name)
			if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
1061
				res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1062 1063 1064
				return
			}

Jeromy's avatar
Jeromy committed
1065
			success = true
Jeromy's avatar
Jeromy committed
1066 1067 1068
			return
		}

Jeromy's avatar
Jeromy committed
1069 1070
		childi, err := pdir.Child(name)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
1071
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1072 1073 1074 1075 1076
			return
		}

		switch childi.(type) {
		case *mfs.Directory:
Jan Winkelmann's avatar
Jan Winkelmann committed
1077
			res.SetError(fmt.Errorf("%s is a directory, use -r to remove directories", path), cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1078
			return
Jeromy's avatar
Jeromy committed
1079 1080 1081
		default:
			err := pdir.Unlink(name)
			if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
1082
				res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
1083 1084
				return
			}
Jeromy's avatar
Jeromy committed
1085 1086

			success = true
Jeromy's avatar
Jeromy committed
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 1112 1113 1114 1115 1116 1117 1118 1119
func getPrefixNew(req *cmds.Request) (*cid.Prefix, error) {
	cidVer, cidVerSet := req.Options["cid-version"].(int)
	hashFunStr, hashFunSet := req.Options["hash"].(string)

	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
}

Michael Muré's avatar
Michael Muré committed
1120
func getPrefix(req oldcmds.Request) (*cid.Prefix, error) {
1121
	cidVer, cidVerSet, _ := req.Option("cid-version").Int()
1122
	hashFunStr, hashFunSet, _ := req.Option("hash").String()
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135

	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
1136

1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
	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
}

func getFileHandle(r *mfs.Root, path string, create bool, prefix *cid.Prefix) (*mfs.File, error) {
Jeromy's avatar
Jeromy committed
1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167
	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
		}

		// if create is specified and the file doesnt exist, we create the file
		dirname, fname := gopath.Split(path)
		pdiri, err := mfs.Lookup(r, dirname)
		if err != nil {
1168
			flog.Error("lookupfail ", dirname)
Jeromy's avatar
Jeromy committed
1169 1170 1171 1172 1173 1174
			return nil, err
		}
		pdir, ok := pdiri.(*mfs.Directory)
		if !ok {
			return nil, fmt.Errorf("%s was not a directory", dirname)
		}
1175 1176 1177
		if prefix == nil {
			prefix = pdir.GetPrefix()
		}
Jeromy's avatar
Jeromy committed
1178

1179
		nd := dag.NodeWithData(ft.FilePBData(nil, 0))
1180
		nd.SetPrefix(prefix)
Jeromy's avatar
Jeromy committed
1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
		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
1191 1192
		fi, ok := fsn.(*mfs.File)
		if !ok {
Michael Muré's avatar
Michael Muré committed
1193
			return nil, errors.New("expected *mfs.File, didnt get it. This is likely a race condition")
Jeromy's avatar
Jeromy committed
1194 1195
		}
		return fi, nil
Jeromy's avatar
Jeromy committed
1196 1197 1198 1199 1200

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

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

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

	cleaned := gopath.Clean(p)
	if p[len(p)-1] == '/' && p != "/" {
		cleaned += "/"
	}
	return cleaned, nil
}