1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-05-17 06:57:40 +08:00

feat: Support storing UnixFS 1.5 Mode and ModTime (#10478)

Co-authored-by: Marcin Rataj <lidel@lidel.org>
This commit is contained in:
Andrew Gillis
2024-08-20 17:02:46 -07:00
committed by GitHub
parent c5b0428860
commit 263edb251e
21 changed files with 1105 additions and 91 deletions

View File

@ -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{}

View File

@ -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():
}

View File

@ -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
}
}

View File

@ -89,6 +89,8 @@ func TestCommands(t *testing.T) {
"/files/rm",
"/files/stat",
"/files/write",
"/files/chmod",
"/files/touch",
"/filestore",
"/filestore/dups",
"/filestore/ls",

View File

@ -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: <size>
CumulativeSize: <cumulsize>
ChildBlocks: <childs>
Type: <type>`
Type: <type>
Mode: <mode> (<mode-octal>)
Mtime: <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: "+
"<hash> <size> <cumulsize> <type> <childs>. Conflicts with other format options.").WithDefault(defaultStatFormat),
"<hash> <size> <cumulsize> <type> <childs> and optional <mode> <mode-octal> <mtime> <mtime-secs> <mtime-nsecs>."+
"Conflicts with other format options.").WithDefault(defaultStatFormat),
cmds.BoolOption(filesHashOptionName, "Print only hash. Implies '--format=<hash>'. Conflicts with other format options."),
cmds.BoolOption(filesSizeOptionName, "Print only size. Implies '--format=<cumulsize>'. Conflicts with other format options."),
cmds.BoolOption(filesWithLocalOptionName, "Compute the amount of the dag that is local, and if possible the total size"),
@ -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, "<hash>", out.Hash, -1)
s = strings.Replace(s, "<size>", fmt.Sprintf("%d", out.Size), -1)
s = strings.Replace(s, "<cumulsize>", fmt.Sprintf("%d", out.CumulativeSize), -1)
s = strings.Replace(s, "<childs>", fmt.Sprintf("%d", out.Blocks), -1)
s = strings.Replace(s, "<type>", out.Type, -1)
s = strings.Replace(s, "<mode>", mode, -1)
s = strings.Replace(s, "<mode-octal>", modeo, -1)
s = strings.Replace(s, "<mtime>", mtime, -1)
s = strings.Replace(s, "<mtime-secs>", mtimes, -1)
s = strings.Replace(s, "<mtime-nsecs>", 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 <your file>
$ ipfs files cp /ipfs/<CID> /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/<CID> /your/desired/mfs/path
$ ipfs pin add <CID>
@ -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)
},
}

View File

@ -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) {

View File

@ -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))
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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=

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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=

View File

@ -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:]'</dev/urandom|head -c 16
}
mk_file() {
mktemp -p "$SHARNESS_TRASH_DIRECTORY" "mk_file_${1}_XXXXXX"
}
mk_dir() {
mktemp -d -p "$SHARNESS_TRASH_DIRECTORY" "mk_dir_${1}_XXXXXX"
}
# force umask for deterministic mode on files created via touch
# (https://github.com/orgs/community/discussions/40876, https://github.com/ipfs/kubo/pull/10478/#discussion_r1717515514)
umask 022
FIXTURESDIR="$(mk_dir fixtures)"
test_file() {
local TESTFILE="$FIXTURESDIR/test$1.txt"
local TESTLINK="$FIXTURESDIR/linkfile$1"
touch "$TESTFILE"
ln -s nothing "$TESTLINK"
test_expect_success "feature on file has no effect when not used [$1]" '
touch "$TESTFILE" &&
HASH=$(ipfs add -q "$TESTFILE") &&
test "$HASH_NO_PRESERVE" = "$HASH"
'
test_expect_success "can preserve file mode [$1]" '
touch "$TESTFILE" &&
chmod $PRESERVE_MODE "$TESTFILE" &&
HASH=$(ipfs add -q --preserve-mode "$TESTFILE") &&
test "$HASH_PRESERVE_MODE" = "$HASH"
'
test_expect_success "can preserve file modification time [$1]" '
touch -m -d @$PRESERVE_MTIME "$TESTFILE" &&
HASH=$(ipfs add -q --preserve-mtime "$TESTFILE") &&
test "$HASH_PRESERVE_MTIME" = "$HASH"
'
test_expect_success "can preserve file mode and modification time [$1]" '
touch -m -d @$PRESERVE_MTIME "$TESTFILE" &&
chmod $PRESERVE_MODE "$TESTFILE" &&
HASH=$(ipfs add -q --preserve-mode --preserve-mtime "$TESTFILE") &&
test "$HASH_PRESERVE_MODE_AND_MTIME" = "$HASH"
'
test_expect_success "can preserve symlink modification time [$1]" '
touch -h -m -d @$PRESERVE_MTIME "$TESTLINK" &&
HASH=$(ipfs add -q --preserve-mtime "$TESTLINK") &&
test "$HASH_PRESERVE_LINK_MTIME" = "$HASH"
'
test_expect_success "can set file mode [$1]" '
touch "$TESTFILE" &&
chmod 0600 "$TESTFILE" &&
HASH=$(ipfs add -q --mode=$CUSTOM_MODE "$TESTFILE") &&
test "$HASH_CUSTOM_MODE" = "$HASH"
'
test_expect_success "can set file modification time [$1]" '
touch -m -t 202011021234.42 "$TESTFILE" &&
HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME "$TESTFILE") &&
test "$HASH_CUSTOM_MTIME" = "$HASH"
'
test_expect_success "can set file modification time nanoseconds [$1]" '
touch -m -t 202011021234.42 "$TESTFILE" &&
HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$TESTFILE") &&
test "$HASH_CUSTOM_MTIME_NSECS" = "$HASH"
'
test_expect_success "can set file mode and modification time [$1]" '
touch -m -t 202011021234.42 "$TESTFILE" &&
chmod 0600 "$TESTFILE" &&
HASH=$(ipfs add -q --mode=$CUSTOM_MODE --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$TESTFILE") &&
test "$HASH_CUSTOM_MODE_AND_MTIME" = "$HASH"
'
test_expect_success "can set symlink modification time [$1]" '
touch -h -m -t 202011021234.42 "$TESTLINK" &&
HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME "$TESTLINK") &&
test "$HASH_CUSTOM_LINK_MTIME" = "$HASH"
'
test_expect_success "cannot set mode on symbolic link" '
HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME --mode=$CUSTOM_MODE "$TESTLINK") &&
ACTUAL=$(ipfs files stat --format="<mode>" /ipfs/$HASH) &&
test "$ACTUAL" = "lrwxrwxrwx"
'
test_expect_success "can set symlink modification time nanoseconds [$1]" '
touch -h -m -t 202011021234.42 "$TESTLINK" &&
HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$TESTLINK") &&
test "$HASH_CUSTOM_LINK_MTIME_NSECS" = "$HASH"
'
test_expect_success "can get preserved mode and modification time [$1]" '
OUTFILE="$(mk_file $HASH_PRESERVE_MODE_AND_MTIME)" &&
ipfs get -o "$OUTFILE" $HASH_PRESERVE_MODE_AND_MTIME &&
test "$PRESERVE_MODE:$PRESERVE_MTIME" = "$(stat -c "0%a:%Y" "$OUTFILE")"
'
test_expect_success "can get custom mode and modification time [$1]" '
OUTFILE="$(mk_file $HASH_CUSTOM_MODE_AND_MTIME)" &&
ipfs get -o "$OUTFILE" $HASH_CUSTOM_MODE_AND_MTIME &&
TIMESTAMP=$(date +%s%N --date="$(stat -c "%y" "$OUTFILE")") &&
MODETIME=$(stat -c "0%a:$TIMESTAMP" "$OUTFILE") &&
printf -v EXPECTED "$CUSTOM_MODE:$CUSTOM_MTIME%09d" $CUSTOM_MTIME_NSECS &&
test "$EXPECTED" = "$MODETIME"
'
test_expect_success "can get custom symlink modification time [$1]" '
OUTFILE="$(mk_file $HASH_CUSTOM_LINK_MTIME_NSECS)" &&
ipfs get -o "$OUTFILE" $HASH_CUSTOM_LINK_MTIME_NSECS &&
TIMESTAMP=$(date +%s%N --date="$(stat -c "%y" "$OUTFILE")") &&
printf -v EXPECTED "$CUSTOM_MTIME%09d" $CUSTOM_MTIME_NSECS &&
test "$EXPECTED" = "$TIMESTAMP"
'
test_expect_success "can change file mode [$1]" '
NAME=$(mk_name) &&
HASH=$(echo testfile | ipfs add -q --mode=0600) &&
OUTFILE=$(mk_file "${NAME}") &&
ipfs files cp "/ipfs/$HASH" /$NAME &&
ipfs files chmod 444 /$NAME &&
HASH2=$(ipfs files stat /$NAME|head -1) &&
ipfs get -o "$OUTFILE" $HASH2 &&
test $(stat -c "%a" "$OUTFILE") = 444
'
# special case, because storing mode requires dag-pb envelope
# and when dealing with CIDv1 we can have 'raw' block instead of 'dag-pb'
# so it needs to be converted before adding attribute
test_expect_success "can add file mode to cidv1 raw block [$1]" '
NAME=$(mk_name) &&
HASH=$(date | ipfs add -q --cid-version 1 --raw-leaves=true) &&
OUTFILE=$(mk_file "${NAME}") &&
ipfs files cp "/ipfs/$HASH" /$NAME &&
ipfs files chmod 445 /$NAME &&
HASH2=$(ipfs files stat /$NAME|head -1) &&
ipfs get -o "$OUTFILE" $HASH2 &&
test $(stat -c "%a" "$OUTFILE") = 445
'
test_expect_success "can change file modification time [$1]" '
NAME=$(mk_name) &&
OUTFILE="$(mk_file "$NAME")" &&
NOW=$(date +%s) &&
HASH=$(echo testfile | ipfs add -q --mtime=$NOW) &&
ipfs files cp "/ipfs/$HASH" /$NAME &&
sleep 1 &&
ipfs files touch /$NAME &&
HASH=$(ipfs files stat /$NAME|head -1) &&
ipfs get -o "$OUTFILE" "$HASH" &&
test $(stat -c "%Y" "$OUTFILE") -gt $NOW
'
# special case, because storing mtime requires dag-pb envelope
# and when dealing with CIDv1 we can have 'raw' block instead of 'dag-pb'
# so it needs to be converted to dag-pb before adding attribute
test_expect_success "can add file modification time to cidv1 raw block [$1]" '
NAME=$(mk_name) &&
OUTFILE="$(mk_file "$NAME")" &&
EXPECTED="$CUSTOM_MTIME" &&
HASH=$(date | ipfs add -q --cid-version 1 --raw-leaves=true) &&
ipfs files cp "/ipfs/$HASH" /$NAME &&
ipfs files touch --mtime=$EXPECTED /$NAME &&
test $(ipfs files stat --format="<mtime-secs>" "/$NAME") -eq $EXPECTED &&
HASH=$(ipfs files stat /$NAME|head -1) &&
ipfs get -o "$OUTFILE" "$HASH" &&
test $(stat -c "%Y" "$OUTFILE") -eq $EXPECTED
'
test_expect_success "can change file modification time nanoseconds [$1]" '
NAME=$(mk_name) &&
echo test|ipfs files write --create /$NAME &&
EXPECTED=$(date --date="yesterday" +%s) &&
ipfs files touch --mtime=$EXPECTED --mtime-nsecs=55567 /$NAME &&
test $(ipfs files stat --format="<mtime-secs>" /$NAME) -eq $EXPECTED &&
test $(ipfs files stat --format="<mtime-nsecs>" /$NAME) -eq 55567
'
## TODO: update these tests if/when symbolic links are fully supported in go-mfs
test_expect_success "can change symlink modification time [$1]" '
NAME=$(mk_name) &&
EXPECTED=$(date +%s) &&
ipfs files cp "/ipfs/$HASH_PRESERVE_LINK_MTIME" "/$NAME" ||
ipfs files touch --mtime=$EXPECTED "/$NAME" &&
test $(ipfs files stat --format="<mtime-secs>" "/$NAME") -eq $EXPECTED
'
test_expect_success "can change symlink modification time nanoseconds [$1]" '
NAME=$(mk_name) &&
EXPECTED=$(date +%s) &&
ipfs files cp "/ipfs/$HASH_PRESERVE_LINK_MTIME" "/$NAME" ||
ipfs files touch --mtime=$EXPECTED --mtime-nsecs=938475 "/$NAME" &&
test $(ipfs files stat --format="<mtime-secs>" "/$NAME") -eq $EXPECTED &&
test $(ipfs files stat --format="<mtime-nsecs>" "/$NAME") -eq 938475
'
}
DIR_TIME=1655158632
setup_directory() {
local TESTDIR="$(mktemp -d -p "$FIXTURESDIR" "${1}XXXXXX")"
mkdir -p "$TESTDIR"/{dir1,dir2/sub1/sub2,dir3}
chmod 0755 "$TESTDIR/dir1"
touch -md @$(($DIR_TIME+10)) "$TESTDIR/dir2/sub1/sub2/file3"
ln -s ../sub2/file3 "$TESTDIR/dir2/sub1/link1"
touch -h -md @$(($DIR_TIME+20)) "$TESTDIR/dir2/sub1/link1"
touch -md @$(($DIR_TIME+30)) "$TESTDIR/dir2/sub1/sub2"
touch -md @$(($DIR_TIME+40)) "$TESTDIR/dir2/sub1"
touch -md @$(($DIR_TIME+50)) "$TESTDIR/dir2"
touch -md @$(($DIR_TIME+60)) "$TESTDIR/dir3/file2"
touch -md @$(($DIR_TIME+70)) "$TESTDIR/dir3"
touch -md @$(($DIR_TIME+80)) "$TESTDIR/file1"
touch -md @$(($DIR_TIME+90)) "$TESTDIR/dir1"
touch -md @$DIR_TIME "$TESTDIR"
echo "$TESTDIR"
}
test_directory() {
CUSTOM_DIR_MODE=0713
TESTDIR=$(setup_directory $1)
TESTDIR1="$TESTDIR/dir1"
OUTDIR="$(mk_dir "${1}")"
HASH_DIR_ROOT=QmSioyvQuXetxg7uo8FswGn9XKKEsisDq1HTMzGyWbw2R6
HASH_DIR1_NO_PRESERVE=QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
HASH_DIR1_PRESERVE_MODE=QmRviohgafvCsbkiTgfQFipbuXJ6k1YtoiaQW4quttJPKu
HASH_DIR1_PRESERVE_MTIME=QmYMy7CZGb498QFSQBF5ZFwv1FYbrAtYZMe4VxhDXxAcvf
HASH_DIR1_CUSTOM_MODE=QmQ1ABnw2iip7sj23EzzBZ9T77KyyfESP6SUboiXPyzNQe
HASH_DIR1_CUSTOM_MTIME=QmfWitW6F13WHFXLbJzXRYmwrS1p4gaAJAfucUSMytRPn3
HASH_DIR1_CUSTOM_MTIME_NSECS=QmZFdCLJay31hT3Tx1LygJ7XfiLEs3qLCXtbeBfhf38aZg
HASH_DIR_SUB1=QmeQwX5qAX18fcPDxDdkfM6ttuFCZetF5hgeUa6ov8D5oc
HASH_DIR_MODE_AND_MTIME=(
QmRCG3Pprg4jbhfYBzVzfJVyneFHnBquPGXwvXU3jSuf5j
QmReHCn4BSJJdtd6Le8Hd8Puai6TmgpPCYb13wyM7FD9AD
QmSioyvQuXetxg7uo8FswGn9XKKEsisDq1HTMzGyWbw2R6
QmTMoVgJKhPrz9DfkvT132mxyBXNae5azXQ42WbM9abdSE
QmVzXqpuQGCAgRwEbGuE9xe8Fidi1HEXaPKsQEFEbPJW9j
QmW6Nqy2nziduAp3UGx2a52gtSUsYzhVcZMuPdxBRnwCyP
QmeQwX5qAX18fcPDxDdkfM6ttuFCZetF5hgeUa6ov8D5oc
QmefofUNwC2U3Xp87rB1x8Aws6AdsDuoXR7B9u2RkEZ4dQ
Qmeu24TFarJwLzJgMTDYDJTr4BMGnzafoSnfxov1513abW
Qmf82bbFg2e8HmcqiewutVVw5NoMpiXZD57LpLdC1poBuH)
HASH_DIR_CUSTOM_MODE=(
QmNZ5cyx3f6maXkczwhh3ufjDCh9f3k9zrDhX218ZZGvoV
QmRqtFVLkXfWJuqWtYiCPthgomo3gouno8uvMeGAyCVaWS
QmSkrWNcyDA7s1qiT6Ps7ey4zcB7uBH3sqGcKRfW4UMKhM
QmSkrWNcyDA7s1qiT6Ps7ey4zcB7uBH3sqGcKRfW4UMKhM
QmSkrWNcyDA7s1qiT6Ps7ey4zcB7uBH3sqGcKRfW4UMKhM
QmZNAZXB6JyJ1cK9h1uJEK4XDo1CKsSuHMPGUUMrzDXCQz
QmbSz6GyS8MNR4M9xtCteuGVJQRYkCXLbW174Fdy8jtaoZ
QmccnAQQeJGtmtgZoi3hpEmgdxbuX1ao2hQmrKmmwQnCn9
QmeTZoiAiduFY2hXaNQP4ehiE71BrQFEnrqduBZ5ZjHuFy
Qmf13KNurvAHUfMBhMWvZuftmUikhhGY7ohWVaBDDndFMz)
HASH_DIR_CUSTOM_MTIME=(
QmPCGFZ8ZFowAwfWdCeGsr9wSbGXwZiHW3bZ7XSYcc1Zby
QmT3aY4avDcYXCWpU8CJzqUkW7YEuEsx36S8cTNoLcuK1B
QmT3aY4avDcYXCWpU8CJzqUkW7YEuEsx36S8cTNoLcuK1B
QmT3aY4avDcYXCWpU8CJzqUkW7YEuEsx36S8cTNoLcuK1B
QmUGMu9epCEz5HMsuJFgpJxxt3HoahsTQcC65Jje6LNqYF
QmXhzoPKuqmkqbyr4kJFznFRXtGwriCXKGFPr4vviyK3aV
QmZ5wKCcL11TckypuDTKLLNFP6JMCBJRCn385XKQQ6PCLt
Qmdw3hiAxn6R5MRkkdzLdFvZUa2WJeLCTXXCyB8byFsHSA
QmedF4m2Y8341azfkpvaHSkxbSrZa4fo6FT25h6sRUVkpq
QmfWitW6F13WHFXLbJzXRYmwrS1p4gaAJAfucUSMytRPn3)
test_expect_success "feature on directory has no effect when not used [$1]" '
HASH=$(ipfs add -qr "$TESTDIR1") &&
test "$HASH_DIR1_NO_PRESERVE" = "$HASH"
'
test_expect_success "can preserve directory mode [$1]" '
HASH=$(ipfs add -qr --preserve-mode "$TESTDIR1") &&
test "$HASH_DIR1_PRESERVE_MODE" = "$HASH"
'
test_expect_success "can preserve directory modification time [$1]" '
HASH=$(ipfs add -qr --preserve-mtime "$TESTDIR1") &&
test "$HASH_DIR1_PRESERVE_MTIME" = "$HASH"
'
test_expect_success "can set directory mode [$1]" '
HASH=$(ipfs add -qr --mode=$CUSTOM_DIR_MODE "$TESTDIR1") &&
test "$HASH_DIR1_CUSTOM_MODE" = "$HASH"
'
test_expect_success "can set directory modification time [$1]" '
HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME "$TESTDIR1") &&
test "$HASH_DIR1_CUSTOM_MTIME" = "$HASH"
'
test_expect_success "can set directory modification time nanoseconds [$1]" '
HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$TESTDIR1") &&
test "$HASH_DIR1_CUSTOM_MTIME_NSECS" = "$HASH"
'
test_expect_success "can recursively preserve mode and modification time [$1]" '
test "700:$DIR_TIME" = "$(stat -c "%a:%Y" "$TESTDIR")" &&
test "644:$((DIR_TIME+10))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/sub2/file3")" &&
test "777:$((DIR_TIME+20))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/link1")" &&
test "755:$((DIR_TIME+30))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/sub2")" &&
test "755:$((DIR_TIME+40))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1")" &&
test "755:$((DIR_TIME+50))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2")" &&
test "644:$((DIR_TIME+60))" = "$(stat -c "%a:%Y" "$TESTDIR/dir3/file2")" &&
test "755:$((DIR_TIME+70))" = "$(stat -c "%a:%Y" "$TESTDIR/dir3")" &&
test "644:$((DIR_TIME+80))" = "$(stat -c "%a:%Y" "$TESTDIR/file1")" &&
test "755:$((DIR_TIME+90))" = "$(stat -c "%a:%Y" "$TESTDIR/dir1")" &&
HASHES=($(ipfs add -qr --preserve-mode --preserve-mtime "$TESTDIR"|sort)) &&
test "${HASHES[*]}" = "${HASH_DIR_MODE_AND_MTIME[*]}"
'
test_expect_success "can recursively set directory mode [$1]" '
HASHES=($(ipfs add -qr --mode=0753 "$TESTDIR"|sort)) &&
test "${HASHES[*]}" = "${HASH_DIR_CUSTOM_MODE[*]}"
'
test_expect_success "can recursively set directory mtime [$1]" '
HASHES=($(ipfs add -qr --mtime=$CUSTOM_MTIME "$TESTDIR"|sort)) &&
test "${HASHES[*]}" = "${HASH_DIR_CUSTOM_MTIME[*]}"
'
test_expect_success "can recursively restore mode and mtime [$1]" '
ipfs get -o "$OUTDIR" $HASH_DIR_ROOT &&
test "700:$DIR_TIME" = "$(stat -c "%a:%Y" "$OUTDIR")" &&
test "644:$((DIR_TIME+10))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2/sub1/sub2/file3")" &&
test "777:$((DIR_TIME+20))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2/sub1/link1")" &&
test "755:$((DIR_TIME+30))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2/sub1/sub2")" &&
test "755:$((DIR_TIME+40))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2/sub1")" &&
test "755:$((DIR_TIME+50))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2")" &&
test "644:$((DIR_TIME+60))" = "$(stat -c "%a:%Y" "$OUTDIR/dir3/file2")" &&
test "755:$((DIR_TIME+70))" = "$(stat -c "%a:%Y" "$OUTDIR/dir3")" &&
test "644:$((DIR_TIME+80))" = "$(stat -c "%a:%Y" "$OUTDIR/file1")" &&
test "755:$((DIR_TIME+90))" = "$(stat -c "%a:%Y" "$OUTDIR/dir1")"
'
# basic smoke-test for cidv1 (we dont care about CID, just care about
# mode/mtime surviving ipfs import and export if --cid-version 1 is at play)
test_expect_success "can recursively preserve and restore mode and mtime with CIDv1 [$1]" '
test "700:$DIR_TIME" = "$(stat -c "%a:%Y" "$TESTDIR")" &&
test "644:$((DIR_TIME+10))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/sub2/file3")" &&
test "777:$((DIR_TIME+20))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/link1")" &&
test "755:$((DIR_TIME+30))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/sub2")" &&
test "755:$((DIR_TIME+40))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1")" &&
test "755:$((DIR_TIME+50))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2")" &&
test "644:$((DIR_TIME+60))" = "$(stat -c "%a:%Y" "$TESTDIR/dir3/file2")" &&
test "755:$((DIR_TIME+70))" = "$(stat -c "%a:%Y" "$TESTDIR/dir3")" &&
test "644:$((DIR_TIME+80))" = "$(stat -c "%a:%Y" "$TESTDIR/file1")" &&
test "755:$((DIR_TIME+90))" = "$(stat -c "%a:%Y" "$TESTDIR/dir1")" &&
CIDV1DIR=$(ipfs add -Qr --preserve-mode --preserve-mtime --cid-version 1 "$TESTDIR") &&
OUTDIRV1=$(mk_dir cidv1roundtrip$1) &&
ipfs get -o "$OUTDIRV1" $CIDV1DIR &&
test "700:$DIR_TIME" = "$(stat -c "%a:%Y" "$OUTDIRV1")" &&
test "644:$((DIR_TIME+10))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2/sub1/sub2/file3")" &&
test "777:$((DIR_TIME+20))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2/sub1/link1")" &&
test "755:$((DIR_TIME+30))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2/sub1/sub2")" &&
test "755:$((DIR_TIME+40))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2/sub1")" &&
test "755:$((DIR_TIME+50))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2")" &&
test "644:$((DIR_TIME+60))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir3/file2")" &&
test "755:$((DIR_TIME+70))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir3")" &&
test "644:$((DIR_TIME+80))" = "$(stat -c "%a:%Y" "$OUTDIRV1/file1")" &&
test "755:$((DIR_TIME+90))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir1")"
'
test_expect_success "can change directory mode [$1]" '
NAME=$(mk_name) &&
ipfs files cp "/ipfs/$HASH_DIR_SUB1" /$NAME &&
ipfs files chmod 0710 /$NAME &&
test $(ipfs files stat --format="<mode>" /$NAME) = "drwx--x---"
'
test_expect_success "can change directory modification time [$1]" '
NAME=$(mk_name) &&
ipfs files cp "/ipfs/$HASH_DIR_SUB1" /$NAME &&
ipfs files touch --mtime=$CUSTOM_MTIME /$NAME &&
test $(ipfs files stat --format="<mtime-secs>" /$NAME) -eq $CUSTOM_MTIME
'
test_expect_success "can change directory modification time nanoseconds [$1]" '
NAME=$(mk_name) &&
MTIME=$(date --date="yesterday" +%s) &&
ipfs files cp "/ipfs/$HASH_DIR_SUB1" /$NAME &&
ipfs files touch --mtime=$MTIME --mtime-nsecs=94783 /$NAME &&
test $(ipfs files stat --format="<mtime-secs>" /$NAME) -eq $MTIME &&
test $(ipfs files stat --format="<mtime-nsecs>" /$NAME) -eq 94783
'
}
test_stat_template() {
test_expect_success "can stat $2 string mode [$1]" '
touch "$STAT_TARGET" &&
HASH=$(ipfs add -qr --mode="$STAT_MODE_OCTAL" "$STAT_TARGET") &&
ACTUAL=$(ipfs files stat --format="<mode>" /ipfs/$HASH) &&
test "$ACTUAL" = "$STAT_MODE_STRING"
'
test_expect_success "can stat $2 octal mode [$1]" '
touch "$STAT_TARGET" &&
HASH=$(ipfs add -qr --mode="$STAT_MODE_OCTAL" "$STAT_TARGET") &&
ACTUAL=$(ipfs files stat --format="<mode-octal>" /ipfs/$HASH) &&
test "$ACTUAL" = "$STAT_MODE_OCTAL"
'
test_expect_success "can stat $2 modification time string [$1]" '
touch "$STAT_TARGET" &&
HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME "$STAT_TARGET") &&
ACTUAL=$(ipfs files stat --format="<mtime>" /ipfs/$HASH) &&
test "$ACTUAL" = "24 Oct 2020, 11:42:00 UTC"
'
test_expect_success "can stat $2 modification time seconds [$1]" '
touch "$STAT_TARGET" &&
HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME "$STAT_TARGET") &&
ACTUAL=$(ipfs files stat --format="<mtime-secs>" /ipfs/$HASH) &&
test $ACTUAL -eq $CUSTOM_MTIME
'
test_expect_success "can stat $2 modification time nanoseconds [$1]" '
touch "$STAT_TARGET" &&
HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$STAT_TARGET") &&
ACTUAL=$(ipfs files stat --format="<mtime-nsecs>" /ipfs/$HASH) &&
test $ACTUAL -eq $CUSTOM_MTIME_NSECS
'
}
test_stat() {
STAT_TARGET="$FIXTURESDIR/statfile$1"
STAT_MODE_OCTAL="$CUSTOM_MODE"
STAT_MODE_STRING="-rwxrw-r--"
test_stat_template "$1" "file"
STAT_TARGET="$FIXTURESDIR/statdir$1"
STAT_MODE_OCTAL="0731"
STAT_MODE_STRING="drwx-wx--x"
mkdir "$STAT_TARGET"
test_stat_template "$1" "directory"
STAT_TARGET="$FIXTURESDIR/statlink$1"
STAT_MODE_OCTAL="0777"
STAT_MODE_STRING="lrwxrwxrwx"
ln -s nothing "$STAT_TARGET"
test_stat_template "$1" "link"
STAT_TARGET="$FIXTURESDIR/statfile$1"
test_expect_success "can chain stat template [$1]" '
HASH=$(ipfs add -q --mode=0644 --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$STAT_TARGET") &&
ACTUAL=$(ipfs files stat --format="<mtime> <mtime-secs> <mtime-nsecs> <mode> <mode-octal>" /ipfs/$HASH) &&
test "$ACTUAL" = "24 Oct 2020, 11:42:00 UTC 1603539720 54321 -rw-r--r-- 0644"
'
}
test_all() {
test_stat "$1"
test_file "$1"
test_directory "$1"
}
# test direct
test_all "direct"
# test daemon
test_launch_ipfs_daemon_without_network
test_all "daemon"
test_kill_ipfs_daemon
test_done

View File

@ -230,6 +230,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
'
@ -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
'