From 263edb251eecfb375110bcacf170b193ddfea179 Mon Sep 17 00:00:00 2001 From: Andrew Gillis <11790789+gammazero@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:02:46 -0700 Subject: [PATCH] feat: Support storing UnixFS 1.5 Mode and ModTime (#10478) Co-authored-by: Marcin Rataj --- client/rpc/apifile.go | 123 +++++- client/rpc/unixfs.go | 19 +- core/commands/add.go | 120 +++++- core/commands/commands_test.go | 2 + core/commands/files.go | 213 ++++++++-- core/commands/get.go | 9 +- core/commands/ls.go | 7 + core/coreapi/unixfs.go | 6 + core/coreiface/options/unixfs.go | 55 +++ core/coreiface/unixfs.go | 16 +- core/coreunix/add.go | 55 ++- core/node/storage.go | 2 +- docs/changelogs/v0.30.md | 32 ++ docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 6 +- test/sharness/t0047-add-mode-mtime.sh | 513 +++++++++++++++++++++++++ test/sharness/t0250-files-api.sh | 4 + 21 files changed, 1105 insertions(+), 91 deletions(-) create mode 100755 test/sharness/t0047-add-mode-mtime.sh diff --git a/client/rpc/apifile.go b/client/rpc/apifile.go index 7a54995b181..57d82c5f712 100644 --- a/client/rpc/apifile.go +++ b/client/rpc/apifile.go @@ -1,10 +1,14 @@ package rpc import ( + "bytes" "context" "encoding/json" "fmt" "io" + "os" + "strconv" + "time" "github.com/ipfs/boxo/files" unixfs "github.com/ipfs/boxo/ipld/unixfs" @@ -24,20 +28,35 @@ func (api *UnixfsAPI) Get(ctx context.Context, p path.Path) (files.Node, error) } var stat struct { - Hash string - Type string - Size int64 // unixfs size + Hash string + Type string + Size int64 // unixfs size + Mode string + Mtime int64 + MtimeNsecs int } err := api.core().Request("files/stat", p.String()).Exec(ctx, &stat) if err != nil { return nil, err } + mode, err := stringToFileMode(stat.Mode) + if err != nil { + return nil, err + } + + var modTime time.Time + if stat.Mtime != 0 { + modTime = time.Unix(stat.Mtime, int64(stat.MtimeNsecs)).UTC() + } + switch stat.Type { case "file": - return api.getFile(ctx, p, stat.Size) + return api.getFile(ctx, p, stat.Size, mode, modTime) case "directory": - return api.getDir(ctx, p, stat.Size) + return api.getDir(ctx, p, stat.Size, mode, modTime) + case "symlink": + return api.getSymlink(ctx, p, modTime) default: return nil, fmt.Errorf("unsupported file type '%s'", stat.Type) } @@ -49,6 +68,9 @@ type apiFile struct { size int64 path path.Path + mode os.FileMode + mtime time.Time + r *Response at int64 } @@ -128,16 +150,37 @@ func (f *apiFile) Close() error { return nil } +func (f *apiFile) Mode() os.FileMode { + return f.mode +} + +func (f *apiFile) ModTime() time.Time { + return f.mtime +} + func (f *apiFile) Size() (int64, error) { return f.size, nil } -func (api *UnixfsAPI) getFile(ctx context.Context, p path.Path, size int64) (files.Node, error) { +func stringToFileMode(mode string) (os.FileMode, error) { + if mode == "" { + return 0, nil + } + mode64, err := strconv.ParseUint(mode, 8, 32) + if err != nil { + return 0, fmt.Errorf("cannot parse mode %s: %s", mode, err) + } + return os.FileMode(uint32(mode64)), nil +} + +func (api *UnixfsAPI) getFile(ctx context.Context, p path.Path, size int64, mode os.FileMode, mtime time.Time) (files.Node, error) { f := &apiFile{ - ctx: ctx, - core: api.core(), - size: size, - path: p, + ctx: ctx, + core: api.core(), + size: size, + path: p, + mode: mode, + mtime: mtime, } return f, f.reset() @@ -195,13 +238,19 @@ func (it *apiIter) Next() bool { switch it.cur.Type { case unixfs.THAMTShard, unixfs.TMetadata, unixfs.TDirectory: - it.curFile, err = it.core.getDir(it.ctx, path.FromCid(c), int64(it.cur.Size)) + it.curFile, err = it.core.getDir(it.ctx, path.FromCid(c), int64(it.cur.Size), it.cur.Mode, it.cur.ModTime) if err != nil { it.err = err return false } case unixfs.TFile: - it.curFile, err = it.core.getFile(it.ctx, path.FromCid(c), int64(it.cur.Size)) + it.curFile, err = it.core.getFile(it.ctx, path.FromCid(c), int64(it.cur.Size), it.cur.Mode, it.cur.ModTime) + if err != nil { + it.err = err + return false + } + case unixfs.TSymlink: + it.curFile, err = it.core.getSymlink(it.ctx, path.FromCid(c), it.cur.ModTime) if err != nil { it.err = err return false @@ -223,6 +272,9 @@ type apiDir struct { size int64 path path.Path + mode os.FileMode + mtime time.Time + dec *json.Decoder } @@ -230,6 +282,14 @@ func (d *apiDir) Close() error { return nil } +func (d *apiDir) Mode() os.FileMode { + return d.mode +} + +func (d *apiDir) ModTime() time.Time { + return d.mtime +} + func (d *apiDir) Size() (int64, error) { return d.size, nil } @@ -242,7 +302,7 @@ func (d *apiDir) Entries() files.DirIterator { } } -func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64) (files.Node, error) { +func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64, mode os.FileMode, modTime time.Time) (files.Node, error) { resp, err := api.core().Request("ls", p.String()). Option("resolve-size", true). Option("stream", true).Send(ctx) @@ -253,18 +313,43 @@ func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64) (file return nil, resp.Error } - d := &apiDir{ - ctx: ctx, - core: api, - size: size, - path: p, + data, _ := io.ReadAll(resp.Output) + rdr := bytes.NewReader(data) - dec: json.NewDecoder(resp.Output), + d := &apiDir{ + ctx: ctx, + core: api, + size: size, + path: p, + mode: mode, + mtime: modTime, + + //dec: json.NewDecoder(resp.Output), + dec: json.NewDecoder(rdr), } return d, nil } +func (api *UnixfsAPI) getSymlink(ctx context.Context, p path.Path, modTime time.Time) (files.Node, error) { + resp, err := api.core().Request("cat", p.String()). + Option("resolve-size", true). + Option("stream", true).Send(ctx) + if err != nil { + return nil, err + } + if resp.Error != nil { + return nil, resp.Error + } + + target, err := io.ReadAll(resp.Output) + if err != nil { + return nil, err + } + + return files.NewSymlinkFile(string(target), modTime), nil +} + var ( _ files.File = &apiFile{} _ files.Directory = &apiDir{} diff --git a/client/rpc/unixfs.go b/client/rpc/unixfs.go index 501e8d02511..3ba2c1c15f0 100644 --- a/client/rpc/unixfs.go +++ b/client/rpc/unixfs.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "io" + "os" + "time" "github.com/ipfs/boxo/files" unixfs "github.com/ipfs/boxo/ipld/unixfs" @@ -80,14 +82,13 @@ func (api *UnixfsAPI) Add(ctx context.Context, f files.Node, opts ...caopts.Unix } defer resp.Output.Close() dec := json.NewDecoder(resp.Output) -loop: + for { var evt addEvent - switch err := dec.Decode(&evt); err { - case nil: - case io.EOF: - break loop - default: + if err := dec.Decode(&evt); err != nil { + if errors.Is(err, io.EOF) { + break + } return path.ImmutablePath{}, err } out = evt @@ -129,6 +130,9 @@ type lsLink struct { Size uint64 Type unixfs_pb.Data_DataType Target string + + Mode os.FileMode + ModTime time.Time } type lsObject struct { @@ -222,6 +226,9 @@ func (api *UnixfsAPI) Ls(ctx context.Context, p path.Path, opts ...caopts.Unixfs Size: l0.Size, Type: ftype, Target: l0.Target, + + Mode: l0.Mode, + ModTime: l0.ModTime, }: case <-ctx.Done(): } diff --git a/core/commands/add.go b/core/commands/add.go index 94a5a0f51f5..90861302551 100644 --- a/core/commands/add.go +++ b/core/commands/add.go @@ -6,7 +6,9 @@ import ( "io" "os" gopath "path" + "strconv" "strings" + "time" "github.com/ipfs/kubo/config" "github.com/ipfs/kubo/core/commands/cmdenv" @@ -25,11 +27,31 @@ import ( // ErrDepthLimitExceeded indicates that the max depth has been exceeded. var ErrDepthLimitExceeded = fmt.Errorf("depth limit exceeded") +type TimeParts struct { + t *time.Time +} + +func (t TimeParts) MarshalJSON() ([]byte, error) { + return t.t.MarshalJSON() +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// The time is expected to be a quoted string in RFC 3339 format. +func (t *TimeParts) UnmarshalJSON(data []byte) (err error) { + // Fractional seconds are handled implicitly by Parse. + tt, err := time.Parse("\"2006-01-02T15:04:05Z\"", string(data)) + *t = TimeParts{&tt} + return +} + type AddEvent struct { - Name string - Hash string `json:",omitempty"` - Bytes int64 `json:",omitempty"` - Size string `json:",omitempty"` + Name string + Hash string `json:",omitempty"` + Bytes int64 `json:",omitempty"` + Size string `json:",omitempty"` + Mode string `json:",omitempty"` + Mtime int64 `json:",omitempty"` + MtimeNsecs int `json:",omitempty"` } const ( @@ -50,6 +72,12 @@ const ( inlineOptionName = "inline" inlineLimitOptionName = "inline-limit" toFilesOptionName = "to-files" + + preserveModeOptionName = "preserve-mode" + preserveMtimeOptionName = "preserve-mtime" + modeOptionName = "mode" + mtimeOptionName = "mtime" + mtimeNsecsOptionName = "mtime-nsecs" ) const adderOutChanSize = 8 @@ -166,22 +194,24 @@ See 'dag export' and 'dag import' for more information. cmds.IntOption(inlineLimitOptionName, "Maximum block size to inline. (experimental)").WithDefault(32), cmds.BoolOption(pinOptionName, "Pin locally to protect added files from garbage collection.").WithDefault(true), cmds.StringOption(toFilesOptionName, "Add reference to Files API (MFS) at the provided path."), + cmds.BoolOption(preserveModeOptionName, "Apply existing POSIX permissions to created UnixFS entries. Disables raw-leaves. (experimental)"), + cmds.BoolOption(preserveMtimeOptionName, "Apply existing POSIX modification time to created UnixFS entries. Disables raw-leaves. (experimental)"), + cmds.UintOption(modeOptionName, "Custom POSIX file mode to store in created UnixFS entries. Disables raw-leaves. (experimental)"), + cmds.Int64Option(mtimeOptionName, "Custom POSIX modification time to store in created UnixFS entries (seconds before or after the Unix Epoch). Disables raw-leaves. (experimental)"), + cmds.UintOption(mtimeNsecsOptionName, "Custom POSIX modification time (optional time fraction in nanoseconds)"), }, PreRun: func(req *cmds.Request, env cmds.Environment) error { quiet, _ := req.Options[quietOptionName].(bool) quieter, _ := req.Options[quieterOptionName].(bool) quiet = quiet || quieter - silent, _ := req.Options[silentOptionName].(bool) - if quiet || silent { - return nil - } - - // ipfs cli progress bar defaults to true unless quiet or silent is used - _, found := req.Options[progressOptionName].(bool) - if !found { - req.Options[progressOptionName] = true + if !quiet && !silent { + // ipfs cli progress bar defaults to true unless quiet or silent is used + _, found := req.Options[progressOptionName].(bool) + if !found { + req.Options[progressOptionName] = true + } } return nil @@ -217,6 +247,11 @@ See 'dag export' and 'dag import' for more information. inline, _ := req.Options[inlineOptionName].(bool) inlineLimit, _ := req.Options[inlineLimitOptionName].(int) toFilesStr, toFilesSet := req.Options[toFilesOptionName].(string) + preserveMode, _ := req.Options[preserveModeOptionName].(bool) + preserveMtime, _ := req.Options[preserveMtimeOptionName].(bool) + mode, _ := req.Options[modeOptionName].(uint) + mtime, _ := req.Options[mtimeOptionName].(int64) + mtimeNsecs, _ := req.Options[mtimeNsecsOptionName].(uint) if chunker == "" { chunker = cfg.Import.UnixFSChunker.WithDefault(config.DefaultUnixFSChunker) @@ -236,6 +271,19 @@ See 'dag export' and 'dag import' for more information. rawblks = cfg.Import.UnixFSRawLeaves.WithDefault(config.DefaultUnixFSRawLeaves) } + // Storing optional mode or mtime (UnixFS 1.5) requires root block + // to always be 'dag-pb' and not 'raw'. Below adjusts raw-leaves setting, if possible. + if preserveMode || preserveMtime || mode != 0 || mtime != 0 { + // Error if --raw-leaves flag was explicitly passed by the user. + // (let user make a decision to manually disable it and retry) + if rbset && rawblks { + return fmt.Errorf("%s can't be used with UnixFS metadata like mode or modification time", rawLeavesOptionName) + } + // No explicit preference from user, disable raw-leaves and continue + rbset = true + rawblks = false + } + if onlyHash && toFilesSet { return fmt.Errorf("%s and %s options are not compatible", onlyHashOptionName, toFilesOptionName) } @@ -272,6 +320,19 @@ See 'dag export' and 'dag import' for more information. options.Unixfs.Progress(progress), options.Unixfs.Silent(silent), + + options.Unixfs.PreserveMode(preserveMode), + options.Unixfs.PreserveMtime(preserveMtime), + } + + if mode != 0 { + opts = append(opts, options.Unixfs.Mode(os.FileMode(mode))) + } + + if mtime != 0 { + opts = append(opts, options.Unixfs.Mtime(mtime, uint32(mtimeNsecs))) + } else if mtimeNsecs != 0 { + return fmt.Errorf("option %q requires %q to be provided as well", mtimeNsecsOptionName, mtimeOptionName) } if cidVerSet { @@ -383,12 +444,33 @@ See 'dag export' and 'dag import' for more information. output.Name = gopath.Join(addit.Name(), output.Name) } - if err := res.Emit(&AddEvent{ - Name: output.Name, - Hash: h, - Bytes: output.Bytes, - Size: output.Size, - }); err != nil { + output.Mode = addit.Node().Mode() + if ts := addit.Node().ModTime(); !ts.IsZero() { + output.Mtime = addit.Node().ModTime().Unix() + output.MtimeNsecs = addit.Node().ModTime().Nanosecond() + } + + addEvent := AddEvent{ + Name: output.Name, + Hash: h, + Bytes: output.Bytes, + Size: output.Size, + Mtime: output.Mtime, + MtimeNsecs: output.MtimeNsecs, + } + + if output.Mode != 0 { + addEvent.Mode = "0" + strconv.FormatUint(uint64(output.Mode), 8) + } + + if output.Mtime > 0 { + addEvent.Mtime = output.Mtime + if output.MtimeNsecs > 0 { + addEvent.MtimeNsecs = output.MtimeNsecs + } + } + + if err := res.Emit(&addEvent); err != nil { return err } } diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index 018b6734e79..b04a5459b50 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -89,6 +89,8 @@ func TestCommands(t *testing.T) { "/files/rm", "/files/stat", "/files/write", + "/files/chmod", + "/files/touch", "/filestore", "/filestore/dups", "/filestore/ls", diff --git a/core/commands/files.go b/core/commands/files.go index 12891a7301d..6add671ce87 100644 --- a/core/commands/files.go +++ b/core/commands/files.go @@ -2,13 +2,16 @@ package commands import ( "context" + "encoding/json" "errors" "fmt" "io" "os" gopath "path" "sort" + "strconv" "strings" + "time" humanize "github.com/dustin/go-humanize" "github.com/ipfs/kubo/config" @@ -81,6 +84,8 @@ operations. "rm": filesRmCmd, "flush": filesFlushCmd, "chcid": filesChcidCmd, + "chmod": filesChmodCmd, + "touch": filesTouchCmd, }, } @@ -105,6 +110,43 @@ type statOutput struct { WithLocality bool `json:",omitempty"` Local bool `json:",omitempty"` SizeLocal uint64 `json:",omitempty"` + Mode uint32 `json:",omitempty"` + Mtime int64 `json:",omitempty"` + MtimeNsecs int `json:",omitempty"` +} + +func (s *statOutput) MarshalJSON() ([]byte, error) { + type so statOutput + out := &struct { + *so + Mode string `json:",omitempty"` + }{so: (*so)(s)} + + if s.Mode != 0 { + out.Mode = fmt.Sprintf("%04o", s.Mode) + } + return json.Marshal(out) +} + +func (s *statOutput) UnmarshalJSON(data []byte) error { + var err error + type so statOutput + tmp := &struct { + *so + Mode string `json:",omitempty"` + }{so: (*so)(s)} + + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + + if tmp.Mode != "" { + mode, err := strconv.ParseUint(tmp.Mode, 8, 32) + if err == nil { + s.Mode = uint32(mode) + } + } + return err } const ( @@ -112,10 +154,13 @@ const ( Size: CumulativeSize: ChildBlocks: -Type: ` +Type: +Mode: () +Mtime: ` filesFormatOptionName = "format" filesSizeOptionName = "size" filesWithLocalOptionName = "with-local" + filesStatUnspecified = "not set" ) var filesStatCmd = &cmds.Command{ @@ -128,7 +173,8 @@ var filesStatCmd = &cmds.Command{ }, Options: []cmds.Option{ cmds.StringOption(filesFormatOptionName, "Print statistics in given format. Allowed tokens: "+ - " . Conflicts with other format options.").WithDefault(defaultStatFormat), + " and optional ."+ + "Conflicts with other format options.").WithDefault(defaultStatFormat), cmds.BoolOption(filesHashOptionName, "Print only hash. Implies '--format='. Conflicts with other format options."), cmds.BoolOption(filesSizeOptionName, "Print only size. Implies '--format='. Conflicts with other format options."), cmds.BoolOption(filesWithLocalOptionName, "Compute the amount of the dag that is local, and if possible the total size"), @@ -199,12 +245,29 @@ var filesStatCmd = &cmds.Command{ }, Encoders: cmds.EncoderMap{ cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *statOutput) error { + mode, modeo := filesStatUnspecified, filesStatUnspecified + if out.Mode != 0 { + mode = strings.ToLower(os.FileMode(out.Mode).String()) + modeo = "0" + strconv.FormatInt(int64(out.Mode&0x1FF), 8) + } + mtime, mtimes, mtimens := filesStatUnspecified, filesStatUnspecified, filesStatUnspecified + if out.Mtime > 0 { + mtime = time.Unix(out.Mtime, int64(out.MtimeNsecs)).UTC().Format("2 Jan 2006, 15:04:05 MST") + mtimes = strconv.FormatInt(out.Mtime, 10) + mtimens = strconv.Itoa(out.MtimeNsecs) + } + s, _ := statGetFormatOptions(req) s = strings.Replace(s, "", out.Hash, -1) s = strings.Replace(s, "", fmt.Sprintf("%d", out.Size), -1) s = strings.Replace(s, "", fmt.Sprintf("%d", out.CumulativeSize), -1) s = strings.Replace(s, "", fmt.Sprintf("%d", out.Blocks), -1) s = strings.Replace(s, "", out.Type, -1) + s = strings.Replace(s, "", mode, -1) + s = strings.Replace(s, "", modeo, -1) + s = strings.Replace(s, "", mtime, -1) + s = strings.Replace(s, "", mtimes, -1) + s = strings.Replace(s, "", mtimens, -1) fmt.Fprintln(w, s) @@ -254,28 +317,7 @@ func statNode(nd ipld.Node, enc cidenc.Encoder) (*statOutput, error) { switch n := nd.(type) { case *dag.ProtoNode: - d, err := ft.FSNodeFromBytes(n.Data()) - if err != nil { - return nil, err - } - - var ndtype string - switch d.Type() { - case ft.TDirectory, ft.THAMTShard: - ndtype = "directory" - case ft.TFile, ft.TMetadata, ft.TRaw: - ndtype = "file" - default: - return nil, fmt.Errorf("unrecognized node type: %s", d.Type()) - } - - return &statOutput{ - Hash: enc.Encode(c), - Blocks: len(nd.Links()), - Size: d.FileSize(), - CumulativeSize: cumulsize, - Type: ndtype, - }, nil + return statProtoNode(n, enc, c, cumulsize) case *dag.RawNode: return &statOutput{ Hash: enc.Encode(c), @@ -289,6 +331,44 @@ func statNode(nd ipld.Node, enc cidenc.Encoder) (*statOutput, error) { } } +func statProtoNode(n *dag.ProtoNode, enc cidenc.Encoder, cid cid.Cid, cumulsize uint64) (*statOutput, error) { + d, err := ft.FSNodeFromBytes(n.Data()) + if err != nil { + return nil, err + } + + stat := statOutput{ + Hash: enc.Encode(cid), + Blocks: len(n.Links()), + Size: d.FileSize(), + CumulativeSize: cumulsize, + } + + switch d.Type() { + case ft.TDirectory, ft.THAMTShard: + stat.Type = "directory" + case ft.TFile, ft.TSymlink, ft.TMetadata, ft.TRaw: + stat.Type = "file" + default: + return nil, fmt.Errorf("unrecognized node type: %s", d.Type()) + } + + if mode := d.Mode(); mode != 0 { + stat.Mode = uint32(mode) + } else if d.Type() == ft.TSymlink { + stat.Mode = uint32(os.ModeSymlink | 0x1FF) + } + + if mt := d.ModTime(); !mt.IsZero() { + stat.Mtime = mt.Unix() + if ns := mt.Nanosecond(); ns > 0 { + stat.MtimeNsecs = ns + } + } + + return &stat, nil +} + 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())) @@ -341,7 +421,7 @@ $ ipfs add --quieter --pin=false $ ipfs files cp /ipfs/ /your/desired/mfs/path If you wish to fully copy content from a different IPFS peer into MFS, do not -forget to force IPFS to fetch to full DAG after doing the "cp" operation. i.e: +forget to force IPFS to fetch the full DAG after doing a "cp" operation. i.e: $ ipfs files cp /ipfs/ /your/desired/mfs/path $ ipfs pin add @@ -1313,3 +1393,86 @@ func getParentDir(root *mfs.Root, dir string) (*mfs.Directory, error) { } return pdir, nil } + +var filesChmodCmd = &cmds.Command{ + Status: cmds.Experimental, + Helptext: cmds.HelpText{ + Tagline: "Change optional POSIX mode permissions", + ShortDescription: ` +The mode argument must be specified in Unix numeric notation. + + $ ipfs files chmod 0644 /foo + $ ipfs files stat /foo + ... + Type: file + Mode: -rw-r--r-- (0644) + ... +`, + }, + Arguments: []cmds.Argument{ + cmds.StringArg("mode", true, false, "Mode to apply to node (numeric notation)"), + cmds.StringArg("path", true, false, "Path to apply mode"), + }, + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + nd, err := cmdenv.GetNode(env) + if err != nil { + return err + } + + path, err := checkPath(req.Arguments[1]) + if err != nil { + return err + } + + mode, err := strconv.ParseInt(req.Arguments[0], 8, 32) + if err != nil { + return err + } + + return mfs.Chmod(nd.FilesRoot, path, os.FileMode(mode)) + }, +} + +var filesTouchCmd = &cmds.Command{ + Status: cmds.Experimental, + Helptext: cmds.HelpText{ + Tagline: "Set or change optional POSIX modification times.", + ShortDescription: ` +Examples: + # set modification time to now. + $ ipfs files touch /foo + # set a custom modification time. + $ ipfs files touch --mtime=1630937926 /foo +`, + }, + Arguments: []cmds.Argument{ + cmds.StringArg("path", true, false, "Path of target to update."), + }, + Options: []cmds.Option{ + cmds.Int64Option(mtimeOptionName, "Modification time in seconds before or since the Unix Epoch to apply to created UnixFS entries."), + cmds.UintOption(mtimeNsecsOptionName, "Modification time fraction in nanoseconds"), + }, + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + nd, err := cmdenv.GetNode(env) + if err != nil { + return err + } + + path, err := checkPath(req.Arguments[0]) + if err != nil { + return err + } + + mtime, _ := req.Options[mtimeOptionName].(int64) + nsecs, _ := req.Options[mtimeNsecsOptionName].(uint) + + var ts time.Time + if mtime != 0 { + ts = time.Unix(mtime, int64(nsecs)).UTC() + } else { + ts = time.Now().UTC() + } + + return mfs.Touch(nd.FilesRoot, path, ts) + }, +} diff --git a/core/commands/get.go b/core/commands/get.go index 5b64c281bbd..12a3ea8ca26 100644 --- a/core/commands/get.go +++ b/core/commands/get.go @@ -1,6 +1,7 @@ package commands import ( + gotar "archive/tar" "bufio" "compress/gzip" "errors" @@ -331,7 +332,8 @@ func fileArchive(f files.Node, name string, archive bool, compression int) (io.R closeGzwAndPipe() // everything seems to be ok }() } else { - // the case for 1. archive, and 2. not archived and not compressed, in which tar is used anyway as a transport format + // the case for 1. archive, and 2. not archived and not compressed, in + // which tar is used anyway as a transport format // construct the tar writer w, err := files.NewTarWriter(maybeGzw) @@ -339,6 +341,11 @@ func fileArchive(f files.Node, name string, archive bool, compression int) (io.R return nil, err } + // if not creating an archive set the format to PAX in order to preserve nanoseconds + if !archive { + w.SetFormat(gotar.FormatPAX) + } + go func() { // write all the nodes recursively if err := w.WriteFile(f, filename); checkErrAndClosePipe(err) { diff --git a/core/commands/ls.go b/core/commands/ls.go index 6fd53528271..ab914bb0e15 100644 --- a/core/commands/ls.go +++ b/core/commands/ls.go @@ -6,6 +6,7 @@ import ( "os" "sort" "text/tabwriter" + "time" cmdenv "github.com/ipfs/kubo/core/commands/cmdenv" "github.com/ipfs/kubo/core/commands/cmdutils" @@ -23,6 +24,8 @@ type LsLink struct { Size uint64 Type unixfs_pb.Data_DataType Target string + Mode os.FileMode + ModTime time.Time } // LsObject is an element of LsOutput @@ -163,6 +166,9 @@ The JSON output contains type information. Size: link.Size, Type: ftype, Target: link.Target, + + Mode: link.Mode, + ModTime: link.ModTime, } if err := processLink(paths[i], lsLink); err != nil { return err @@ -256,6 +262,7 @@ func tabularOutput(req *cmds.Request, w io.Writer, out *LsOutput, lastObjectHash } } + // TODO: Print link.Mode and link.ModTime? fmt.Fprintf(tw, s, link.Hash, link.Size, cmdenv.EscNonPrint(link.Name)) } } diff --git a/core/coreapi/unixfs.go b/core/coreapi/unixfs.go index 860574945d7..e175488f37f 100644 --- a/core/coreapi/unixfs.go +++ b/core/coreapi/unixfs.go @@ -130,6 +130,10 @@ func (api *UnixfsAPI) Add(ctx context.Context, files files.Node, opts ...options fileAdder.RawLeaves = settings.RawLeaves fileAdder.NoCopy = settings.NoCopy fileAdder.CidBuilder = prefix + fileAdder.PreserveMode = settings.PreserveMode + fileAdder.PreserveMtime = settings.PreserveMtime + fileAdder.FileMode = settings.Mode + fileAdder.FileMtime = settings.Mtime switch settings.Layout { case options.BalancedLayout: @@ -270,6 +274,8 @@ func (api *UnixfsAPI) processLink(ctx context.Context, linkres ft.LinkResult, se if !settings.UseCumulativeSize { lnk.Size = d.FileSize() } + lnk.Mode = d.Mode() + lnk.ModTime = d.ModTime() } } diff --git a/core/coreiface/options/unixfs.go b/core/coreiface/options/unixfs.go index f00fffb87b0..c837ec1b2db 100644 --- a/core/coreiface/options/unixfs.go +++ b/core/coreiface/options/unixfs.go @@ -3,6 +3,8 @@ package options import ( "errors" "fmt" + "os" + "time" dag "github.com/ipfs/boxo/ipld/merkledag" cid "github.com/ipfs/go-cid" @@ -36,6 +38,11 @@ type UnixfsAddSettings struct { Events chan<- interface{} Silent bool Progress bool + + PreserveMode bool + PreserveMtime bool + Mode os.FileMode + Mtime time.Time } type UnixfsLsSettings struct { @@ -69,6 +76,11 @@ func UnixfsAddOptions(opts ...UnixfsAddOption) (*UnixfsAddSettings, cid.Prefix, Events: nil, Silent: false, Progress: false, + + PreserveMode: false, + PreserveMtime: false, + Mode: 0, + Mtime: time.Time{}, } for _, opt := range opts { @@ -106,6 +118,14 @@ func UnixfsAddOptions(opts ...UnixfsAddOption) (*UnixfsAddSettings, cid.Prefix, } } + if !options.Mtime.IsZero() && options.PreserveMtime { + options.PreserveMtime = false + } + + if options.Mode != 0 && options.PreserveMode { + options.PreserveMode = false + } + // cidV1 -> raw blocks (by default) if options.CidVersion > 0 && !options.RawLeavesSet { options.RawLeaves = true @@ -293,3 +313,38 @@ func (unixfsOpts) UseCumulativeSize(use bool) UnixfsLsOption { return nil } } + +// PreserveMode tells the adder to store the file permissions +func (unixfsOpts) PreserveMode(enable bool) UnixfsAddOption { + return func(settings *UnixfsAddSettings) error { + settings.PreserveMode = enable + return nil + } +} + +// PreserveMtime tells the adder to store the file modification time +func (unixfsOpts) PreserveMtime(enable bool) UnixfsAddOption { + return func(settings *UnixfsAddSettings) error { + settings.PreserveMtime = enable + return nil + } +} + +// Mode represents a unix file mode +func (unixfsOpts) Mode(mode os.FileMode) UnixfsAddOption { + return func(settings *UnixfsAddSettings) error { + settings.Mode = mode + return nil + } +} + +// Mtime represents a unix file mtime +func (unixfsOpts) Mtime(seconds int64, nsecs uint32) UnixfsAddOption { + return func(settings *UnixfsAddSettings) error { + if nsecs > 999999999 { + return errors.New("mtime nanoseconds must be in range [1, 999999999]") + } + settings.Mtime = time.Unix(seconds, int64(nsecs)) + return nil + } +} diff --git a/core/coreiface/unixfs.go b/core/coreiface/unixfs.go index d0dc4d8ce09..c0150bd12c6 100644 --- a/core/coreiface/unixfs.go +++ b/core/coreiface/unixfs.go @@ -2,6 +2,8 @@ package iface import ( "context" + "os" + "time" "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/path" @@ -10,10 +12,13 @@ import ( ) type AddEvent struct { - Name string - Path path.ImmutablePath `json:",omitempty"` - Bytes int64 `json:",omitempty"` - Size string `json:",omitempty"` + Name string + Path path.ImmutablePath `json:",omitempty"` + Bytes int64 `json:",omitempty"` + Size string `json:",omitempty"` + Mode os.FileMode `json:",omitempty"` + Mtime int64 `json:",omitempty"` + MtimeNsecs int `json:",omitempty"` } // FileType is an enum of possible UnixFS file types. @@ -56,6 +61,9 @@ type DirEntry struct { Type FileType // The type of the file. Target string // The symlink target (if a symlink). + Mode os.FileMode + ModTime time.Time + Err error } diff --git a/core/coreunix/add.go b/core/coreunix/add.go index a8d7e5982f0..5f7cbb61065 100644 --- a/core/coreunix/add.go +++ b/core/coreunix/add.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "io" + "os" gopath "path" "strconv" + "time" bstore "github.com/ipfs/boxo/blockstore" chunker "github.com/ipfs/boxo/chunker" @@ -81,6 +83,11 @@ type Adder struct { tempRoot cid.Cid CidBuilder cid.Builder liveNodes uint64 + + PreserveMode bool + PreserveMtime bool + FileMode os.FileMode + FileMtime time.Time } func (adder *Adder) mfsRoot() (*mfs.Root, error) { @@ -113,11 +120,13 @@ func (adder *Adder) add(reader io.Reader) (ipld.Node, error) { } params := ihelper.DagBuilderParams{ - Dagserv: adder.bufferedDS, - RawLeaves: adder.RawLeaves, - Maxlinks: ihelper.DefaultLinksPerBlock, - NoCopy: adder.NoCopy, - CidBuilder: adder.CidBuilder, + Dagserv: adder.bufferedDS, + RawLeaves: adder.RawLeaves, + Maxlinks: ihelper.DefaultLinksPerBlock, + NoCopy: adder.NoCopy, + CidBuilder: adder.CidBuilder, + FileMode: adder.FileMode, + FileModTime: adder.FileMtime, } db, err := params.New(chnk) @@ -359,6 +368,14 @@ func (adder *Adder) addFileNode(ctx context.Context, path string, file files.Nod return err } + if adder.PreserveMtime { + adder.FileMtime = file.ModTime() + } + + if adder.PreserveMode { + adder.FileMode = file.Mode() + } + if adder.liveNodes >= liveCacheSize { // TODO: A smarter cache that uses some sort of lru cache with an eviction handler mr, err := adder.mfsRoot() @@ -391,6 +408,18 @@ func (adder *Adder) addSymlink(path string, l *files.Symlink) error { return err } + if !adder.FileMtime.IsZero() { + fsn, err := unixfs.FSNodeFromBytes(sdata) + if err != nil { + return err + } + + fsn.SetModTime(adder.FileMtime) + if sdata, err = fsn.GetBytes(); err != nil { + return err + } + } + dagnode := dag.NodeWithData(sdata) err = dagnode.SetCidBuilder(adder.CidBuilder) if err != nil { @@ -429,6 +458,20 @@ func (adder *Adder) addFile(path string, file files.File) error { func (adder *Adder) addDir(ctx context.Context, path string, dir files.Directory, toplevel bool) error { log.Infof("adding directory: %s", path) + // if we need to store mode or modification time then create a new root which includes that data + if toplevel && (adder.FileMode != 0 || !adder.FileMtime.IsZero()) { + nd := unixfs.EmptyDirNodeWithStat(adder.FileMode, adder.FileMtime) + err := nd.SetCidBuilder(adder.CidBuilder) + if err != nil { + return err + } + mr, err := mfs.NewRoot(ctx, adder.dagService, nd, nil) + if err != nil { + return err + } + adder.SetMfsRoot(mr) + } + if !(toplevel && path == "") { mr, err := adder.mfsRoot() if err != nil { @@ -438,6 +481,8 @@ func (adder *Adder) addDir(ctx context.Context, path string, dir files.Directory Mkparents: true, Flush: false, CidBuilder: adder.CidBuilder, + Mode: adder.FileMode, + ModTime: adder.FileMtime, }) if err != nil { return err diff --git a/core/node/storage.go b/core/node/storage.go index a303ddc23a3..aedf0ee6a1d 100644 --- a/core/node/storage.go +++ b/core/node/storage.go @@ -56,7 +56,7 @@ func GcBlockstoreCtor(bb BaseBlocks) (gclocker blockstore.GCLocker, gcbs blockst return } -// GcBlockstoreCtor wraps GcBlockstore and adds Filestore support +// FilestoreBlockstoreCtor wraps GcBlockstore and adds Filestore support func FilestoreBlockstoreCtor(repo repo.Repo, bb BaseBlocks) (gclocker blockstore.GCLocker, gcbs blockstore.GCBlockstore, bs blockstore.Blockstore, fstore *filestore.Filestore) { gclocker = blockstore.NewGCLocker() diff --git a/docs/changelogs/v0.30.md b/docs/changelogs/v0.30.md index 7fe13e19eca..6cc212f7a8b 100644 --- a/docs/changelogs/v0.30.md +++ b/docs/changelogs/v0.30.md @@ -12,6 +12,7 @@ - [Version Suffix Configuration](#version-suffix-configuration) - [`/unix/` socket support in `Addresses.API`](#unix-socket-support-in-addressesapi) - [Cleaned Up `ipfs daemon` Startup Log](#cleaned-up-ipfs-daemon-startup-log) + - [UnixFS 1.5: Mode and Modification Time Support](#unixfs-15-mode-and-modification-time-support) - [๐Ÿ“ Changelog](#-changelog) - [๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors](#-contributors) @@ -96,6 +97,37 @@ The previous lengthy listing of all listener and announced multiaddrs has been r The output now features a simplified list of swarm listeners, displayed in the format `host:port (TCP+UDP)`, which provides essential information for debugging connectivity issues, particularly related to port forwarding. Announced libp2p addresses are no longer printed on startup, because libp2p may change or augument them based on AutoNAT, relay, and UPnP state. Instead, users are prompted to run `ipfs id` to obtain up-to-date list of listeners and announced multiaddrs in libp2p format. +#### UnixFS 1.5: Mode and Modification Time Support + +Kubo now allows users to opt-in to store mode and modification time for files, directories, and symbolic links. +By default, if you do not opt-in, the old behavior remains unchanged, and the same CIDs will be generated as before. + +The `ipfs add` CLI options `--preserve-mode` and `--preserve-mtime` can be used to store the original mode and last modified time of the file being added, and `ipfs files stat /ipfs/CID` can be used for inspecting these optional attributes: + +```console +$ touch ./file +$ chmod 654 ./file +$ ipfs add --preserve-mode --preserve-mtime -Q ./file +QmczQr4XS1rRnWVopyg5Chr9EQ7JKpbhgnrjpb5kTQ1DKQ + +$ ipfs files stat /ipfs/QmczQr4XS1rRnWVopyg5Chr9EQ7JKpbhgnrjpb5kTQ1DKQ +QmczQr4XS1rRnWVopyg5Chr9EQ7JKpbhgnrjpb5kTQ1DKQ +Size: 0 +CumulativeSize: 22 +ChildBlocks: 0 +Type: file +Mode: -rw-r-xr-- (0654) +Mtime: 13 Aug 2024, 21:15:31 UTC +``` + +The CLI and HTTP RPC options `--mode`, `--mtime` and `--mtime-nsecs` can be used to set them to arbitrary values. + +Opt-in support for `mode` and `mtime` was also added to MFS (`ipfs files --help`). For more information see `--help` text of `ipfs files touch|stat|chmod` commands. + + +> [!NOTE] +> Storing `mode` and `mtime` requires root block to be `dag-pb` and disabled `raw-leaves` setting to create envelope for storing the metadata. + ### ๐Ÿ“ Changelog ### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index c51eb189dfb..79d8d4600be 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -9,7 +9,7 @@ toolchain go1.22.0 replace github.com/ipfs/kubo => ./../../.. require ( - github.com/ipfs/boxo v0.22.0 + github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 github.com/libp2p/go-libp2p v0.36.2 github.com/multiformats/go-multiaddr v0.13.0 diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 71d5bd54a68..2d2f68eb709 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -266,8 +266,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7Uy github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c/go.mod h1:6EekK/jo+TynwSE/ZOiOJd4eEvRXoavEC3vquKtv4yI= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.22.0 h1:QTC+P5uhsBNq6HzX728nsLyFW6rYDeR/5hggf9YZX78= -github.com/ipfs/boxo v0.22.0/go.mod h1:yp1loimX0BDYOR0cyjtcXHv15muEh5V1FqO2QLlzykw= +github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 h1:rW0xGaZW9+74cc8etCm6DwrHhIEtNxklFn8YrUaWjx4= +github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053/go.mod h1:bMB1tnSTr+6/CS5p3jkS4rtifpl+ul6P4ZgeTZn8Ty0= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= diff --git a/go.mod b/go.mod index ee336089e9f..65e2ac2955a 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/ipfs-shipyard/nopfs v0.0.12 github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c - github.com/ipfs/boxo v0.22.0 + github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 github.com/ipfs/go-block-format v0.2.0 github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-cidutil v0.1.0 diff --git a/go.sum b/go.sum index 05be97779ce..f5a27bc2efe 100644 --- a/go.sum +++ b/go.sum @@ -330,8 +330,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7Uy github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c/go.mod h1:6EekK/jo+TynwSE/ZOiOJd4eEvRXoavEC3vquKtv4yI= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.22.0 h1:QTC+P5uhsBNq6HzX728nsLyFW6rYDeR/5hggf9YZX78= -github.com/ipfs/boxo v0.22.0/go.mod h1:yp1loimX0BDYOR0cyjtcXHv15muEh5V1FqO2QLlzykw= +github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 h1:rW0xGaZW9+74cc8etCm6DwrHhIEtNxklFn8YrUaWjx4= +github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053/go.mod h1:bMB1tnSTr+6/CS5p3jkS4rtifpl+ul6P4ZgeTZn8Ty0= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 277b31541c7..a263e3d9860 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -113,7 +113,7 @@ require ( github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/bbloom v0.0.4 // indirect - github.com/ipfs/boxo v0.22.0 // indirect + github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 // indirect github.com/ipfs/go-block-format v0.2.0 // indirect github.com/ipfs/go-cid v0.4.1 // indirect github.com/ipfs/go-datastore v0.6.0 // indirect diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index 8553ddb6ee5..27876d29f93 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -280,8 +280,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.22.0 h1:QTC+P5uhsBNq6HzX728nsLyFW6rYDeR/5hggf9YZX78= -github.com/ipfs/boxo v0.22.0/go.mod h1:yp1loimX0BDYOR0cyjtcXHv15muEh5V1FqO2QLlzykw= +github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 h1:rW0xGaZW9+74cc8etCm6DwrHhIEtNxklFn8YrUaWjx4= +github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053/go.mod h1:bMB1tnSTr+6/CS5p3jkS4rtifpl+ul6P4ZgeTZn8Ty0= github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= @@ -292,8 +292,6 @@ github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0M github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= -github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= -github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE= diff --git a/test/sharness/t0047-add-mode-mtime.sh b/test/sharness/t0047-add-mode-mtime.sh new file mode 100755 index 00000000000..520c692f3bc --- /dev/null +++ b/test/sharness/t0047-add-mode-mtime.sh @@ -0,0 +1,513 @@ +#!/usr/bin/env bash + +test_description="Test storing and retrieving mode and mtime" + +. lib/test-lib.sh + +test_init_ipfs + +test_expect_success "set Import defaults to ensure deterministic cids for mod and mtime tests" ' + ipfs config --json Import.CidVersion 0 && + ipfs config Import.HashFunction sha2-256 && + ipfs config Import.UnixFSChunker size-262144 +' + +HASH_NO_PRESERVE=QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH + +PRESERVE_MTIME=1604320482 +PRESERVE_MODE="0640" +HASH_PRESERVE_MODE=QmQLgxypSNGNFTuUPGCecq6dDEjb6hNB5xSyVmP3cEuNtq +HASH_PRESERVE_MTIME=QmQ6kErEW8kztQFV8vbwNU8E4dmtGsYpRiboiLxUEwibvj +HASH_PRESERVE_LINK_MTIME=QmbJwotgtr84JxcnjpwJ86uZiyMoxbZuNH4YrdJMypkYaB +HASH_PRESERVE_MODE_AND_MTIME=QmYkvboLsvLFcSYmqVJRxvBdYRQLroLv9kELf3LRiCqBri + +CUSTOM_MTIME=1603539720 +CUSTOM_MTIME_NSECS=54321 +CUSTOM_MODE="0764" +HASH_CUSTOM_MODE=QmchD3BN8TQ3RW6jPLxSaNkqvfuj7syKhzTRmL4EpyY1Nz +HASH_CUSTOM_MTIME=QmT3aY4avDcYXCWpU8CJzqUkW7YEuEsx36S8cTNoLcuK1B +HASH_CUSTOM_MTIME_NSECS=QmaKH8H5rXBUBCX4vdxi7ktGQEL7wejV7L9rX2qpZjwncz +HASH_CUSTOM_MODE_AND_MTIME=QmUkxrtBA8tPjwCYz1HrsoRfDz6NgKut3asVeHVQNH4C8L +HASH_CUSTOM_LINK_MTIME=QmV1Uot2gy4bhY9yvYiZxhhchhyYC6MKKoGV1XtWNmpCLe +HASH_CUSTOM_LINK_MTIME_NSECS=QmPHYCxYvvHj6VxiPNJ3kXxcPsnJLDYUJqsDJWjvytmrmY + +mk_name() { + tr -dc '[:alnum:]'> file1stat_expect && echo "ChildBlocks: 0" >> file1stat_expect && echo "Type: file" >> file1stat_expect && + echo "Mode: not set (not set)" >> file1stat_expect && + echo "Mtime: not set" >> file1stat_expect && test_cmp file1stat_expect file1stat_actual ' @@ -243,6 +245,8 @@ test_files_api() { echo "Size: 4" >> file1stat_expect && echo "ChildBlocks: 0" >> file1stat_expect && echo "Type: file" >> file1stat_expect && + echo "Mode: not set (not set)" >> file1stat_expect && + echo "Mtime: not set" >> file1stat_expect && test_cmp file1stat_expect file1stat_actual '