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:
@ -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{}
|
||||
|
@ -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():
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,8 @@ func TestCommands(t *testing.T) {
|
||||
"/files/rm",
|
||||
"/files/stat",
|
||||
"/files/write",
|
||||
"/files/chmod",
|
||||
"/files/touch",
|
||||
"/filestore",
|
||||
"/filestore/dups",
|
||||
"/filestore/ls",
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
2
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
|
||||
|
4
go.sum
4
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=
|
||||
|
@ -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
|
||||
|
@ -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=
|
||||
|
513
test/sharness/t0047-add-mode-mtime.sh
Executable file
513
test/sharness/t0047-add-mode-mtime.sh
Executable 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
|
@ -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
|
||||
'
|
||||
|
||||
|
Reference in New Issue
Block a user