master
go 1,810 lines 47.7 KB
Raw
1 package commands
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 gopath "path"
11 "slices"
12 "strconv"
13 "strings"
14 "sync"
15 "sync/atomic"
16 "time"
17
18 humanize "github.com/dustin/go-humanize"
19 oldcmds "github.com/ipfs/kubo/commands"
20 "github.com/ipfs/kubo/config"
21 "github.com/ipfs/kubo/core"
22 "github.com/ipfs/kubo/core/commands/cmdenv"
23 "github.com/ipfs/kubo/core/node"
24 fsrepo "github.com/ipfs/kubo/repo/fsrepo"
25
26 bservice "github.com/ipfs/boxo/blockservice"
27 bstore "github.com/ipfs/boxo/blockstore"
28 offline "github.com/ipfs/boxo/exchange/offline"
29 dag "github.com/ipfs/boxo/ipld/merkledag"
30 ft "github.com/ipfs/boxo/ipld/unixfs"
31 mfs "github.com/ipfs/boxo/mfs"
32 "github.com/ipfs/boxo/path"
33 cid "github.com/ipfs/go-cid"
34 cidenc "github.com/ipfs/go-cidutil/cidenc"
35 "github.com/ipfs/go-datastore"
36 cmds "github.com/ipfs/go-ipfs-cmds"
37 ipld "github.com/ipfs/go-ipld-format"
38 logging "github.com/ipfs/go-log/v2"
39 iface "github.com/ipfs/kubo/core/coreiface"
40 mh "github.com/multiformats/go-multihash"
41 )
42
43 var flog = logging.Logger("cmds/files")
44
45 // Global counter for unflushed MFS operations
46 var noFlushOperationCounter atomic.Int64
47
48 // Cached limit value (read once on first use)
49 var (
50 noFlushLimit int64
51 noFlushLimitInit sync.Once
52 )
53
54 // updateNoFlushCounter manages the counter for unflushed operations
55 func updateNoFlushCounter(nd *core.IpfsNode, flush bool) error {
56 if flush {
57 // Reset counter when flushing
58 noFlushOperationCounter.Store(0)
59 return nil
60 }
61
62 // Cache the limit on first use (config doesn't change at runtime)
63 noFlushLimitInit.Do(func() {
64 noFlushLimit = int64(config.DefaultMFSNoFlushLimit)
65 if cfg, err := nd.Repo.Config(); err == nil && cfg.Internal.MFSNoFlushLimit != nil {
66 noFlushLimit = cfg.Internal.MFSNoFlushLimit.WithDefault(int64(config.DefaultMFSNoFlushLimit))
67 }
68 })
69
70 // Check if limit reached
71 if noFlushLimit > 0 && noFlushOperationCounter.Load() >= noFlushLimit {
72 return fmt.Errorf("reached limit of %d unflushed MFS operations. "+
73 "To resolve: 1) run 'ipfs files flush' to persist changes, "+
74 "2) use --flush=true (default), or "+
75 "3) increase Internal.MFSNoFlushLimit in config", noFlushLimit)
76 }
77
78 noFlushOperationCounter.Add(1)
79 return nil
80 }
81
82 // FilesCmd is the 'ipfs files' command
83 var FilesCmd = &cmds.Command{
84 Helptext: cmds.HelpText{
85 Tagline: "Interact with unixfs files.",
86 ShortDescription: `
87 Files is an API for manipulating IPFS objects as if they were a Unix
88 filesystem.
89
90 The files facility interacts with MFS (Mutable File System). MFS acts as a
91 single, dynamic filesystem mount. MFS has a root CID that is transparently
92 updated when a change happens (and can be checked with "ipfs files stat /").
93
94 All files and folders within MFS are respected and will not be deleted
95 during garbage collections. However, a DAG may be referenced in MFS without
96 being fully available locally (MFS content is lazy loaded when accessed).
97 MFS is independent from the list of pinned items ("ipfs pin ls"). Calls to
98 "ipfs pin add" and "ipfs pin rm" will add and remove pins independently of
99 MFS. If MFS content that was additionally pinned is removed by calling
100 "ipfs files rm", it will still remain pinned.
101
102 Content added with "ipfs add" (which by default also becomes pinned), is not
103 added to MFS. Any content can be lazily referenced from MFS with the command
104 "ipfs files cp /ipfs/<cid> /some/path/" (see ipfs files cp --help).
105
106 NOTE: Most of the subcommands of 'ipfs files' accept the '--flush' flag. It
107 defaults to true and ensures two things: 1) that the changes are reflected in
108 the full MFS structure (updated CIDs) 2) that the parent-folder's cache is
109 cleared. Use caution when setting this flag to false. It will improve
110 performance for large numbers of file operations, but it does so at the cost
111 of consistency guarantees. If the daemon is unexpectedly killed before running
112 'ipfs files flush' on the files in question, then data may be lost. This also
113 applies to run 'ipfs repo gc' concurrently with '--flush=false' operations.
114
115 When using '--flush=false', operations are limited to prevent unbounded
116 memory growth. After reaching Internal.MFSNoFlushLimit operations, further
117 operations will fail until you run 'ipfs files flush'. This explicit failure
118 (instead of auto-flushing) ensures you maintain control over when data is
119 persisted, preventing unexpected partial states and making batch operations
120 predictable. We recommend flushing paths regularly, especially folders with
121 many write operations, to clear caches, free memory, and maintain good
122 performance.`,
123 },
124 Options: []cmds.Option{
125 cmds.BoolOption(filesFlushOptionName, "f", "Flush target and ancestors after write.").WithDefault(true),
126 },
127 Subcommands: map[string]*cmds.Command{
128 "read": filesReadCmd,
129 "write": filesWriteCmd,
130 "mv": filesMvCmd,
131 "cp": filesCpCmd,
132 "ls": filesLsCmd,
133 "mkdir": filesMkdirCmd,
134 "stat": filesStatCmd,
135 "rm": filesRmCmd,
136 "flush": filesFlushCmd,
137 "chcid": filesChcidCmd,
138 "chmod": filesChmodCmd,
139 "chroot": filesChrootCmd,
140 "touch": filesTouchCmd,
141 },
142 }
143
144 const (
145 filesCidVersionOptionName = "cid-version"
146 filesHashOptionName = "hash"
147 )
148
149 var (
150 cidVersionOption = cmds.IntOption(filesCidVersionOptionName, "cid-ver", "Cid version to use. (experimental)")
151 hashOption = cmds.StringOption(filesHashOptionName, "Hash function to use. Will set Cid version to 1 if used. (experimental)")
152 )
153
154 var errFormat = errors.New("format was set by multiple options. Only one format option is allowed")
155
156 type statOutput struct {
157 Hash string
158 Size uint64
159 CumulativeSize uint64
160 Blocks int
161 Type string
162 WithLocality bool `json:",omitempty"`
163 Local bool `json:",omitempty"`
164 SizeLocal uint64 `json:",omitempty"`
165 Mode uint32 `json:",omitempty"`
166 Mtime int64 `json:",omitempty"`
167 MtimeNsecs int `json:",omitempty"`
168 }
169
170 func (s *statOutput) MarshalJSON() ([]byte, error) {
171 type so statOutput
172 out := &struct {
173 *so
174 Mode string `json:",omitempty"`
175 }{so: (*so)(s)}
176
177 if s.Mode != 0 {
178 out.Mode = fmt.Sprintf("%04o", s.Mode)
179 }
180 return json.Marshal(out)
181 }
182
183 func (s *statOutput) UnmarshalJSON(data []byte) error {
184 var err error
185 type so statOutput
186 tmp := &struct {
187 *so
188 Mode string `json:",omitempty"`
189 }{so: (*so)(s)}
190
191 if err := json.Unmarshal(data, &tmp); err != nil {
192 return err
193 }
194
195 if tmp.Mode != "" {
196 mode, err := strconv.ParseUint(tmp.Mode, 8, 32)
197 if err == nil {
198 s.Mode = uint32(mode)
199 }
200 }
201 return err
202 }
203
204 const (
205 defaultStatFormat = `<hash>
206 Size: <size>
207 CumulativeSize: <cumulsize>
208 ChildBlocks: <childs>
209 Type: <type>
210 Mode: <mode> (<mode-octal>)
211 Mtime: <mtime>`
212 filesFormatOptionName = "format"
213 filesSizeOptionName = "size"
214 filesWithLocalOptionName = "with-local"
215 filesStatUnspecified = "not set"
216 )
217
218 var filesStatCmd = &cmds.Command{
219 Helptext: cmds.HelpText{
220 Tagline: "Display file status.",
221 },
222
223 Arguments: []cmds.Argument{
224 cmds.StringArg("path", true, false, "Path to node to stat."),
225 },
226 Options: []cmds.Option{
227 cmds.StringOption(filesFormatOptionName, "Print statistics in given format. Allowed tokens: "+
228 "<hash> <size> <cumulsize> <type> <childs> and optional <mode> <mode-octal> <mtime> <mtime-secs> <mtime-nsecs>."+
229 "Conflicts with other format options.").WithDefault(defaultStatFormat),
230 cmds.BoolOption(filesHashOptionName, "Print only hash. Implies '--format=<hash>'. Conflicts with other format options."),
231 cmds.BoolOption(filesSizeOptionName, "Print only size. Implies '--format=<cumulsize>'. Conflicts with other format options."),
232 cmds.BoolOption(filesWithLocalOptionName, "Compute the amount of the dag that is local, and if possible the total size"),
233 },
234 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
235 _, err := statGetFormatOptions(req)
236 if err != nil {
237 return cmds.Errorf(cmds.ErrClient, "invalid parameters: %s", err)
238 }
239
240 node, err := cmdenv.GetNode(env)
241 if err != nil {
242 return err
243 }
244
245 api, err := cmdenv.GetApi(env, req)
246 if err != nil {
247 return err
248 }
249
250 path, err := checkPath(req.Arguments[0])
251 if err != nil {
252 return err
253 }
254
255 withLocal, _ := req.Options[filesWithLocalOptionName].(bool)
256
257 enc, err := cmdenv.GetCidEncoder(req)
258 if err != nil {
259 return err
260 }
261
262 var dagserv ipld.DAGService
263 if withLocal {
264 // an offline DAGService will not fetch from the network
265 dagserv = dag.NewDAGService(bservice.New(
266 node.Blockstore,
267 offline.Exchange(node.Blockstore),
268 ))
269 } else {
270 dagserv = node.DAG
271 }
272
273 nd, err := getNodeFromPath(req.Context, node, api, path)
274 if err != nil {
275 return err
276 }
277
278 o, err := statNode(nd, enc)
279 if err != nil {
280 return err
281 }
282
283 if !withLocal {
284 return cmds.EmitOnce(res, o)
285 }
286
287 local, sizeLocal, err := walkBlock(req.Context, dagserv, nd)
288 if err != nil {
289 return err
290 }
291
292 o.WithLocality = true
293 o.Local = local
294 o.SizeLocal = sizeLocal
295
296 return cmds.EmitOnce(res, o)
297 },
298 Encoders: cmds.EncoderMap{
299 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *statOutput) error {
300 mode, modeo := filesStatUnspecified, filesStatUnspecified
301 if out.Mode != 0 {
302 mode = strings.ToLower(os.FileMode(out.Mode).String())
303 modeo = "0" + strconv.FormatInt(int64(out.Mode&0x1FF), 8)
304 }
305 mtime, mtimes, mtimens := filesStatUnspecified, filesStatUnspecified, filesStatUnspecified
306 if out.Mtime > 0 {
307 mtime = time.Unix(out.Mtime, int64(out.MtimeNsecs)).UTC().Format("2 Jan 2006, 15:04:05 MST")
308 mtimes = strconv.FormatInt(out.Mtime, 10)
309 mtimens = strconv.Itoa(out.MtimeNsecs)
310 }
311
312 s, _ := statGetFormatOptions(req)
313 s = strings.Replace(s, "<hash>", out.Hash, -1)
314 s = strings.Replace(s, "<size>", fmt.Sprintf("%d", out.Size), -1)
315 s = strings.Replace(s, "<cumulsize>", fmt.Sprintf("%d", out.CumulativeSize), -1)
316 s = strings.Replace(s, "<childs>", fmt.Sprintf("%d", out.Blocks), -1)
317 s = strings.Replace(s, "<type>", out.Type, -1)
318 s = strings.Replace(s, "<mode>", mode, -1)
319 s = strings.Replace(s, "<mode-octal>", modeo, -1)
320 s = strings.Replace(s, "<mtime>", mtime, -1)
321 s = strings.Replace(s, "<mtime-secs>", mtimes, -1)
322 s = strings.Replace(s, "<mtime-nsecs>", mtimens, -1)
323
324 fmt.Fprintln(w, s)
325
326 if out.WithLocality {
327 fmt.Fprintf(w, "Local: %s of %s (%.2f%%)\n",
328 humanize.Bytes(out.SizeLocal),
329 humanize.Bytes(out.CumulativeSize),
330 100.0*float64(out.SizeLocal)/float64(out.CumulativeSize),
331 )
332 }
333
334 return nil
335 }),
336 },
337 Type: statOutput{},
338 }
339
340 func moreThanOne(a, b, c bool) bool {
341 return a && b || b && c || a && c
342 }
343
344 func statGetFormatOptions(req *cmds.Request) (string, error) {
345 hash, _ := req.Options[filesHashOptionName].(bool)
346 size, _ := req.Options[filesSizeOptionName].(bool)
347 format, _ := req.Options[filesFormatOptionName].(string)
348
349 if moreThanOne(hash, size, format != defaultStatFormat) {
350 return "", errFormat
351 }
352
353 if hash {
354 return "<hash>", nil
355 } else if size {
356 return "<cumulsize>", nil
357 } else {
358 return format, nil
359 }
360 }
361
362 func statNode(nd ipld.Node, enc cidenc.Encoder) (*statOutput, error) {
363 c := nd.Cid()
364
365 cumulsize, err := nd.Size()
366 if err != nil {
367 return nil, err
368 }
369
370 switch n := nd.(type) {
371 case *dag.ProtoNode:
372 return statProtoNode(n, enc, c, cumulsize)
373 case *dag.RawNode:
374 return &statOutput{
375 Hash: enc.Encode(c),
376 Blocks: 0,
377 Size: cumulsize,
378 CumulativeSize: cumulsize,
379 Type: "file",
380 }, nil
381 default:
382 return nil, errors.New("not unixfs node (proto or raw)")
383 }
384 }
385
386 func statProtoNode(n *dag.ProtoNode, enc cidenc.Encoder, cid cid.Cid, cumulsize uint64) (*statOutput, error) {
387 d, err := ft.FSNodeFromBytes(n.Data())
388 if err != nil {
389 return nil, err
390 }
391
392 stat := statOutput{
393 Hash: enc.Encode(cid),
394 Blocks: len(n.Links()),
395 Size: d.FileSize(),
396 CumulativeSize: cumulsize,
397 }
398
399 switch d.Type() {
400 case ft.TDirectory, ft.THAMTShard:
401 stat.Type = "directory"
402 case ft.TFile, ft.TSymlink, ft.TMetadata, ft.TRaw:
403 stat.Type = "file"
404 default:
405 return nil, fmt.Errorf("unrecognized node type: %s", d.Type())
406 }
407
408 if mode := d.Mode(); mode != 0 {
409 stat.Mode = uint32(mode)
410 } else if d.Type() == ft.TSymlink {
411 stat.Mode = uint32(os.ModeSymlink | 0x1FF)
412 }
413
414 if mt := d.ModTime(); !mt.IsZero() {
415 stat.Mtime = mt.Unix()
416 if ns := mt.Nanosecond(); ns > 0 {
417 stat.MtimeNsecs = ns
418 }
419 }
420
421 return &stat, nil
422 }
423
424 func walkBlock(ctx context.Context, dagserv ipld.DAGService, nd ipld.Node) (bool, uint64, error) {
425 // Start with the block data size
426 sizeLocal := uint64(len(nd.RawData()))
427
428 local := true
429
430 for _, link := range nd.Links() {
431 child, err := dagserv.Get(ctx, link.Cid)
432
433 if ipld.IsNotFound(err) {
434 local = false
435 continue
436 }
437
438 if err != nil {
439 return local, sizeLocal, err
440 }
441
442 childLocal, childLocalSize, err := walkBlock(ctx, dagserv, child)
443 if err != nil {
444 return local, sizeLocal, err
445 }
446
447 // Recursively add the child size
448 local = local && childLocal
449 sizeLocal += childLocalSize
450 }
451
452 return local, sizeLocal, nil
453 }
454
455 var errFilesCpInvalidUnixFS = errors.New("cp: source must be a valid UnixFS (dag-pb or raw codec)")
456 var filesCpCmd = &cmds.Command{
457 Helptext: cmds.HelpText{
458 Tagline: "Add references to IPFS files and directories in MFS (or copy within MFS).",
459 ShortDescription: `
460 "ipfs files cp" can be used to add references to any IPFS file or directory
461 (usually in the form /ipfs/<CID>, but also any resolvable path) into MFS.
462 This performs a lazy copy: the full DAG will not be fetched, only the root
463 node being copied.
464
465 It can also be used to copy files within MFS, but in the case when an
466 IPFS-path matches an existing MFS path, the IPFS path wins.
467
468 In order to add content to MFS from disk, you can use "ipfs add" to obtain the
469 IPFS Content Identifier and then "ipfs files cp" to copy it into MFS:
470
471 $ ipfs add --quieter --pin=false <your file>
472 # ...
473 # ... outputs the root CID at the end
474 $ ipfs files cp /ipfs/<CID> /your/desired/mfs/path
475
476 If you wish to fully copy content from a different IPFS peer into MFS, do not
477 forget to force IPFS to fetch the full DAG after doing a "cp" operation. i.e:
478
479 $ ipfs files cp /ipfs/<CID> /your/desired/mfs/path
480 $ ipfs pin add <CID>
481
482 The lazy-copy feature can also be used to protect partial DAG contents from
483 garbage collection. i.e. adding the Wikipedia root to MFS would not download
484 all the Wikipedia, but will prevent any downloaded Wikipedia-DAG content from
485 being GC'ed.
486 `,
487 },
488 Arguments: []cmds.Argument{
489 cmds.StringArg("source", true, false, "Source IPFS or MFS path to copy."),
490 cmds.StringArg("dest", true, false, "Destination within MFS."),
491 },
492 Options: []cmds.Option{
493 cmds.BoolOption(forceOptionName, "Force overwrite of existing files."),
494 cmds.BoolOption(filesParentsOptionName, "p", "Make parent directories as needed."),
495 },
496 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
497 nd, err := cmdenv.GetNode(env)
498 if err != nil {
499 return err
500 }
501
502 cfg, err := nd.Repo.Config()
503 if err != nil {
504 return err
505 }
506
507 prefix, err := getPrefix(req, &cfg.Import)
508 if err != nil {
509 return err
510 }
511
512 api, err := cmdenv.GetApi(env, req)
513 if err != nil {
514 return err
515 }
516
517 src, err := checkPath(req.Arguments[0])
518 if err != nil {
519 return err
520 }
521 src = strings.TrimRight(src, "/")
522
523 dst, err := checkPath(req.Arguments[1])
524 if err != nil {
525 return err
526 }
527
528 if dst[len(dst)-1] == '/' {
529 dst += gopath.Base(src)
530 }
531
532 node, err := getNodeFromPath(req.Context, nd, api, src)
533 if err != nil {
534 return fmt.Errorf("cp: cannot get node from path %s: %s", src, err)
535 }
536
537 // Sanity-check: ensure root CID is a valid UnixFS (dag-pb or raw block)
538 // Context: https://github.com/ipfs/kubo/issues/10331
539 srcCidType := node.Cid().Type()
540 switch srcCidType {
541 case cid.Raw:
542 if _, ok := node.(*dag.RawNode); !ok {
543 return errFilesCpInvalidUnixFS
544 }
545 case cid.DagProtobuf:
546 if _, ok := node.(*dag.ProtoNode); !ok {
547 return errFilesCpInvalidUnixFS
548 }
549 if _, err = ft.FSNodeFromBytes(node.(*dag.ProtoNode).Data()); err != nil {
550 return fmt.Errorf("%w: %v", errFilesCpInvalidUnixFS, err)
551 }
552 default:
553 return errFilesCpInvalidUnixFS
554 }
555
556 mkParents, _ := req.Options[filesParentsOptionName].(bool)
557 if mkParents {
558 maxDirLinks := int(cfg.Import.UnixFSDirectoryMaxLinks.WithDefault(config.DefaultUnixFSDirectoryMaxLinks))
559 sizeEstimationMode := cfg.Import.HAMTSizeEstimationMode()
560 err := ensureContainingDirectoryExists(nd.FilesRoot, dst,
561 mfs.WithCidBuilder(prefix),
562 mfs.WithMaxLinks(maxDirLinks),
563 mfs.WithSizeEstimationMode(sizeEstimationMode),
564 )
565 if err != nil {
566 return err
567 }
568 }
569
570 force, _ := req.Options[forceOptionName].(bool)
571 if force {
572 if err = unlinkNodeIfExists(nd, dst); err != nil {
573 return fmt.Errorf("cp: cannot unlink existing file: %s", err)
574 }
575 }
576
577 flush, _ := req.Options[filesFlushOptionName].(bool)
578
579 if err := updateNoFlushCounter(nd, flush); err != nil {
580 return err
581 }
582
583 err = mfs.PutNode(nd.FilesRoot, dst, node)
584 if err != nil {
585 return fmt.Errorf("cp: cannot put node in path %s: %s", dst, err)
586 }
587 if flush {
588 if _, err := mfs.FlushPath(req.Context, nd.FilesRoot, dst); err != nil {
589 return fmt.Errorf("cp: cannot flush the created file %s: %s", dst, err)
590 }
591 // Flush parent to clear directory cache and free memory.
592 parent := gopath.Dir(dst)
593 if _, err = mfs.FlushPath(req.Context, nd.FilesRoot, parent); err != nil {
594 return fmt.Errorf("cp: cannot flush the created file's parent folder %s: %s", dst, err)
595 }
596 }
597
598 return nil
599 },
600 }
601
602 func getNodeFromPath(ctx context.Context, node *core.IpfsNode, api iface.CoreAPI, p string) (ipld.Node, error) {
603 switch {
604 case strings.HasPrefix(p, "/ipfs/"):
605 pth, err := path.NewPath(p)
606 if err != nil {
607 return nil, err
608 }
609
610 return api.ResolveNode(ctx, pth)
611 default:
612 fsn, err := mfs.Lookup(node.FilesRoot, p)
613 if err != nil {
614 return nil, err
615 }
616
617 return fsn.GetNode()
618 }
619 }
620
621 func unlinkNodeIfExists(node *core.IpfsNode, path string) error {
622 dir, name := gopath.Split(path)
623 parent, err := mfs.Lookup(node.FilesRoot, dir)
624 if err != nil {
625 if errors.Is(err, os.ErrNotExist) {
626 return nil
627 }
628 return err
629 }
630
631 pdir, ok := parent.(*mfs.Directory)
632 if !ok {
633 return fmt.Errorf("not a directory: %s", dir)
634 }
635
636 // Attempt to unlink if child is a file, ignore error since
637 // we are only concerned with unlinking an existing file.
638 child, err := pdir.Child(name)
639 if err != nil {
640 return nil // no child file, nothing to unlink
641 }
642
643 if child.Type() != mfs.TFile {
644 return fmt.Errorf("not a file: %s", path)
645 }
646
647 return pdir.Unlink(name)
648 }
649
650 type filesLsOutput struct {
651 Entries []mfs.NodeListing
652 }
653
654 const (
655 longOptionName = "long"
656 dontSortOptionName = "U"
657 )
658
659 var filesLsCmd = &cmds.Command{
660 Helptext: cmds.HelpText{
661 Tagline: "List directories in the local mutable namespace.",
662 ShortDescription: `
663 List directories in the local mutable namespace (works on both IPFS and MFS paths).
664
665 Examples:
666
667 $ ipfs files ls /welcome/docs/
668 about
669 contact
670 help
671 quick-start
672 readme
673 security-notes
674
675 $ ipfs files ls /myfiles/a/b/c/d
676 foo
677 bar
678 `,
679 },
680 Arguments: []cmds.Argument{
681 cmds.StringArg("path", false, false, "Path to show listing for. Defaults to '/'."),
682 },
683 Options: []cmds.Option{
684 cmds.BoolOption(longOptionName, "l", "Use long listing format."),
685 cmds.BoolOption(dontSortOptionName, "Do not sort; list entries in directory order."),
686 },
687 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
688 var arg string
689
690 if len(req.Arguments) == 0 {
691 arg = "/"
692 } else {
693 arg = req.Arguments[0]
694 }
695
696 path, err := checkPath(arg)
697 if err != nil {
698 return err
699 }
700
701 nd, err := cmdenv.GetNode(env)
702 if err != nil {
703 return err
704 }
705
706 fsn, err := mfs.Lookup(nd.FilesRoot, path)
707 if err != nil {
708 return err
709 }
710
711 long, _ := req.Options[longOptionName].(bool)
712
713 enc, err := cmdenv.GetCidEncoder(req)
714 if err != nil {
715 return err
716 }
717
718 switch fsn := fsn.(type) {
719 case *mfs.Directory:
720 if !long {
721 var output []mfs.NodeListing
722 names, err := fsn.ListNames(req.Context)
723 if err != nil {
724 return err
725 }
726
727 for _, name := range names {
728 output = append(output, mfs.NodeListing{
729 Name: name,
730 })
731 }
732 return cmds.EmitOnce(res, &filesLsOutput{output})
733 }
734 listing, err := fsn.List(req.Context)
735 if err != nil {
736 return err
737 }
738 return cmds.EmitOnce(res, &filesLsOutput{listing})
739 case *mfs.File:
740 _, name := gopath.Split(path)
741 out := &filesLsOutput{[]mfs.NodeListing{{Name: name}}}
742 if long {
743 out.Entries[0].Type = int(fsn.Type())
744
745 size, err := fsn.Size()
746 if err != nil {
747 return err
748 }
749 out.Entries[0].Size = size
750
751 nd, err := fsn.GetNode()
752 if err != nil {
753 return err
754 }
755 out.Entries[0].Hash = enc.Encode(nd.Cid())
756 }
757 return cmds.EmitOnce(res, out)
758 default:
759 return errors.New("unrecognized type")
760 }
761 },
762 Encoders: cmds.EncoderMap{
763 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *filesLsOutput) error {
764 noSort, _ := req.Options[dontSortOptionName].(bool)
765 if !noSort {
766 slices.SortFunc(out.Entries, func(a, b mfs.NodeListing) int {
767 return strings.Compare(a.Name, b.Name)
768 })
769 }
770
771 long, _ := req.Options[longOptionName].(bool)
772 for _, o := range out.Entries {
773 if long {
774 if o.Type == int(mfs.TDir) {
775 o.Name += "/"
776 }
777 fmt.Fprintf(w, "%s\t%s\t%d\n", o.Name, o.Hash, o.Size)
778 } else {
779 fmt.Fprintf(w, "%s\n", o.Name)
780 }
781 }
782
783 return nil
784 }),
785 },
786 Type: filesLsOutput{},
787 }
788
789 const (
790 filesOffsetOptionName = "offset"
791 filesCountOptionName = "count"
792 )
793
794 var filesReadCmd = &cmds.Command{
795 Helptext: cmds.HelpText{
796 Tagline: "Read a file from MFS.",
797 ShortDescription: `
798 Read a specified number of bytes from a file at a given offset. By default,
799 it will read the entire file similar to the Unix cat.
800
801 Examples:
802
803 $ ipfs files read /test/hello
804 hello
805 `,
806 },
807
808 Arguments: []cmds.Argument{
809 cmds.StringArg("path", true, false, "Path to file to be read."),
810 },
811 Options: []cmds.Option{
812 cmds.Int64Option(filesOffsetOptionName, "o", "Byte offset to begin reading from."),
813 cmds.Int64Option(filesCountOptionName, "n", "Maximum number of bytes to read."),
814 },
815 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
816 nd, err := cmdenv.GetNode(env)
817 if err != nil {
818 return err
819 }
820
821 path, err := checkPath(req.Arguments[0])
822 if err != nil {
823 return err
824 }
825
826 fsn, err := mfs.Lookup(nd.FilesRoot, path)
827 if err != nil {
828 return fmt.Errorf("%s: %w", path, err)
829 }
830
831 fi, ok := fsn.(*mfs.File)
832 if !ok {
833 return fmt.Errorf("%s was not a file", path)
834 }
835
836 rfd, err := fi.Open(mfs.Flags{Read: true})
837 if err != nil {
838 return err
839 }
840
841 defer rfd.Close()
842
843 offset, _ := req.Options[offsetOptionName].(int64)
844 if offset < 0 {
845 return fmt.Errorf("cannot specify negative offset")
846 }
847
848 filen, err := rfd.Size()
849 if err != nil {
850 return err
851 }
852
853 if int64(offset) > filen {
854 return fmt.Errorf("offset was past end of file (%d > %d)", offset, filen)
855 }
856
857 _, err = rfd.Seek(int64(offset), io.SeekStart)
858 if err != nil {
859 return err
860 }
861
862 var r io.Reader = &contextReaderWrapper{R: rfd, ctx: req.Context}
863 count, found := req.Options[filesCountOptionName].(int64)
864 if found {
865 if count < 0 {
866 return fmt.Errorf("cannot specify negative 'count'")
867 }
868 r = io.LimitReader(r, int64(count))
869 }
870 return res.Emit(r)
871 },
872 }
873
874 type contextReader interface {
875 CtxReadFull(context.Context, []byte) (int, error)
876 }
877
878 type contextReaderWrapper struct {
879 R contextReader
880 ctx context.Context
881 }
882
883 func (crw *contextReaderWrapper) Read(b []byte) (int, error) {
884 return crw.R.CtxReadFull(crw.ctx, b)
885 }
886
887 var filesMvCmd = &cmds.Command{
888 Helptext: cmds.HelpText{
889 Tagline: "Move files.",
890 ShortDescription: `
891 Move files around. Just like the traditional Unix mv.
892
893 Example:
894
895 $ ipfs files mv /myfs/a/b/c /myfs/foo/newc
896
897 `,
898 },
899
900 Arguments: []cmds.Argument{
901 cmds.StringArg("source", true, false, "Source file to move."),
902 cmds.StringArg("dest", true, false, "Destination path for file to be moved to."),
903 },
904 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
905 nd, err := cmdenv.GetNode(env)
906 if err != nil {
907 return err
908 }
909
910 flush, _ := req.Options[filesFlushOptionName].(bool)
911
912 if err := updateNoFlushCounter(nd, flush); err != nil {
913 return err
914 }
915
916 src, err := checkPath(req.Arguments[0])
917 if err != nil {
918 return err
919 }
920 dst, err := checkPath(req.Arguments[1])
921 if err != nil {
922 return err
923 }
924
925 err = mfs.Mv(nd.FilesRoot, src, dst)
926 if err != nil {
927 return err
928 }
929 if flush {
930 parentSrc := gopath.Dir(src)
931 parentDst := gopath.Dir(dst)
932 // Flush parent to clear directory cache and free memory.
933 if _, err = mfs.FlushPath(req.Context, nd.FilesRoot, parentDst); err != nil {
934 return fmt.Errorf("cp: cannot flush the destination file's parent folder %s: %s", dst, err)
935 }
936
937 // Avoid re-flushing when moving within the same folder.
938 if parentSrc != parentDst {
939 if _, err = mfs.FlushPath(req.Context, nd.FilesRoot, parentSrc); err != nil {
940 return fmt.Errorf("cp: cannot flush the source's file's parent folder %s: %s", dst, err)
941 }
942 }
943
944 if _, err = mfs.FlushPath(req.Context, nd.FilesRoot, "/"); err != nil {
945 return err
946 }
947 }
948
949 return nil
950 },
951 }
952
953 const (
954 filesCreateOptionName = "create"
955 filesParentsOptionName = "parents"
956 filesTruncateOptionName = "truncate"
957 filesRawLeavesOptionName = "raw-leaves"
958 filesFlushOptionName = "flush"
959 )
960
961 var filesWriteCmd = &cmds.Command{
962 Helptext: cmds.HelpText{
963 Tagline: "Append to (modify) a file in MFS.",
964 ShortDescription: `
965 A low-level MFS command that allows you to append data to a file. If you want
966 to add a file without modifying an existing one, use 'ipfs add --to-files'
967 instead.
968 `,
969 LongDescription: `
970 A low-level MFS command that allows you to append data at the end of a file, or
971 specify a beginning offset within a file to write to. The entire length of the
972 input will be written.
973
974 If the '--create' option is specified, the file will be created if it does not
975 exist. Nonexistent intermediate directories will not be created unless the
976 '--parents' option is specified.
977
978 Newly created files will have the same CID version and hash function of the
979 parent directory unless the '--cid-version' and '--hash' options are used.
980
981 Newly created leaves will be in the legacy format (Protobuf) if the
982 CID version is 0, or raw if the CID version is non-zero. Use of the
983 '--raw-leaves' option will override this behavior.
984
985 If the '--flush' option is set to false, changes will not be propagated to the
986 merkledag root. This can make operations much faster when doing a large number
987 of writes to a deeper directory structure.
988
989 EXAMPLE:
990
991 echo "hello world" | ipfs files write --create --parents /myfs/a/b/file
992 echo "hello world" | ipfs files write --truncate /myfs/a/b/file
993
994 WARNING:
995
996 Usage of the '--flush=false' option does not guarantee data durability until
997 the tree has been flushed. This can be accomplished by running 'ipfs files
998 stat' on the file or any of its ancestors.
999
1000 WARNING:
1001
1002 The CID produced by 'files write' will be different from 'ipfs add' because
1003 'ipfs files write' creates a trickle-dag optimized for append-only operations.
1004 See '--trickle' in 'ipfs add --help' for more information.
1005
1006 NOTE: The 'Import.UnixFSFileMaxLinks' config option does not apply to this command.
1007 Trickle DAG has a fixed internal structure optimized for append operations.
1008 To use configurable max-links, use 'ipfs add' with balanced DAG layout.
1009
1010 If you want to add a file without modifying an existing one,
1011 use 'ipfs add' with '--to-files':
1012
1013 > ipfs files mkdir -p /myfs/dir
1014 > ipfs add example.jpg --to-files /myfs/dir/
1015 > ipfs files ls /myfs/dir/
1016 example.jpg
1017
1018 See '--to-files' in 'ipfs add --help' for more information.
1019 `,
1020 },
1021 Arguments: []cmds.Argument{
1022 cmds.StringArg("path", true, false, "Path to write to."),
1023 cmds.FileArg("data", true, false, "Data to write.").EnableStdin(),
1024 },
1025 Options: []cmds.Option{
1026 cmds.Int64Option(filesOffsetOptionName, "o", "Byte offset to begin writing at."),
1027 cmds.BoolOption(filesCreateOptionName, "e", "Create the file if it does not exist."),
1028 cmds.BoolOption(filesParentsOptionName, "p", "Make parent directories as needed."),
1029 cmds.BoolOption(filesTruncateOptionName, "t", "Truncate the file to size zero before writing."),
1030 cmds.Int64Option(filesCountOptionName, "n", "Maximum number of bytes to read."),
1031 cmds.BoolOption(filesRawLeavesOptionName, "Use raw blocks for newly created leaf nodes. (experimental)"),
1032 cidVersionOption,
1033 hashOption,
1034 },
1035 Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) (retErr error) {
1036 path, err := checkPath(req.Arguments[0])
1037 if err != nil {
1038 return err
1039 }
1040
1041 nd, err := cmdenv.GetNode(env)
1042 if err != nil {
1043 return err
1044 }
1045
1046 cfg, err := nd.Repo.Config()
1047 if err != nil {
1048 return err
1049 }
1050
1051 create, _ := req.Options[filesCreateOptionName].(bool)
1052 mkParents, _ := req.Options[filesParentsOptionName].(bool)
1053 trunc, _ := req.Options[filesTruncateOptionName].(bool)
1054 flush, _ := req.Options[filesFlushOptionName].(bool)
1055 rawLeaves, rawLeavesDef := req.Options[filesRawLeavesOptionName].(bool)
1056
1057 if err := updateNoFlushCounter(nd, flush); err != nil {
1058 return err
1059 }
1060
1061 if !rawLeavesDef && cfg.Import.UnixFSRawLeaves != config.Default {
1062 rawLeavesDef = true
1063 rawLeaves = cfg.Import.UnixFSRawLeaves.WithDefault(config.DefaultUnixFSRawLeaves)
1064 }
1065
1066 prefix, err := getPrefix(req, &cfg.Import)
1067 if err != nil {
1068 return err
1069 }
1070
1071 offset, _ := req.Options[filesOffsetOptionName].(int64)
1072 if offset < 0 {
1073 return fmt.Errorf("cannot have negative write offset")
1074 }
1075
1076 if mkParents {
1077 maxDirLinks := int(cfg.Import.UnixFSDirectoryMaxLinks.WithDefault(config.DefaultUnixFSDirectoryMaxLinks))
1078 sizeEstimationMode := cfg.Import.HAMTSizeEstimationMode()
1079 err := ensureContainingDirectoryExists(nd.FilesRoot, path,
1080 mfs.WithCidBuilder(prefix),
1081 mfs.WithMaxLinks(maxDirLinks),
1082 mfs.WithSizeEstimationMode(sizeEstimationMode),
1083 )
1084 if err != nil {
1085 return err
1086 }
1087 }
1088
1089 fi, err := getFileHandle(nd.FilesRoot, path, create, prefix)
1090 if err != nil {
1091 return err
1092 }
1093 if rawLeavesDef {
1094 fi.RawLeaves = rawLeaves
1095 }
1096
1097 wfd, err := fi.Open(mfs.Flags{Write: true, Sync: flush})
1098 if err != nil {
1099 return err
1100 }
1101
1102 defer func() {
1103 err := wfd.Close()
1104 if err != nil {
1105 if retErr == nil {
1106 retErr = err
1107 } else {
1108 flog.Error("files: error closing file mfs file descriptor", err)
1109 }
1110 }
1111 if flush {
1112 // Flush parent to clear directory cache and free memory.
1113 parent := gopath.Dir(path)
1114 if _, err := mfs.FlushPath(req.Context, nd.FilesRoot, parent); err != nil {
1115 if retErr == nil {
1116 retErr = err
1117 } else {
1118 flog.Error("files: flushing the parent folder", err)
1119 }
1120 }
1121 }
1122 }()
1123
1124 if trunc {
1125 if err := wfd.Truncate(0); err != nil {
1126 return err
1127 }
1128 }
1129
1130 count, countfound := req.Options[filesCountOptionName].(int64)
1131 if countfound && count < 0 {
1132 return fmt.Errorf("cannot have negative byte count")
1133 }
1134
1135 _, err = wfd.Seek(int64(offset), io.SeekStart)
1136 if err != nil {
1137 flog.Error("seekfail: ", err)
1138 return err
1139 }
1140
1141 var r io.Reader
1142 r, err = cmdenv.GetFileArg(req.Files.Entries())
1143 if err != nil {
1144 return err
1145 }
1146 if countfound {
1147 r = io.LimitReader(r, int64(count))
1148 }
1149
1150 _, err = io.Copy(wfd, r)
1151 return err
1152 },
1153 }
1154
1155 var filesMkdirCmd = &cmds.Command{
1156 Helptext: cmds.HelpText{
1157 Tagline: "Make directories.",
1158 ShortDescription: `
1159 Create the directory if it does not already exist.
1160
1161 The directory will have the same CID version and hash function of the
1162 parent directory unless the --cid-version and --hash options are used.
1163
1164 NOTE: All paths must be absolute.
1165
1166 Examples:
1167
1168 $ ipfs files mkdir /test/newdir
1169 $ ipfs files mkdir -p /test/does/not/exist/yet
1170 `,
1171 },
1172
1173 Arguments: []cmds.Argument{
1174 cmds.StringArg("path", true, false, "Path to dir to make."),
1175 },
1176 Options: []cmds.Option{
1177 cmds.BoolOption(filesParentsOptionName, "p", "No error if existing, make parent directories as needed."),
1178 cidVersionOption,
1179 hashOption,
1180 },
1181 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1182 n, err := cmdenv.GetNode(env)
1183 if err != nil {
1184 return err
1185 }
1186
1187 cfg, err := n.Repo.Config()
1188 if err != nil {
1189 return err
1190 }
1191
1192 dashp, _ := req.Options[filesParentsOptionName].(bool)
1193 dirtomake, err := checkPath(req.Arguments[0])
1194 if err != nil {
1195 return err
1196 }
1197
1198 flush, _ := req.Options[filesFlushOptionName].(bool)
1199
1200 if err := updateNoFlushCounter(n, flush); err != nil {
1201 return err
1202 }
1203
1204 prefix, err := getPrefix(req, &cfg.Import)
1205 if err != nil {
1206 return err
1207 }
1208 root := n.FilesRoot
1209
1210 maxDirLinks := int(cfg.Import.UnixFSDirectoryMaxLinks.WithDefault(config.DefaultUnixFSDirectoryMaxLinks))
1211 sizeEstimationMode := cfg.Import.HAMTSizeEstimationMode()
1212
1213 err = mfs.Mkdir(root, dirtomake, mfs.MkdirOpts{Mkparents: dashp, Flush: flush},
1214 mfs.WithCidBuilder(prefix),
1215 mfs.WithMaxLinks(maxDirLinks),
1216 mfs.WithSizeEstimationMode(sizeEstimationMode),
1217 )
1218
1219 return err
1220 },
1221 }
1222
1223 type flushRes struct {
1224 Cid string
1225 }
1226
1227 var filesFlushCmd = &cmds.Command{
1228 Helptext: cmds.HelpText{
1229 Tagline: "Flush a given path's data to disk.",
1230 ShortDescription: `
1231 Flush a given path to the disk. This is only useful when other commands
1232 are run with the '--flush=false'.
1233 `,
1234 },
1235 Arguments: []cmds.Argument{
1236 cmds.StringArg("path", false, false, "Path to flush. Default: '/'."),
1237 },
1238 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1239 nd, err := cmdenv.GetNode(env)
1240 if err != nil {
1241 return err
1242 }
1243
1244 enc, err := cmdenv.GetCidEncoder(req)
1245 if err != nil {
1246 return err
1247 }
1248
1249 path := "/"
1250 if len(req.Arguments) > 0 {
1251 path = req.Arguments[0]
1252 }
1253
1254 n, err := mfs.FlushPath(req.Context, nd.FilesRoot, path)
1255 if err != nil {
1256 return err
1257 }
1258
1259 // Reset the counter (flush always resets)
1260 noFlushOperationCounter.Store(0)
1261
1262 return cmds.EmitOnce(res, &flushRes{enc.Encode(n.Cid())})
1263 },
1264 Type: flushRes{},
1265 }
1266
1267 var filesChcidCmd = &cmds.Command{
1268 Helptext: cmds.HelpText{
1269 Tagline: "Change the CID version or hash function of the root node of a given path.",
1270 ShortDescription: `
1271 Change the CID version or hash function of the root node of a given path.
1272
1273 Note: the MFS root ('/') CID format is controlled by Import.CidVersion and
1274 Import.HashFunction in the config and cannot be changed with this command.
1275 Use 'ipfs config' to modify these values instead. This command only works
1276 on subdirectories of the MFS root.
1277 `,
1278 },
1279 Arguments: []cmds.Argument{
1280 cmds.StringArg("path", true, false, "Path to change (must not be '/')."),
1281 },
1282 Options: []cmds.Option{
1283 cidVersionOption,
1284 hashOption,
1285 },
1286 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1287 nd, err := cmdenv.GetNode(env)
1288 if err != nil {
1289 return err
1290 }
1291
1292 path := req.Arguments[0]
1293 if path == "/" {
1294 return fmt.Errorf("cannot change CID format of the MFS root; " +
1295 "use 'ipfs config Import.CidVersion' and 'ipfs config Import.HashFunction' instead")
1296 }
1297
1298 flush, _ := req.Options[filesFlushOptionName].(bool)
1299
1300 // Note: files chcid is for explicitly changing CID format, so we don't
1301 // fall back to Import config here. If no options are provided, it does nothing.
1302 prefix, err := getPrefix(req, nil)
1303 if err != nil {
1304 return err
1305 }
1306
1307 if err := updatePath(nd.FilesRoot, path, prefix); err != nil {
1308 return err
1309 }
1310 if flush {
1311 if _, err = mfs.FlushPath(req.Context, nd.FilesRoot, path); err != nil {
1312 return err
1313 }
1314 // Flush parent to clear directory cache and free memory.
1315 parent := gopath.Dir(path)
1316 if _, err = mfs.FlushPath(req.Context, nd.FilesRoot, parent); err != nil {
1317 return err
1318 }
1319 }
1320 return nil
1321 },
1322 }
1323
1324 func updatePath(rt *mfs.Root, pth string, builder cid.Builder) error {
1325 if builder == nil {
1326 return nil
1327 }
1328
1329 nd, err := mfs.Lookup(rt, pth)
1330 if err != nil {
1331 return err
1332 }
1333
1334 switch n := nd.(type) {
1335 case *mfs.Directory:
1336 n.SetCidBuilder(builder)
1337 default:
1338 return fmt.Errorf("can only update directories")
1339 }
1340
1341 return nil
1342 }
1343
1344 var filesRmCmd = &cmds.Command{
1345 Helptext: cmds.HelpText{
1346 Tagline: "Remove a file from MFS.",
1347 ShortDescription: `
1348 Remove files or directories.
1349
1350 $ ipfs files rm /foo
1351 $ ipfs files ls /bar
1352 cat
1353 dog
1354 fish
1355 $ ipfs files rm -r /bar
1356 `,
1357 },
1358
1359 Arguments: []cmds.Argument{
1360 cmds.StringArg("path", true, true, "File to remove."),
1361 },
1362 Options: []cmds.Option{
1363 cmds.BoolOption(recursiveOptionName, "r", "Recursively remove directories."),
1364 cmds.BoolOption(forceOptionName, "Forcibly remove target at path; implies -r for directories"),
1365 },
1366 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1367 // Check if user explicitly set --flush=false
1368 if flushOpt, ok := req.Options[filesFlushOptionName]; ok {
1369 if flush, ok := flushOpt.(bool); ok && !flush {
1370 return fmt.Errorf("files rm always flushes for safety. The --flush flag cannot be set to false for this command")
1371 }
1372 }
1373
1374 nd, err := cmdenv.GetNode(env)
1375 if err != nil {
1376 return err
1377 }
1378 // if '--force' specified, it will remove anything else,
1379 // including file, directory, corrupted node, etc
1380 force, _ := req.Options[forceOptionName].(bool)
1381 dashr, _ := req.Options[recursiveOptionName].(bool)
1382 var errs []error
1383 for _, arg := range req.Arguments {
1384 path, err := checkPath(arg)
1385 if err != nil {
1386 errs = append(errs, fmt.Errorf("%s is not a valid path: %w", arg, err))
1387 continue
1388 }
1389
1390 if err := removePath(nd.FilesRoot, path, force, dashr); err != nil {
1391 errs = append(errs, fmt.Errorf("%s: %w", path, err))
1392 }
1393 }
1394 if len(errs) > 0 {
1395 for _, err = range errs {
1396 e := res.Emit(err.Error())
1397 if e != nil {
1398 return e
1399 }
1400 }
1401 return fmt.Errorf("can't remove some files")
1402 }
1403 return nil
1404 },
1405 }
1406
1407 func removePath(filesRoot *mfs.Root, path string, force bool, dashr bool) error {
1408 if path == "/" {
1409 return fmt.Errorf("cannot delete root")
1410 }
1411
1412 // 'rm a/b/c/' will fail unless we trim the slash at the end
1413 if path[len(path)-1] == '/' {
1414 path = path[:len(path)-1]
1415 }
1416
1417 dir, name := gopath.Split(path)
1418
1419 pdir, err := getParentDir(filesRoot, dir)
1420 if err != nil {
1421 if force && err == os.ErrNotExist {
1422 return nil
1423 }
1424 return err
1425 }
1426
1427 if force {
1428 err := pdir.Unlink(name)
1429 if err != nil {
1430 if err == os.ErrNotExist {
1431 return nil
1432 }
1433 return err
1434 }
1435 return pdir.Flush()
1436 }
1437
1438 // get child node by name, when the node is corrupted and nonexistent,
1439 // it will return specific error.
1440 child, err := pdir.Child(name)
1441 if err != nil {
1442 return err
1443 }
1444
1445 switch child.(type) {
1446 case *mfs.Directory:
1447 if !dashr {
1448 return fmt.Errorf("path is a directory, use -r to remove directories")
1449 }
1450 }
1451
1452 err = pdir.Unlink(name)
1453 if err != nil {
1454 return err
1455 }
1456
1457 return pdir.Flush()
1458 }
1459
1460 // getPrefix builds a cid.Builder from CLI flags, falling back to importCfg
1461 // when provided. Returns (nil, nil) when neither CLI nor config set a value.
1462 func getPrefix(req *cmds.Request, importCfg *config.Import) (cid.Builder, error) {
1463 cidVer, cidVerSet := req.Options[filesCidVersionOptionName].(int)
1464 hashFunStr, hashFunSet := req.Options[filesHashOptionName].(string)
1465
1466 if cidVerSet || hashFunSet {
1467 // CLI flags take precedence: build prefix from them directly.
1468 if hashFunSet && cidVer == 0 {
1469 cidVer = 1
1470 }
1471 prefix, err := dag.PrefixForCidVersion(cidVer)
1472 if err != nil {
1473 return nil, err
1474 }
1475 if hashFunSet {
1476 hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
1477 if !ok {
1478 return nil, fmt.Errorf("unrecognized hash function: %q", hashFunStr)
1479 }
1480 prefix.MhType = hashFunCode
1481 prefix.MhLength = -1
1482 }
1483 return &prefix, nil
1484 }
1485
1486 // No CLI flags: fall back to Import config.
1487 if importCfg != nil {
1488 return importCfg.UnixFSCidBuilder()
1489 }
1490
1491 return nil, nil
1492 }
1493
1494 func ensureContainingDirectoryExists(r *mfs.Root, path string, opts ...mfs.Option) error {
1495 dirtomake := gopath.Dir(path)
1496
1497 if dirtomake == "/" {
1498 return nil
1499 }
1500
1501 return mfs.Mkdir(r, dirtomake, mfs.MkdirOpts{Mkparents: true}, opts...)
1502 }
1503
1504 func getFileHandle(r *mfs.Root, path string, create bool, builder cid.Builder) (*mfs.File, error) {
1505 target, err := mfs.Lookup(r, path)
1506 switch err {
1507 case nil:
1508 fi, ok := target.(*mfs.File)
1509 if !ok {
1510 return nil, fmt.Errorf("%s was not a file", path)
1511 }
1512 return fi, nil
1513
1514 case os.ErrNotExist:
1515 if !create {
1516 return nil, err
1517 }
1518
1519 // if create is specified and the file doesn't exist, we create the file
1520 dirname, fname := gopath.Split(path)
1521 pdir, err := getParentDir(r, dirname)
1522 if err != nil {
1523 return nil, err
1524 }
1525
1526 if builder == nil {
1527 builder = pdir.GetCidBuilder()
1528 }
1529
1530 nd := dag.NodeWithData(ft.FilePBData(nil, 0))
1531 err = nd.SetCidBuilder(builder)
1532 if err != nil {
1533 return nil, err
1534 }
1535 err = pdir.AddChild(fname, nd)
1536 if err != nil {
1537 return nil, err
1538 }
1539
1540 fsn, err := pdir.Child(fname)
1541 if err != nil {
1542 return nil, err
1543 }
1544
1545 fi, ok := fsn.(*mfs.File)
1546 if !ok {
1547 return nil, errors.New("expected *mfs.File, didn't get it. This is likely a race condition")
1548 }
1549 return fi, nil
1550
1551 default:
1552 return nil, err
1553 }
1554 }
1555
1556 func checkPath(p string) (string, error) {
1557 if len(p) == 0 {
1558 return "", fmt.Errorf("paths must not be empty")
1559 }
1560
1561 if p[0] != '/' {
1562 return "", fmt.Errorf("paths must start with a leading slash")
1563 }
1564
1565 cleaned := gopath.Clean(p)
1566 if p[len(p)-1] == '/' && p != "/" {
1567 cleaned += "/"
1568 }
1569 return cleaned, nil
1570 }
1571
1572 func getParentDir(root *mfs.Root, dir string) (*mfs.Directory, error) {
1573 parent, err := mfs.Lookup(root, dir)
1574 if err != nil {
1575 return nil, err
1576 }
1577
1578 pdir, ok := parent.(*mfs.Directory)
1579 if !ok {
1580 return nil, errors.New("expected *mfs.Directory, didn't get it. This is likely a race condition")
1581 }
1582 return pdir, nil
1583 }
1584
1585 var filesChmodCmd = &cmds.Command{
1586 Status: cmds.Experimental,
1587 Helptext: cmds.HelpText{
1588 Tagline: "Change optional POSIX mode permissions",
1589 ShortDescription: `
1590 The mode argument must be specified in Unix numeric notation.
1591
1592 $ ipfs files chmod 0644 /foo
1593 $ ipfs files stat /foo
1594 ...
1595 Type: file
1596 Mode: -rw-r--r-- (0644)
1597 ...
1598 `,
1599 },
1600 Arguments: []cmds.Argument{
1601 cmds.StringArg("mode", true, false, "Mode to apply to node (numeric notation)"),
1602 cmds.StringArg("path", true, false, "Path to apply mode"),
1603 },
1604 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1605 nd, err := cmdenv.GetNode(env)
1606 if err != nil {
1607 return err
1608 }
1609
1610 path, err := checkPath(req.Arguments[1])
1611 if err != nil {
1612 return err
1613 }
1614
1615 mode, err := strconv.ParseInt(req.Arguments[0], 8, 32)
1616 if err != nil {
1617 return err
1618 }
1619
1620 return mfs.Chmod(nd.FilesRoot, path, os.FileMode(mode))
1621 },
1622 }
1623
1624 var filesTouchCmd = &cmds.Command{
1625 Status: cmds.Experimental,
1626 Helptext: cmds.HelpText{
1627 Tagline: "Set or change optional POSIX modification times.",
1628 ShortDescription: `
1629 Examples:
1630 # set modification time to now.
1631 $ ipfs files touch /foo
1632 # set a custom modification time.
1633 $ ipfs files touch --mtime=1630937926 /foo
1634 `,
1635 },
1636 Arguments: []cmds.Argument{
1637 cmds.StringArg("path", true, false, "Path of target to update."),
1638 },
1639 Options: []cmds.Option{
1640 cmds.Int64Option(mtimeOptionName, "Modification time in seconds before or since the Unix Epoch to apply to created UnixFS entries."),
1641 cmds.UintOption(mtimeNsecsOptionName, "Modification time fraction in nanoseconds"),
1642 },
1643 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1644 nd, err := cmdenv.GetNode(env)
1645 if err != nil {
1646 return err
1647 }
1648
1649 path, err := checkPath(req.Arguments[0])
1650 if err != nil {
1651 return err
1652 }
1653
1654 mtime, _ := req.Options[mtimeOptionName].(int64)
1655 nsecs, _ := req.Options[mtimeNsecsOptionName].(uint)
1656
1657 var ts time.Time
1658 if mtime != 0 {
1659 ts = time.Unix(mtime, int64(nsecs)).UTC()
1660 } else {
1661 ts = time.Now().UTC()
1662 }
1663
1664 return mfs.Touch(nd.FilesRoot, path, ts)
1665 },
1666 }
1667
1668 const chrootConfirmOptionName = "confirm"
1669
1670 var filesChrootCmd = &cmds.Command{
1671 Status: cmds.Experimental,
1672 Helptext: cmds.HelpText{
1673 Tagline: "Change the MFS root CID.",
1674 ShortDescription: `
1675 'ipfs files chroot' changes the root CID used by MFS (Mutable File System).
1676 This is a recovery command for when MFS becomes corrupted and prevents the
1677 daemon from starting.
1678
1679 When run without a CID argument, resets MFS to an empty directory.
1680
1681 WARNING: The old MFS root and its unpinned children will be removed during
1682 the next garbage collection. Pin the old root first if you want to preserve.
1683
1684 This command can only run when the daemon is not running.
1685
1686 Examples:
1687
1688 # Reset MFS to empty directory (recovery from corruption)
1689 $ ipfs files chroot --confirm
1690
1691 # Restore MFS to a known good directory CID
1692 $ ipfs files chroot --confirm QmYourBackupCID
1693 `,
1694 },
1695 Arguments: []cmds.Argument{
1696 cmds.StringArg("cid", false, false, "New root CID (defaults to empty directory if not specified)."),
1697 },
1698 Options: []cmds.Option{
1699 cmds.BoolOption(chrootConfirmOptionName, "Confirm this potentially destructive operation."),
1700 },
1701 NoRemote: true,
1702 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
1703 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1704 confirm, _ := req.Options[chrootConfirmOptionName].(bool)
1705 if !confirm {
1706 return errors.New("this is a potentially destructive operation; pass --confirm to proceed")
1707 }
1708
1709 enc, err := cmdenv.GetCidEncoder(req)
1710 if err != nil {
1711 return err
1712 }
1713
1714 // Determine new root CID
1715 var newRootCid cid.Cid
1716 if len(req.Arguments) > 0 {
1717 var err error
1718 newRootCid, err = cid.Decode(req.Arguments[0])
1719 if err != nil {
1720 return fmt.Errorf("invalid CID %q: %w", req.Arguments[0], err)
1721 }
1722 } else {
1723 // Default to empty directory
1724 newRootCid = ft.EmptyDirNode().Cid()
1725 }
1726
1727 // Get config root to open repo directly
1728 cctx := env.(*oldcmds.Context)
1729 cfgRoot := cctx.ConfigRoot
1730
1731 // Open repo directly (daemon must not be running)
1732 repo, err := fsrepo.Open(cfgRoot)
1733 if err != nil {
1734 return fmt.Errorf("opening repo (is the daemon running?): %w", err)
1735 }
1736 defer repo.Close()
1737
1738 localDS := repo.Datastore()
1739 bs := bstore.NewBlockstore(localDS)
1740
1741 // Check new root exists locally and is a directory
1742 hasBlock, err := bs.Has(req.Context, newRootCid)
1743 if err != nil {
1744 return fmt.Errorf("checking if new root exists: %w", err)
1745 }
1746 if !hasBlock {
1747 // Special case: empty dir is always available (hardcoded in boxo)
1748 emptyDirCid := ft.EmptyDirNode().Cid()
1749 if !newRootCid.Equals(emptyDirCid) {
1750 return fmt.Errorf("new root %s does not exist locally; fetch it first with 'ipfs block get'", enc.Encode(newRootCid))
1751 }
1752 }
1753
1754 // Validate it's a directory (not a file)
1755 if hasBlock {
1756 blk, err := bs.Get(req.Context, newRootCid)
1757 if err != nil {
1758 return fmt.Errorf("reading new root block: %w", err)
1759 }
1760 pbNode, err := dag.DecodeProtobuf(blk.RawData())
1761 if err != nil {
1762 return fmt.Errorf("new root is not a valid dag-pb node: %w", err)
1763 }
1764 fsNode, err := ft.FSNodeFromBytes(pbNode.Data())
1765 if err != nil {
1766 return fmt.Errorf("new root is not a valid UnixFS node: %w", err)
1767 }
1768 if fsNode.Type() != ft.TDirectory && fsNode.Type() != ft.THAMTShard {
1769 return fmt.Errorf("new root must be a directory, got %s", fsNode.Type())
1770 }
1771 }
1772
1773 // Get old root for display (if exists)
1774 var oldRootStr string
1775 oldRootBytes, err := localDS.Get(req.Context, node.FilesRootDatastoreKey)
1776 if err == nil {
1777 oldRootCid, err := cid.Cast(oldRootBytes)
1778 if err == nil {
1779 oldRootStr = enc.Encode(oldRootCid)
1780 }
1781 } else if !errors.Is(err, datastore.ErrNotFound) {
1782 return fmt.Errorf("reading current MFS root: %w", err)
1783 }
1784
1785 // Write new root
1786 err = localDS.Put(req.Context, node.FilesRootDatastoreKey, newRootCid.Bytes())
1787 if err != nil {
1788 return fmt.Errorf("writing new MFS root: %w", err)
1789 }
1790
1791 // Build output message
1792 newRootStr := enc.Encode(newRootCid)
1793 var msg string
1794 if oldRootStr != "" {
1795 msg = fmt.Sprintf("MFS root changed from %s to %s\n", oldRootStr, newRootStr)
1796 msg += fmt.Sprintf("The old root %s will be garbage collected unless pinned.\n", oldRootStr)
1797 } else {
1798 msg = fmt.Sprintf("MFS root set to %s\n", newRootStr)
1799 }
1800
1801 return cmds.EmitOnce(res, &MessageOutput{Message: msg})
1802 },
1803 Type: MessageOutput{},
1804 Encoders: cmds.EncoderMap{
1805 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *MessageOutput) error {
1806 _, err := fmt.Fprint(w, out.Message)
1807 return err
1808 }),
1809 },
1810 }