feat: Support storing UnixFS 1.5 Mode and ModTime (#10478)
Co-authored-by: Marcin Rataj <lidel@lidel.org>
Andrew Gillis committed
Aug 20, 2024 at 17:02 UTC
263edb251eecfb375110bcacf170b193ddfea179
21 files changed
+1105
-91
client/rpc/apifile.go
+104
-19
@@ -1,10 +1,14 @@
1
package rpc
2
3
import (
4
+ "bytes"
5
"context"
6
"encoding/json"
7
"fmt"
8
"io"
9
+ "os"
10
+ "strconv"
11
+ "time"
12
13
"github.com/ipfs/boxo/files"
14
unixfs "github.com/ipfs/boxo/ipld/unixfs"
@@ -24,20 +28,35 @@ func (api *UnixfsAPI) Get(ctx context.Context, p path.Path) (files.Node, error)
28
}
29
30
var stat struct {
27
- Hash string
28
- Type string
29
- Size int64 // unixfs size
31
+ Hash string
32
+ Type string
33
+ Size int64 // unixfs size
34
+ Mode string
35
+ Mtime int64
36
+ MtimeNsecs int
37
}
38
err := api.core().Request("files/stat", p.String()).Exec(ctx, &stat)
39
if err != nil {
40
return nil, err
41
}
42
43
+ mode, err := stringToFileMode(stat.Mode)
44
+ if err != nil {
45
+ return nil, err
46
+ }
47
+
48
+ var modTime time.Time
49
+ if stat.Mtime != 0 {
50
+ modTime = time.Unix(stat.Mtime, int64(stat.MtimeNsecs)).UTC()
51
+ }
52
+
53
switch stat.Type {
54
case "file":
38
- return api.getFile(ctx, p, stat.Size)
55
+ return api.getFile(ctx, p, stat.Size, mode, modTime)
56
case "directory":
40
- return api.getDir(ctx, p, stat.Size)
57
+ return api.getDir(ctx, p, stat.Size, mode, modTime)
58
+ case "symlink":
59
+ return api.getSymlink(ctx, p, modTime)
60
default:
61
return nil, fmt.Errorf("unsupported file type '%s'", stat.Type)
62
}
@@ -49,6 +68,9 @@ type apiFile struct {
68
size int64
69
path path.Path
70
71
+ mode os.FileMode
72
+ mtime time.Time
73
+
74
r *Response
75
at int64
76
}
@@ -128,16 +150,37 @@ func (f *apiFile) Close() error {
150
return nil
151
}
152
153
+func (f *apiFile) Mode() os.FileMode {
154
+ return f.mode
155
+}
156
+
157
+func (f *apiFile) ModTime() time.Time {
158
+ return f.mtime
159
+}
160
+
161
func (f *apiFile) Size() (int64, error) {
162
return f.size, nil
163
}
164
135
-func (api *UnixfsAPI) getFile(ctx context.Context, p path.Path, size int64) (files.Node, error) {
165
+func stringToFileMode(mode string) (os.FileMode, error) {
166
+ if mode == "" {
167
+ return 0, nil
168
+ }
169
+ mode64, err := strconv.ParseUint(mode, 8, 32)
170
+ if err != nil {
171
+ return 0, fmt.Errorf("cannot parse mode %s: %s", mode, err)
172
+ }
173
+ return os.FileMode(uint32(mode64)), nil
174
+}
175
+
176
+func (api *UnixfsAPI) getFile(ctx context.Context, p path.Path, size int64, mode os.FileMode, mtime time.Time) (files.Node, error) {
177
f := &apiFile{
137
- ctx: ctx,
138
- core: api.core(),
139
- size: size,
140
- path: p,
178
+ ctx: ctx,
179
+ core: api.core(),
180
+ size: size,
181
+ path: p,
182
+ mode: mode,
183
+ mtime: mtime,
184
}
185
186
return f, f.reset()
@@ -195,13 +238,19 @@ func (it *apiIter) Next() bool {
238
239
switch it.cur.Type {
240
case unixfs.THAMTShard, unixfs.TMetadata, unixfs.TDirectory:
198
- it.curFile, err = it.core.getDir(it.ctx, path.FromCid(c), int64(it.cur.Size))
241
+ it.curFile, err = it.core.getDir(it.ctx, path.FromCid(c), int64(it.cur.Size), it.cur.Mode, it.cur.ModTime)
242
if err != nil {
243
it.err = err
244
return false
245
}
246
case unixfs.TFile:
204
- it.curFile, err = it.core.getFile(it.ctx, path.FromCid(c), int64(it.cur.Size))
247
+ it.curFile, err = it.core.getFile(it.ctx, path.FromCid(c), int64(it.cur.Size), it.cur.Mode, it.cur.ModTime)
248
+ if err != nil {
249
+ it.err = err
250
+ return false
251
+ }
252
+ case unixfs.TSymlink:
253
+ it.curFile, err = it.core.getSymlink(it.ctx, path.FromCid(c), it.cur.ModTime)
254
if err != nil {
255
it.err = err
256
return false
@@ -223,6 +272,9 @@ type apiDir struct {
272
size int64
273
path path.Path
274
275
+ mode os.FileMode
276
+ mtime time.Time
277
+
278
dec *json.Decoder
279
}
280
@@ -230,6 +282,14 @@ func (d *apiDir) Close() error {
282
return nil
283
}
284
285
+func (d *apiDir) Mode() os.FileMode {
286
+ return d.mode
287
+}
288
+
289
+func (d *apiDir) ModTime() time.Time {
290
+ return d.mtime
291
+}
292
+
293
func (d *apiDir) Size() (int64, error) {
294
return d.size, nil
295
}
@@ -242,7 +302,7 @@ func (d *apiDir) Entries() files.DirIterator {
302
}
303
}
304
245
-func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64) (files.Node, error) {
305
+func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64, mode os.FileMode, modTime time.Time) (files.Node, error) {
306
resp, err := api.core().Request("ls", p.String()).
307
Option("resolve-size", true).
308
Option("stream", true).Send(ctx)
@@ -253,18 +313,43 @@ func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64) (file
313
return nil, resp.Error
314
}
315
256
- d := &apiDir{
257
- ctx: ctx,
258
- core: api,
259
- size: size,
260
- path: p,
316
+ data, _ := io.ReadAll(resp.Output)
317
+ rdr := bytes.NewReader(data)
318
262
- dec: json.NewDecoder(resp.Output),
319
+ d := &apiDir{
320
+ ctx: ctx,
321
+ core: api,
322
+ size: size,
323
+ path: p,
324
+ mode: mode,
325
+ mtime: modTime,
326
+
327
+ //dec: json.NewDecoder(resp.Output),
328
+ dec: json.NewDecoder(rdr),
329
}
330
331
return d, nil
332
}
333
334
+func (api *UnixfsAPI) getSymlink(ctx context.Context, p path.Path, modTime time.Time) (files.Node, error) {
335
+ resp, err := api.core().Request("cat", p.String()).
336
+ Option("resolve-size", true).
337
+ Option("stream", true).Send(ctx)
338
+ if err != nil {
339
+ return nil, err
340
+ }
341
+ if resp.Error != nil {
342
+ return nil, resp.Error
343
+ }
344
+
345
+ target, err := io.ReadAll(resp.Output)
346
+ if err != nil {
347
+ return nil, err
348
+ }
349
+
350
+ return files.NewSymlinkFile(string(target), modTime), nil
351
+}
352
+
353
var (
354
_ files.File = &apiFile{}
355
_ files.Directory = &apiDir{}
client/rpc/unixfs.go
+13
-6
@@ -6,6 +6,8 @@ import (
6
"errors"
7
"fmt"
8
"io"
9
+ "os"
10
+ "time"
11
12
"github.com/ipfs/boxo/files"
13
unixfs "github.com/ipfs/boxo/ipld/unixfs"
@@ -80,14 +82,13 @@ func (api *UnixfsAPI) Add(ctx context.Context, f files.Node, opts ...caopts.Unix
82
}
83
defer resp.Output.Close()
84
dec := json.NewDecoder(resp.Output)
83
-loop:
85
+
86
for {
87
var evt addEvent
86
- switch err := dec.Decode(&evt); err {
87
- case nil:
88
- case io.EOF:
89
- break loop
90
- default:
88
+ if err := dec.Decode(&evt); err != nil {
89
+ if errors.Is(err, io.EOF) {
90
+ break
91
+ }
92
return path.ImmutablePath{}, err
93
}
94
out = evt
@@ -129,6 +130,9 @@ type lsLink struct {
130
Size uint64
131
Type unixfs_pb.Data_DataType
132
Target string
133
+
134
+ Mode os.FileMode
135
+ ModTime time.Time
136
}
137
138
type lsObject struct {
@@ -222,6 +226,9 @@ func (api *UnixfsAPI) Ls(ctx context.Context, p path.Path, opts ...caopts.Unixfs
226
Size: l0.Size,
227
Type: ftype,
228
Target: l0.Target,
229
+
230
+ Mode: l0.Mode,
231
+ ModTime: l0.ModTime,
232
}:
233
case <-ctx.Done():
234
}
core/commands/add.go
+101
-19
@@ -6,7 +6,9 @@ import (
6
"io"
7
"os"
8
gopath "path"
9
+ "strconv"
10
"strings"
11
+ "time"
12
13
"github.com/ipfs/kubo/config"
14
"github.com/ipfs/kubo/core/commands/cmdenv"
@@ -25,11 +27,31 @@ import (
27
// ErrDepthLimitExceeded indicates that the max depth has been exceeded.
28
var ErrDepthLimitExceeded = fmt.Errorf("depth limit exceeded")
29
30
+type TimeParts struct {
31
+ t *time.Time
32
+}
33
+
34
+func (t TimeParts) MarshalJSON() ([]byte, error) {
35
+ return t.t.MarshalJSON()
36
+}
37
+
38
+// UnmarshalJSON implements the json.Unmarshaler interface.
39
+// The time is expected to be a quoted string in RFC 3339 format.
40
+func (t *TimeParts) UnmarshalJSON(data []byte) (err error) {
41
+ // Fractional seconds are handled implicitly by Parse.
42
+ tt, err := time.Parse("\"2006-01-02T15:04:05Z\"", string(data))
43
+ *t = TimeParts{&tt}
44
+ return
45
+}
46
+
47
type AddEvent struct {
29
- Name string
30
- Hash string `json:",omitempty"`
31
- Bytes int64 `json:",omitempty"`
32
- Size string `json:",omitempty"`
48
+ Name string
49
+ Hash string `json:",omitempty"`
50
+ Bytes int64 `json:",omitempty"`
51
+ Size string `json:",omitempty"`
52
+ Mode string `json:",omitempty"`
53
+ Mtime int64 `json:",omitempty"`
54
+ MtimeNsecs int `json:",omitempty"`
55
}
56
57
const (
@@ -50,6 +72,12 @@ const (
72
inlineOptionName = "inline"
73
inlineLimitOptionName = "inline-limit"
74
toFilesOptionName = "to-files"
75
+
76
+ preserveModeOptionName = "preserve-mode"
77
+ preserveMtimeOptionName = "preserve-mtime"
78
+ modeOptionName = "mode"
79
+ mtimeOptionName = "mtime"
80
+ mtimeNsecsOptionName = "mtime-nsecs"
81
)
82
83
const adderOutChanSize = 8
@@ -166,22 +194,24 @@ See 'dag export' and 'dag import' for more information.
194
cmds.IntOption(inlineLimitOptionName, "Maximum block size to inline. (experimental)").WithDefault(32),
195
cmds.BoolOption(pinOptionName, "Pin locally to protect added files from garbage collection.").WithDefault(true),
196
cmds.StringOption(toFilesOptionName, "Add reference to Files API (MFS) at the provided path."),
197
+ cmds.BoolOption(preserveModeOptionName, "Apply existing POSIX permissions to created UnixFS entries. Disables raw-leaves. (experimental)"),
198
+ cmds.BoolOption(preserveMtimeOptionName, "Apply existing POSIX modification time to created UnixFS entries. Disables raw-leaves. (experimental)"),
199
+ cmds.UintOption(modeOptionName, "Custom POSIX file mode to store in created UnixFS entries. Disables raw-leaves. (experimental)"),
200
+ cmds.Int64Option(mtimeOptionName, "Custom POSIX modification time to store in created UnixFS entries (seconds before or after the Unix Epoch). Disables raw-leaves. (experimental)"),
201
+ cmds.UintOption(mtimeNsecsOptionName, "Custom POSIX modification time (optional time fraction in nanoseconds)"),
202
},
203
PreRun: func(req *cmds.Request, env cmds.Environment) error {
204
quiet, _ := req.Options[quietOptionName].(bool)
205
quieter, _ := req.Options[quieterOptionName].(bool)
206
quiet = quiet || quieter
174
-
207
silent, _ := req.Options[silentOptionName].(bool)
208
177
- if quiet || silent {
178
- return nil
179
- }
180
-
181
- // ipfs cli progress bar defaults to true unless quiet or silent is used
182
- _, found := req.Options[progressOptionName].(bool)
183
- if !found {
184
- req.Options[progressOptionName] = true
209
+ if !quiet && !silent {
210
+ // ipfs cli progress bar defaults to true unless quiet or silent is used
211
+ _, found := req.Options[progressOptionName].(bool)
212
+ if !found {
213
+ req.Options[progressOptionName] = true
214
+ }
215
}
216
217
return nil
@@ -217,6 +247,11 @@ See 'dag export' and 'dag import' for more information.
247
inline, _ := req.Options[inlineOptionName].(bool)
248
inlineLimit, _ := req.Options[inlineLimitOptionName].(int)
249
toFilesStr, toFilesSet := req.Options[toFilesOptionName].(string)
250
+ preserveMode, _ := req.Options[preserveModeOptionName].(bool)
251
+ preserveMtime, _ := req.Options[preserveMtimeOptionName].(bool)
252
+ mode, _ := req.Options[modeOptionName].(uint)
253
+ mtime, _ := req.Options[mtimeOptionName].(int64)
254
+ mtimeNsecs, _ := req.Options[mtimeNsecsOptionName].(uint)
255
256
if chunker == "" {
257
chunker = cfg.Import.UnixFSChunker.WithDefault(config.DefaultUnixFSChunker)
@@ -236,6 +271,19 @@ See 'dag export' and 'dag import' for more information.
271
rawblks = cfg.Import.UnixFSRawLeaves.WithDefault(config.DefaultUnixFSRawLeaves)
272
}
273
274
+ // Storing optional mode or mtime (UnixFS 1.5) requires root block
275
+ // to always be 'dag-pb' and not 'raw'. Below adjusts raw-leaves setting, if possible.
276
+ if preserveMode || preserveMtime || mode != 0 || mtime != 0 {
277
+ // Error if --raw-leaves flag was explicitly passed by the user.
278
+ // (let user make a decision to manually disable it and retry)
279
+ if rbset && rawblks {
280
+ return fmt.Errorf("%s can't be used with UnixFS metadata like mode or modification time", rawLeavesOptionName)
281
+ }
282
+ // No explicit preference from user, disable raw-leaves and continue
283
+ rbset = true
284
+ rawblks = false
285
+ }
286
+
287
if onlyHash && toFilesSet {
288
return fmt.Errorf("%s and %s options are not compatible", onlyHashOptionName, toFilesOptionName)
289
}
@@ -272,6 +320,19 @@ See 'dag export' and 'dag import' for more information.
320
321
options.Unixfs.Progress(progress),
322
options.Unixfs.Silent(silent),
323
+
324
+ options.Unixfs.PreserveMode(preserveMode),
325
+ options.Unixfs.PreserveMtime(preserveMtime),
326
+ }
327
+
328
+ if mode != 0 {
329
+ opts = append(opts, options.Unixfs.Mode(os.FileMode(mode)))
330
+ }
331
+
332
+ if mtime != 0 {
333
+ opts = append(opts, options.Unixfs.Mtime(mtime, uint32(mtimeNsecs)))
334
+ } else if mtimeNsecs != 0 {
335
+ return fmt.Errorf("option %q requires %q to be provided as well", mtimeNsecsOptionName, mtimeOptionName)
336
}
337
338
if cidVerSet {
@@ -383,12 +444,33 @@ See 'dag export' and 'dag import' for more information.
444
output.Name = gopath.Join(addit.Name(), output.Name)
445
}
446
386
- if err := res.Emit(&AddEvent{
387
- Name: output.Name,
388
- Hash: h,
389
- Bytes: output.Bytes,
390
- Size: output.Size,
391
- }); err != nil {
447
+ output.Mode = addit.Node().Mode()
448
+ if ts := addit.Node().ModTime(); !ts.IsZero() {
449
+ output.Mtime = addit.Node().ModTime().Unix()
450
+ output.MtimeNsecs = addit.Node().ModTime().Nanosecond()
451
+ }
452
+
453
+ addEvent := AddEvent{
454
+ Name: output.Name,
455
+ Hash: h,
456
+ Bytes: output.Bytes,
457
+ Size: output.Size,
458
+ Mtime: output.Mtime,
459
+ MtimeNsecs: output.MtimeNsecs,
460
+ }
461
+
462
+ if output.Mode != 0 {
463
+ addEvent.Mode = "0" + strconv.FormatUint(uint64(output.Mode), 8)
464
+ }
465
+
466
+ if output.Mtime > 0 {
467
+ addEvent.Mtime = output.Mtime
468
+ if output.MtimeNsecs > 0 {
469
+ addEvent.MtimeNsecs = output.MtimeNsecs
470
+ }
471
+ }
472
+
473
+ if err := res.Emit(&addEvent); err != nil {
474
return err
475
}
476
}
core/commands/commands_test.go
+2
@@ -89,6 +89,8 @@ func TestCommands(t *testing.T) {
89
"/files/rm",
90
"/files/stat",
91
"/files/write",
92
+ "/files/chmod",
93
+ "/files/touch",
94
"/filestore",
95
"/filestore/dups",
96
"/filestore/ls",
core/commands/files.go
+188
-25
@@ -2,13 +2,16 @@ package commands
2
3
import (
4
"context"
5
+ "encoding/json"
6
"errors"
7
"fmt"
8
"io"
9
"os"
10
gopath "path"
11
"sort"
12
+ "strconv"
13
"strings"
14
+ "time"
15
16
humanize "github.com/dustin/go-humanize"
17
"github.com/ipfs/kubo/config"
@@ -81,6 +84,8 @@ operations.
84
"rm": filesRmCmd,
85
"flush": filesFlushCmd,
86
"chcid": filesChcidCmd,
87
+ "chmod": filesChmodCmd,
88
+ "touch": filesTouchCmd,
89
},
90
}
91
@@ -105,6 +110,43 @@ type statOutput struct {
110
WithLocality bool `json:",omitempty"`
111
Local bool `json:",omitempty"`
112
SizeLocal uint64 `json:",omitempty"`
113
+ Mode uint32 `json:",omitempty"`
114
+ Mtime int64 `json:",omitempty"`
115
+ MtimeNsecs int `json:",omitempty"`
116
+}
117
+
118
+func (s *statOutput) MarshalJSON() ([]byte, error) {
119
+ type so statOutput
120
+ out := &struct {
121
+ *so
122
+ Mode string `json:",omitempty"`
123
+ }{so: (*so)(s)}
124
+
125
+ if s.Mode != 0 {
126
+ out.Mode = fmt.Sprintf("%04o", s.Mode)
127
+ }
128
+ return json.Marshal(out)
129
+}
130
+
131
+func (s *statOutput) UnmarshalJSON(data []byte) error {
132
+ var err error
133
+ type so statOutput
134
+ tmp := &struct {
135
+ *so
136
+ Mode string `json:",omitempty"`
137
+ }{so: (*so)(s)}
138
+
139
+ if err := json.Unmarshal(data, &tmp); err != nil {
140
+ return err
141
+ }
142
+
143
+ if tmp.Mode != "" {
144
+ mode, err := strconv.ParseUint(tmp.Mode, 8, 32)
145
+ if err == nil {
146
+ s.Mode = uint32(mode)
147
+ }
148
+ }
149
+ return err
150
}
151
152
const (
@@ -112,10 +154,13 @@ const (
154
Size: <size>
155
CumulativeSize: <cumulsize>
156
ChildBlocks: <childs>
115
-Type: <type>`
157
+Type: <type>
158
+Mode: <mode> (<mode-octal>)
159
+Mtime: <mtime>`
160
filesFormatOptionName = "format"
161
filesSizeOptionName = "size"
162
filesWithLocalOptionName = "with-local"
163
+ filesStatUnspecified = "not set"
164
)
165
166
var filesStatCmd = &cmds.Command{
@@ -128,7 +173,8 @@ var filesStatCmd = &cmds.Command{
173
},
174
Options: []cmds.Option{
175
cmds.StringOption(filesFormatOptionName, "Print statistics in given format. Allowed tokens: "+
131
- "<hash> <size> <cumulsize> <type> <childs>. Conflicts with other format options.").WithDefault(defaultStatFormat),
176
+ "<hash> <size> <cumulsize> <type> <childs> and optional <mode> <mode-octal> <mtime> <mtime-secs> <mtime-nsecs>."+
177
+ "Conflicts with other format options.").WithDefault(defaultStatFormat),
178
cmds.BoolOption(filesHashOptionName, "Print only hash. Implies '--format=<hash>'. Conflicts with other format options."),
179
cmds.BoolOption(filesSizeOptionName, "Print only size. Implies '--format=<cumulsize>'. Conflicts with other format options."),
180
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{
245
},
246
Encoders: cmds.EncoderMap{
247
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *statOutput) error {
248
+ mode, modeo := filesStatUnspecified, filesStatUnspecified
249
+ if out.Mode != 0 {
250
+ mode = strings.ToLower(os.FileMode(out.Mode).String())
251
+ modeo = "0" + strconv.FormatInt(int64(out.Mode&0x1FF), 8)
252
+ }
253
+ mtime, mtimes, mtimens := filesStatUnspecified, filesStatUnspecified, filesStatUnspecified
254
+ if out.Mtime > 0 {
255
+ mtime = time.Unix(out.Mtime, int64(out.MtimeNsecs)).UTC().Format("2 Jan 2006, 15:04:05 MST")
256
+ mtimes = strconv.FormatInt(out.Mtime, 10)
257
+ mtimens = strconv.Itoa(out.MtimeNsecs)
258
+ }
259
+
260
s, _ := statGetFormatOptions(req)
261
s = strings.Replace(s, "<hash>", out.Hash, -1)
262
s = strings.Replace(s, "<size>", fmt.Sprintf("%d", out.Size), -1)
263
s = strings.Replace(s, "<cumulsize>", fmt.Sprintf("%d", out.CumulativeSize), -1)
264
s = strings.Replace(s, "<childs>", fmt.Sprintf("%d", out.Blocks), -1)
265
s = strings.Replace(s, "<type>", out.Type, -1)
266
+ s = strings.Replace(s, "<mode>", mode, -1)
267
+ s = strings.Replace(s, "<mode-octal>", modeo, -1)
268
+ s = strings.Replace(s, "<mtime>", mtime, -1)
269
+ s = strings.Replace(s, "<mtime-secs>", mtimes, -1)
270
+ s = strings.Replace(s, "<mtime-nsecs>", mtimens, -1)
271
272
fmt.Fprintln(w, s)
273
@@ -254,28 +317,7 @@ func statNode(nd ipld.Node, enc cidenc.Encoder) (*statOutput, error) {
317
318
switch n := nd.(type) {
319
case *dag.ProtoNode:
257
- d, err := ft.FSNodeFromBytes(n.Data())
258
- if err != nil {
259
- return nil, err
260
- }
261
-
262
- var ndtype string
263
- switch d.Type() {
264
- case ft.TDirectory, ft.THAMTShard:
265
- ndtype = "directory"
266
- case ft.TFile, ft.TMetadata, ft.TRaw:
267
- ndtype = "file"
268
- default:
269
- return nil, fmt.Errorf("unrecognized node type: %s", d.Type())
270
- }
271
-
272
- return &statOutput{
273
- Hash: enc.Encode(c),
274
- Blocks: len(nd.Links()),
275
- Size: d.FileSize(),
276
- CumulativeSize: cumulsize,
277
- Type: ndtype,
278
- }, nil
320
+ return statProtoNode(n, enc, c, cumulsize)
321
case *dag.RawNode:
322
return &statOutput{
323
Hash: enc.Encode(c),
@@ -289,6 +331,44 @@ func statNode(nd ipld.Node, enc cidenc.Encoder) (*statOutput, error) {
331
}
332
}
333
334
+func statProtoNode(n *dag.ProtoNode, enc cidenc.Encoder, cid cid.Cid, cumulsize uint64) (*statOutput, error) {
335
+ d, err := ft.FSNodeFromBytes(n.Data())
336
+ if err != nil {
337
+ return nil, err
338
+ }
339
+
340
+ stat := statOutput{
341
+ Hash: enc.Encode(cid),
342
+ Blocks: len(n.Links()),
343
+ Size: d.FileSize(),
344
+ CumulativeSize: cumulsize,
345
+ }
346
+
347
+ switch d.Type() {
348
+ case ft.TDirectory, ft.THAMTShard:
349
+ stat.Type = "directory"
350
+ case ft.TFile, ft.TSymlink, ft.TMetadata, ft.TRaw:
351
+ stat.Type = "file"
352
+ default:
353
+ return nil, fmt.Errorf("unrecognized node type: %s", d.Type())
354
+ }
355
+
356
+ if mode := d.Mode(); mode != 0 {
357
+ stat.Mode = uint32(mode)
358
+ } else if d.Type() == ft.TSymlink {
359
+ stat.Mode = uint32(os.ModeSymlink | 0x1FF)
360
+ }
361
+
362
+ if mt := d.ModTime(); !mt.IsZero() {
363
+ stat.Mtime = mt.Unix()
364
+ if ns := mt.Nanosecond(); ns > 0 {
365
+ stat.MtimeNsecs = ns
366
+ }
367
+ }
368
+
369
+ return &stat, nil
370
+}
371
+
372
func walkBlock(ctx context.Context, dagserv ipld.DAGService, nd ipld.Node) (bool, uint64, error) {
373
// Start with the block data size
374
sizeLocal := uint64(len(nd.RawData()))
@@ -341,7 +421,7 @@ $ ipfs add --quieter --pin=false <your file>
421
$ ipfs files cp /ipfs/<CID> /your/desired/mfs/path
422
423
If you wish to fully copy content from a different IPFS peer into MFS, do not
344
-forget to force IPFS to fetch to full DAG after doing the "cp" operation. i.e:
424
+forget to force IPFS to fetch the full DAG after doing a "cp" operation. i.e:
425
426
$ ipfs files cp /ipfs/<CID> /your/desired/mfs/path
427
$ ipfs pin add <CID>
@@ -1313,3 +1393,86 @@ func getParentDir(root *mfs.Root, dir string) (*mfs.Directory, error) {
1393
}
1394
return pdir, nil
1395
}
1396
+
1397
+var filesChmodCmd = &cmds.Command{
1398
+ Status: cmds.Experimental,
1399
+ Helptext: cmds.HelpText{
1400
+ Tagline: "Change optional POSIX mode permissions",
1401
+ ShortDescription: `
1402
+The mode argument must be specified in Unix numeric notation.
1403
+
1404
+ $ ipfs files chmod 0644 /foo
1405
+ $ ipfs files stat /foo
1406
+ ...
1407
+ Type: file
1408
+ Mode: -rw-r--r-- (0644)
1409
+ ...
1410
+`,
1411
+ },
1412
+ Arguments: []cmds.Argument{
1413
+ cmds.StringArg("mode", true, false, "Mode to apply to node (numeric notation)"),
1414
+ cmds.StringArg("path", true, false, "Path to apply mode"),
1415
+ },
1416
+ Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1417
+ nd, err := cmdenv.GetNode(env)
1418
+ if err != nil {
1419
+ return err
1420
+ }
1421
+
1422
+ path, err := checkPath(req.Arguments[1])
1423
+ if err != nil {
1424
+ return err
1425
+ }
1426
+
1427
+ mode, err := strconv.ParseInt(req.Arguments[0], 8, 32)
1428
+ if err != nil {
1429
+ return err
1430
+ }
1431
+
1432
+ return mfs.Chmod(nd.FilesRoot, path, os.FileMode(mode))
1433
+ },
1434
+}
1435
+
1436
+var filesTouchCmd = &cmds.Command{
1437
+ Status: cmds.Experimental,
1438
+ Helptext: cmds.HelpText{
1439
+ Tagline: "Set or change optional POSIX modification times.",
1440
+ ShortDescription: `
1441
+Examples:
1442
+ # set modification time to now.
1443
+ $ ipfs files touch /foo
1444
+ # set a custom modification time.
1445
+ $ ipfs files touch --mtime=1630937926 /foo
1446
+`,
1447
+ },
1448
+ Arguments: []cmds.Argument{
1449
+ cmds.StringArg("path", true, false, "Path of target to update."),
1450
+ },
1451
+ Options: []cmds.Option{
1452
+ cmds.Int64Option(mtimeOptionName, "Modification time in seconds before or since the Unix Epoch to apply to created UnixFS entries."),
1453
+ cmds.UintOption(mtimeNsecsOptionName, "Modification time fraction in nanoseconds"),
1454
+ },
1455
+ Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
1456
+ nd, err := cmdenv.GetNode(env)
1457
+ if err != nil {
1458
+ return err
1459
+ }
1460
+
1461
+ path, err := checkPath(req.Arguments[0])
1462
+ if err != nil {
1463
+ return err
1464
+ }
1465
+
1466
+ mtime, _ := req.Options[mtimeOptionName].(int64)
1467
+ nsecs, _ := req.Options[mtimeNsecsOptionName].(uint)
1468
+
1469
+ var ts time.Time
1470
+ if mtime != 0 {
1471
+ ts = time.Unix(mtime, int64(nsecs)).UTC()
1472
+ } else {
1473
+ ts = time.Now().UTC()
1474
+ }
1475
+
1476
+ return mfs.Touch(nd.FilesRoot, path, ts)
1477
+ },
1478
+}
core/commands/get.go
+8
-1
@@ -1,6 +1,7 @@
1
package commands
2
3
import (
4
+ gotar "archive/tar"
5
"bufio"
6
"compress/gzip"
7
"errors"
@@ -331,7 +332,8 @@ func fileArchive(f files.Node, name string, archive bool, compression int) (io.R
332
closeGzwAndPipe() // everything seems to be ok
333
}()
334
} else {
334
- // the case for 1. archive, and 2. not archived and not compressed, in which tar is used anyway as a transport format
335
+ // the case for 1. archive, and 2. not archived and not compressed, in
336
+ // which tar is used anyway as a transport format
337
338
// construct the tar writer
339
w, err := files.NewTarWriter(maybeGzw)
@@ -339,6 +341,11 @@ func fileArchive(f files.Node, name string, archive bool, compression int) (io.R
341
return nil, err
342
}
343
344
+ // if not creating an archive set the format to PAX in order to preserve nanoseconds
345
+ if !archive {
346
+ w.SetFormat(gotar.FormatPAX)
347
+ }
348
+
349
go func() {
350
// write all the nodes recursively
351
if err := w.WriteFile(f, filename); checkErrAndClosePipe(err) {
core/commands/ls.go
+7
@@ -6,6 +6,7 @@ import (
6
"os"
7
"sort"
8
"text/tabwriter"
9
+ "time"
10
11
cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
12
"github.com/ipfs/kubo/core/commands/cmdutils"
@@ -23,6 +24,8 @@ type LsLink struct {
24
Size uint64
25
Type unixfs_pb.Data_DataType
26
Target string
27
+ Mode os.FileMode
28
+ ModTime time.Time
29
}
30
31
// LsObject is an element of LsOutput
@@ -163,6 +166,9 @@ The JSON output contains type information.
166
Size: link.Size,
167
Type: ftype,
168
Target: link.Target,
169
+
170
+ Mode: link.Mode,
171
+ ModTime: link.ModTime,
172
}
173
if err := processLink(paths[i], lsLink); err != nil {
174
return err
@@ -256,6 +262,7 @@ func tabularOutput(req *cmds.Request, w io.Writer, out *LsOutput, lastObjectHash
262
}
263
}
264
265
+ // TODO: Print link.Mode and link.ModTime?
266
fmt.Fprintf(tw, s, link.Hash, link.Size, cmdenv.EscNonPrint(link.Name))
267
}
268
}
core/coreapi/unixfs.go
+6
@@ -130,6 +130,10 @@ func (api *UnixfsAPI) Add(ctx context.Context, files files.Node, opts ...options
130
fileAdder.RawLeaves = settings.RawLeaves
131
fileAdder.NoCopy = settings.NoCopy
132
fileAdder.CidBuilder = prefix
133
+ fileAdder.PreserveMode = settings.PreserveMode
134
+ fileAdder.PreserveMtime = settings.PreserveMtime
135
+ fileAdder.FileMode = settings.Mode
136
+ fileAdder.FileMtime = settings.Mtime
137
138
switch settings.Layout {
139
case options.BalancedLayout:
@@ -270,6 +274,8 @@ func (api *UnixfsAPI) processLink(ctx context.Context, linkres ft.LinkResult, se
274
if !settings.UseCumulativeSize {
275
lnk.Size = d.FileSize()
276
}
277
+ lnk.Mode = d.Mode()
278
+ lnk.ModTime = d.ModTime()
279
}
280
}
281
core/coreiface/options/unixfs.go
+55
@@ -3,6 +3,8 @@ package options
3
import (
4
"errors"
5
"fmt"
6
+ "os"
7
+ "time"
8
9
dag "github.com/ipfs/boxo/ipld/merkledag"
10
cid "github.com/ipfs/go-cid"
@@ -36,6 +38,11 @@ type UnixfsAddSettings struct {
38
Events chan<- interface{}
39
Silent bool
40
Progress bool
41
+
42
+ PreserveMode bool
43
+ PreserveMtime bool
44
+ Mode os.FileMode
45
+ Mtime time.Time
46
}
47
48
type UnixfsLsSettings struct {
@@ -69,6 +76,11 @@ func UnixfsAddOptions(opts ...UnixfsAddOption) (*UnixfsAddSettings, cid.Prefix,
76
Events: nil,
77
Silent: false,
78
Progress: false,
79
+
80
+ PreserveMode: false,
81
+ PreserveMtime: false,
82
+ Mode: 0,
83
+ Mtime: time.Time{},
84
}
85
86
for _, opt := range opts {
@@ -106,6 +118,14 @@ func UnixfsAddOptions(opts ...UnixfsAddOption) (*UnixfsAddSettings, cid.Prefix,
118
}
119
}
120
121
+ if !options.Mtime.IsZero() && options.PreserveMtime {
122
+ options.PreserveMtime = false
123
+ }
124
+
125
+ if options.Mode != 0 && options.PreserveMode {
126
+ options.PreserveMode = false
127
+ }
128
+
129
// cidV1 -> raw blocks (by default)
130
if options.CidVersion > 0 && !options.RawLeavesSet {
131
options.RawLeaves = true
@@ -293,3 +313,38 @@ func (unixfsOpts) UseCumulativeSize(use bool) UnixfsLsOption {
313
return nil
314
}
315
}
316
+
317
+// PreserveMode tells the adder to store the file permissions
318
+func (unixfsOpts) PreserveMode(enable bool) UnixfsAddOption {
319
+ return func(settings *UnixfsAddSettings) error {
320
+ settings.PreserveMode = enable
321
+ return nil
322
+ }
323
+}
324
+
325
+// PreserveMtime tells the adder to store the file modification time
326
+func (unixfsOpts) PreserveMtime(enable bool) UnixfsAddOption {
327
+ return func(settings *UnixfsAddSettings) error {
328
+ settings.PreserveMtime = enable
329
+ return nil
330
+ }
331
+}
332
+
333
+// Mode represents a unix file mode
334
+func (unixfsOpts) Mode(mode os.FileMode) UnixfsAddOption {
335
+ return func(settings *UnixfsAddSettings) error {
336
+ settings.Mode = mode
337
+ return nil
338
+ }
339
+}
340
+
341
+// Mtime represents a unix file mtime
342
+func (unixfsOpts) Mtime(seconds int64, nsecs uint32) UnixfsAddOption {
343
+ return func(settings *UnixfsAddSettings) error {
344
+ if nsecs > 999999999 {
345
+ return errors.New("mtime nanoseconds must be in range [1, 999999999]")
346
+ }
347
+ settings.Mtime = time.Unix(seconds, int64(nsecs))
348
+ return nil
349
+ }
350
+}
core/coreiface/unixfs.go
+12
-4
@@ -2,6 +2,8 @@ package iface
2
3
import (
4
"context"
5
+ "os"
6
+ "time"
7
8
"github.com/ipfs/boxo/files"
9
"github.com/ipfs/boxo/path"
@@ -10,10 +12,13 @@ import (
12
)
13
14
type AddEvent struct {
13
- Name string
14
- Path path.ImmutablePath `json:",omitempty"`
15
- Bytes int64 `json:",omitempty"`
16
- Size string `json:",omitempty"`
15
+ Name string
16
+ Path path.ImmutablePath `json:",omitempty"`
17
+ Bytes int64 `json:",omitempty"`
18
+ Size string `json:",omitempty"`
19
+ Mode os.FileMode `json:",omitempty"`
20
+ Mtime int64 `json:",omitempty"`
21
+ MtimeNsecs int `json:",omitempty"`
22
}
23
24
// FileType is an enum of possible UnixFS file types.
@@ -56,6 +61,9 @@ type DirEntry struct {
61
Type FileType // The type of the file.
62
Target string // The symlink target (if a symlink).
63
64
+ Mode os.FileMode
65
+ ModTime time.Time
66
+
67
Err error
68
}
69
core/coreunix/add.go
+50
-5
@@ -5,8 +5,10 @@ import (
5
"errors"
6
"fmt"
7
"io"
8
+ "os"
9
gopath "path"
10
"strconv"
11
+ "time"
12
13
bstore "github.com/ipfs/boxo/blockstore"
14
chunker "github.com/ipfs/boxo/chunker"
@@ -81,6 +83,11 @@ type Adder struct {
83
tempRoot cid.Cid
84
CidBuilder cid.Builder
85
liveNodes uint64
86
+
87
+ PreserveMode bool
88
+ PreserveMtime bool
89
+ FileMode os.FileMode
90
+ FileMtime time.Time
91
}
92
93
func (adder *Adder) mfsRoot() (*mfs.Root, error) {
@@ -113,11 +120,13 @@ func (adder *Adder) add(reader io.Reader) (ipld.Node, error) {
120
}
121
122
params := ihelper.DagBuilderParams{
116
- Dagserv: adder.bufferedDS,
117
- RawLeaves: adder.RawLeaves,
118
- Maxlinks: ihelper.DefaultLinksPerBlock,
119
- NoCopy: adder.NoCopy,
120
- CidBuilder: adder.CidBuilder,
123
+ Dagserv: adder.bufferedDS,
124
+ RawLeaves: adder.RawLeaves,
125
+ Maxlinks: ihelper.DefaultLinksPerBlock,
126
+ NoCopy: adder.NoCopy,
127
+ CidBuilder: adder.CidBuilder,
128
+ FileMode: adder.FileMode,
129
+ FileModTime: adder.FileMtime,
130
}
131
132
db, err := params.New(chnk)
@@ -359,6 +368,14 @@ func (adder *Adder) addFileNode(ctx context.Context, path string, file files.Nod
368
return err
369
}
370
371
+ if adder.PreserveMtime {
372
+ adder.FileMtime = file.ModTime()
373
+ }
374
+
375
+ if adder.PreserveMode {
376
+ adder.FileMode = file.Mode()
377
+ }
378
+
379
if adder.liveNodes >= liveCacheSize {
380
// TODO: A smarter cache that uses some sort of lru cache with an eviction handler
381
mr, err := adder.mfsRoot()
@@ -391,6 +408,18 @@ func (adder *Adder) addSymlink(path string, l *files.Symlink) error {
408
return err
409
}
410
411
+ if !adder.FileMtime.IsZero() {
412
+ fsn, err := unixfs.FSNodeFromBytes(sdata)
413
+ if err != nil {
414
+ return err
415
+ }
416
+
417
+ fsn.SetModTime(adder.FileMtime)
418
+ if sdata, err = fsn.GetBytes(); err != nil {
419
+ return err
420
+ }
421
+ }
422
+
423
dagnode := dag.NodeWithData(sdata)
424
err = dagnode.SetCidBuilder(adder.CidBuilder)
425
if err != nil {
@@ -429,6 +458,20 @@ func (adder *Adder) addFile(path string, file files.File) error {
458
func (adder *Adder) addDir(ctx context.Context, path string, dir files.Directory, toplevel bool) error {
459
log.Infof("adding directory: %s", path)
460
461
+ // if we need to store mode or modification time then create a new root which includes that data
462
+ if toplevel && (adder.FileMode != 0 || !adder.FileMtime.IsZero()) {
463
+ nd := unixfs.EmptyDirNodeWithStat(adder.FileMode, adder.FileMtime)
464
+ err := nd.SetCidBuilder(adder.CidBuilder)
465
+ if err != nil {
466
+ return err
467
+ }
468
+ mr, err := mfs.NewRoot(ctx, adder.dagService, nd, nil)
469
+ if err != nil {
470
+ return err
471
+ }
472
+ adder.SetMfsRoot(mr)
473
+ }
474
+
475
if !(toplevel && path == "") {
476
mr, err := adder.mfsRoot()
477
if err != nil {
@@ -438,6 +481,8 @@ func (adder *Adder) addDir(ctx context.Context, path string, dir files.Directory
481
Mkparents: true,
482
Flush: false,
483
CidBuilder: adder.CidBuilder,
484
+ Mode: adder.FileMode,
485
+ ModTime: adder.FileMtime,
486
})
487
if err != nil {
488
return err
core/node/storage.go
+1
-1
@@ -56,7 +56,7 @@ func GcBlockstoreCtor(bb BaseBlocks) (gclocker blockstore.GCLocker, gcbs blockst
56
return
57
}
58
59
-// GcBlockstoreCtor wraps GcBlockstore and adds Filestore support
59
+// FilestoreBlockstoreCtor wraps GcBlockstore and adds Filestore support
60
func FilestoreBlockstoreCtor(repo repo.Repo, bb BaseBlocks) (gclocker blockstore.GCLocker, gcbs blockstore.GCBlockstore, bs blockstore.Blockstore, fstore *filestore.Filestore) {
61
gclocker = blockstore.NewGCLocker()
62
docs/changelogs/v0.30.md
+32
@@ -12,6 +12,7 @@
12
- [Version Suffix Configuration](#version-suffix-configuration)
13
- [`/unix/` socket support in `Addresses.API`](#unix-socket-support-in-addressesapi)
14
- [Cleaned Up `ipfs daemon` Startup Log](#cleaned-up-ipfs-daemon-startup-log)
15
+ - [UnixFS 1.5: Mode and Modification Time Support](#unixfs-15-mode-and-modification-time-support)
16
- [📝 Changelog](#-changelog)
17
- [👨👩👧👦 Contributors](#-contributors)
18
@@ -96,6 +97,37 @@ The previous lengthy listing of all listener and announced multiaddrs has been r
97
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.
98
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.
99
100
+#### UnixFS 1.5: Mode and Modification Time Support
101
+
102
+Kubo now allows users to opt-in to store mode and modification time for files, directories, and symbolic links.
103
+By default, if you do not opt-in, the old behavior remains unchanged, and the same CIDs will be generated as before.
104
+
105
+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:
106
+
107
+```console
108
+$ touch ./file
109
+$ chmod 654 ./file
110
+$ ipfs add --preserve-mode --preserve-mtime -Q ./file
111
+QmczQr4XS1rRnWVopyg5Chr9EQ7JKpbhgnrjpb5kTQ1DKQ
112
+
113
+$ ipfs files stat /ipfs/QmczQr4XS1rRnWVopyg5Chr9EQ7JKpbhgnrjpb5kTQ1DKQ
114
+QmczQr4XS1rRnWVopyg5Chr9EQ7JKpbhgnrjpb5kTQ1DKQ
115
+Size: 0
116
+CumulativeSize: 22
117
+ChildBlocks: 0
118
+Type: file
119
+Mode: -rw-r-xr-- (0654)
120
+Mtime: 13 Aug 2024, 21:15:31 UTC
121
+```
122
+
123
+The CLI and HTTP RPC options `--mode`, `--mtime` and `--mtime-nsecs` can be used to set them to arbitrary values.
124
+
125
+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.
126
+
127
+
128
+> [!NOTE]
129
+> Storing `mode` and `mtime` requires root block to be `dag-pb` and disabled `raw-leaves` setting to create envelope for storing the metadata.
130
+
131
### 📝 Changelog
132
133
### 👨👩👧👦 Contributors
docs/examples/kubo-as-a-library/go.mod
+1
-1
@@ -9,7 +9,7 @@ toolchain go1.22.0
9
replace github.com/ipfs/kubo => ./../../..
10
11
require (
12
- github.com/ipfs/boxo v0.22.0
12
+ github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053
13
github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
14
github.com/libp2p/go-libp2p v0.36.2
15
github.com/multiformats/go-multiaddr v0.13.0
docs/examples/kubo-as-a-library/go.sum
+2
-2
@@ -266,8 +266,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7Uy
266
github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c/go.mod h1:6EekK/jo+TynwSE/ZOiOJd4eEvRXoavEC3vquKtv4yI=
267
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
268
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
269
-github.com/ipfs/boxo v0.22.0 h1:QTC+P5uhsBNq6HzX728nsLyFW6rYDeR/5hggf9YZX78=
270
-github.com/ipfs/boxo v0.22.0/go.mod h1:yp1loimX0BDYOR0cyjtcXHv15muEh5V1FqO2QLlzykw=
269
+github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 h1:rW0xGaZW9+74cc8etCm6DwrHhIEtNxklFn8YrUaWjx4=
270
+github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053/go.mod h1:bMB1tnSTr+6/CS5p3jkS4rtifpl+ul6P4ZgeTZn8Ty0=
271
github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
272
github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
273
github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ=
go.mod
+1
-1
@@ -18,7 +18,7 @@ require (
18
github.com/hashicorp/go-version v1.6.0
19
github.com/ipfs-shipyard/nopfs v0.0.12
20
github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c
21
- github.com/ipfs/boxo v0.22.0
21
+ github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053
22
github.com/ipfs/go-block-format v0.2.0
23
github.com/ipfs/go-cid v0.4.1
24
github.com/ipfs/go-cidutil v0.1.0
go.sum
+2
-2
@@ -330,8 +330,8 @@ github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c h1:7Uy
330
github.com/ipfs-shipyard/nopfs/ipfs v0.13.2-0.20231027223058-cde3b5ba964c/go.mod h1:6EekK/jo+TynwSE/ZOiOJd4eEvRXoavEC3vquKtv4yI=
331
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
332
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
333
-github.com/ipfs/boxo v0.22.0 h1:QTC+P5uhsBNq6HzX728nsLyFW6rYDeR/5hggf9YZX78=
334
-github.com/ipfs/boxo v0.22.0/go.mod h1:yp1loimX0BDYOR0cyjtcXHv15muEh5V1FqO2QLlzykw=
333
+github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 h1:rW0xGaZW9+74cc8etCm6DwrHhIEtNxklFn8YrUaWjx4=
334
+github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053/go.mod h1:bMB1tnSTr+6/CS5p3jkS4rtifpl+ul6P4ZgeTZn8Ty0=
335
github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
336
github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
337
github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ=
test/dependencies/go.mod
+1
-1
@@ -113,7 +113,7 @@ require (
113
github.com/hexops/gotextdiff v1.0.3 // indirect
114
github.com/inconshreveable/mousetrap v1.1.0 // indirect
115
github.com/ipfs/bbloom v0.0.4 // indirect
116
- github.com/ipfs/boxo v0.22.0 // indirect
116
+ github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 // indirect
117
github.com/ipfs/go-block-format v0.2.0 // indirect
118
github.com/ipfs/go-cid v0.4.1 // indirect
119
github.com/ipfs/go-datastore v0.6.0 // indirect
test/dependencies/go.sum
+2
-4
@@ -280,8 +280,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
280
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
281
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
282
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
283
-github.com/ipfs/boxo v0.22.0 h1:QTC+P5uhsBNq6HzX728nsLyFW6rYDeR/5hggf9YZX78=
284
-github.com/ipfs/boxo v0.22.0/go.mod h1:yp1loimX0BDYOR0cyjtcXHv15muEh5V1FqO2QLlzykw=
283
+github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053 h1:rW0xGaZW9+74cc8etCm6DwrHhIEtNxklFn8YrUaWjx4=
284
+github.com/ipfs/boxo v0.22.1-0.20240820234446-aa27cd2f8053/go.mod h1:bMB1tnSTr+6/CS5p3jkS4rtifpl+ul6P4ZgeTZn8Ty0=
285
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
286
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
287
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
292
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
293
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
294
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
295
-github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ=
296
-github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk=
295
github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ=
296
github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
297
github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE=
test/sharness/t0047-add-mode-mtime.sh
new
+513
@@ -0,0 +1,513 @@
1
+#!/usr/bin/env bash
2
+
3
+test_description="Test storing and retrieving mode and mtime"
4
+
5
+. lib/test-lib.sh
6
+
7
+test_init_ipfs
8
+
9
+test_expect_success "set Import defaults to ensure deterministic cids for mod and mtime tests" '
10
+ ipfs config --json Import.CidVersion 0 &&
11
+ ipfs config Import.HashFunction sha2-256 &&
12
+ ipfs config Import.UnixFSChunker size-262144
13
+'
14
+
15
+HASH_NO_PRESERVE=QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH
16
+
17
+PRESERVE_MTIME=1604320482
18
+PRESERVE_MODE="0640"
19
+HASH_PRESERVE_MODE=QmQLgxypSNGNFTuUPGCecq6dDEjb6hNB5xSyVmP3cEuNtq
20
+HASH_PRESERVE_MTIME=QmQ6kErEW8kztQFV8vbwNU8E4dmtGsYpRiboiLxUEwibvj
21
+HASH_PRESERVE_LINK_MTIME=QmbJwotgtr84JxcnjpwJ86uZiyMoxbZuNH4YrdJMypkYaB
22
+HASH_PRESERVE_MODE_AND_MTIME=QmYkvboLsvLFcSYmqVJRxvBdYRQLroLv9kELf3LRiCqBri
23
+
24
+CUSTOM_MTIME=1603539720
25
+CUSTOM_MTIME_NSECS=54321
26
+CUSTOM_MODE="0764"
27
+HASH_CUSTOM_MODE=QmchD3BN8TQ3RW6jPLxSaNkqvfuj7syKhzTRmL4EpyY1Nz
28
+HASH_CUSTOM_MTIME=QmT3aY4avDcYXCWpU8CJzqUkW7YEuEsx36S8cTNoLcuK1B
29
+HASH_CUSTOM_MTIME_NSECS=QmaKH8H5rXBUBCX4vdxi7ktGQEL7wejV7L9rX2qpZjwncz
30
+HASH_CUSTOM_MODE_AND_MTIME=QmUkxrtBA8tPjwCYz1HrsoRfDz6NgKut3asVeHVQNH4C8L
31
+HASH_CUSTOM_LINK_MTIME=QmV1Uot2gy4bhY9yvYiZxhhchhyYC6MKKoGV1XtWNmpCLe
32
+HASH_CUSTOM_LINK_MTIME_NSECS=QmPHYCxYvvHj6VxiPNJ3kXxcPsnJLDYUJqsDJWjvytmrmY
33
+
34
+mk_name() {
35
+ tr -dc '[:alnum:]'</dev/urandom|head -c 16
36
+}
37
+
38
+mk_file() {
39
+ mktemp -p "$SHARNESS_TRASH_DIRECTORY" "mk_file_${1}_XXXXXX"
40
+}
41
+
42
+mk_dir() {
43
+ mktemp -d -p "$SHARNESS_TRASH_DIRECTORY" "mk_dir_${1}_XXXXXX"
44
+}
45
+
46
+# force umask for deterministic mode on files created via touch
47
+# (https://github.com/orgs/community/discussions/40876, https://github.com/ipfs/kubo/pull/10478/#discussion_r1717515514)
48
+umask 022
49
+
50
+FIXTURESDIR="$(mk_dir fixtures)"
51
+
52
+test_file() {
53
+ local TESTFILE="$FIXTURESDIR/test$1.txt"
54
+ local TESTLINK="$FIXTURESDIR/linkfile$1"
55
+
56
+ touch "$TESTFILE"
57
+ ln -s nothing "$TESTLINK"
58
+
59
+ test_expect_success "feature on file has no effect when not used [$1]" '
60
+ touch "$TESTFILE" &&
61
+ HASH=$(ipfs add -q "$TESTFILE") &&
62
+ test "$HASH_NO_PRESERVE" = "$HASH"
63
+ '
64
+
65
+ test_expect_success "can preserve file mode [$1]" '
66
+ touch "$TESTFILE" &&
67
+ chmod $PRESERVE_MODE "$TESTFILE" &&
68
+ HASH=$(ipfs add -q --preserve-mode "$TESTFILE") &&
69
+ test "$HASH_PRESERVE_MODE" = "$HASH"
70
+ '
71
+
72
+ test_expect_success "can preserve file modification time [$1]" '
73
+ touch -m -d @$PRESERVE_MTIME "$TESTFILE" &&
74
+ HASH=$(ipfs add -q --preserve-mtime "$TESTFILE") &&
75
+ test "$HASH_PRESERVE_MTIME" = "$HASH"
76
+ '
77
+
78
+ test_expect_success "can preserve file mode and modification time [$1]" '
79
+ touch -m -d @$PRESERVE_MTIME "$TESTFILE" &&
80
+ chmod $PRESERVE_MODE "$TESTFILE" &&
81
+ HASH=$(ipfs add -q --preserve-mode --preserve-mtime "$TESTFILE") &&
82
+ test "$HASH_PRESERVE_MODE_AND_MTIME" = "$HASH"
83
+ '
84
+
85
+ test_expect_success "can preserve symlink modification time [$1]" '
86
+ touch -h -m -d @$PRESERVE_MTIME "$TESTLINK" &&
87
+ HASH=$(ipfs add -q --preserve-mtime "$TESTLINK") &&
88
+ test "$HASH_PRESERVE_LINK_MTIME" = "$HASH"
89
+ '
90
+
91
+ test_expect_success "can set file mode [$1]" '
92
+ touch "$TESTFILE" &&
93
+ chmod 0600 "$TESTFILE" &&
94
+ HASH=$(ipfs add -q --mode=$CUSTOM_MODE "$TESTFILE") &&
95
+ test "$HASH_CUSTOM_MODE" = "$HASH"
96
+ '
97
+
98
+ test_expect_success "can set file modification time [$1]" '
99
+ touch -m -t 202011021234.42 "$TESTFILE" &&
100
+ HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME "$TESTFILE") &&
101
+ test "$HASH_CUSTOM_MTIME" = "$HASH"
102
+ '
103
+
104
+ test_expect_success "can set file modification time nanoseconds [$1]" '
105
+ touch -m -t 202011021234.42 "$TESTFILE" &&
106
+ HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$TESTFILE") &&
107
+ test "$HASH_CUSTOM_MTIME_NSECS" = "$HASH"
108
+ '
109
+
110
+ test_expect_success "can set file mode and modification time [$1]" '
111
+ touch -m -t 202011021234.42 "$TESTFILE" &&
112
+ chmod 0600 "$TESTFILE" &&
113
+ HASH=$(ipfs add -q --mode=$CUSTOM_MODE --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$TESTFILE") &&
114
+ test "$HASH_CUSTOM_MODE_AND_MTIME" = "$HASH"
115
+ '
116
+
117
+ test_expect_success "can set symlink modification time [$1]" '
118
+ touch -h -m -t 202011021234.42 "$TESTLINK" &&
119
+ HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME "$TESTLINK") &&
120
+ test "$HASH_CUSTOM_LINK_MTIME" = "$HASH"
121
+ '
122
+
123
+ test_expect_success "cannot set mode on symbolic link" '
124
+ HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME --mode=$CUSTOM_MODE "$TESTLINK") &&
125
+ ACTUAL=$(ipfs files stat --format="<mode>" /ipfs/$HASH) &&
126
+ test "$ACTUAL" = "lrwxrwxrwx"
127
+ '
128
+
129
+
130
+ test_expect_success "can set symlink modification time nanoseconds [$1]" '
131
+ touch -h -m -t 202011021234.42 "$TESTLINK" &&
132
+ HASH=$(ipfs add -q --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$TESTLINK") &&
133
+ test "$HASH_CUSTOM_LINK_MTIME_NSECS" = "$HASH"
134
+ '
135
+
136
+ test_expect_success "can get preserved mode and modification time [$1]" '
137
+ OUTFILE="$(mk_file $HASH_PRESERVE_MODE_AND_MTIME)" &&
138
+ ipfs get -o "$OUTFILE" $HASH_PRESERVE_MODE_AND_MTIME &&
139
+ test "$PRESERVE_MODE:$PRESERVE_MTIME" = "$(stat -c "0%a:%Y" "$OUTFILE")"
140
+ '
141
+
142
+ test_expect_success "can get custom mode and modification time [$1]" '
143
+ OUTFILE="$(mk_file $HASH_CUSTOM_MODE_AND_MTIME)" &&
144
+ ipfs get -o "$OUTFILE" $HASH_CUSTOM_MODE_AND_MTIME &&
145
+ TIMESTAMP=$(date +%s%N --date="$(stat -c "%y" "$OUTFILE")") &&
146
+ MODETIME=$(stat -c "0%a:$TIMESTAMP" "$OUTFILE") &&
147
+ printf -v EXPECTED "$CUSTOM_MODE:$CUSTOM_MTIME%09d" $CUSTOM_MTIME_NSECS &&
148
+ test "$EXPECTED" = "$MODETIME"
149
+ '
150
+
151
+ test_expect_success "can get custom symlink modification time [$1]" '
152
+ OUTFILE="$(mk_file $HASH_CUSTOM_LINK_MTIME_NSECS)" &&
153
+ ipfs get -o "$OUTFILE" $HASH_CUSTOM_LINK_MTIME_NSECS &&
154
+ TIMESTAMP=$(date +%s%N --date="$(stat -c "%y" "$OUTFILE")") &&
155
+ printf -v EXPECTED "$CUSTOM_MTIME%09d" $CUSTOM_MTIME_NSECS &&
156
+ test "$EXPECTED" = "$TIMESTAMP"
157
+ '
158
+
159
+ test_expect_success "can change file mode [$1]" '
160
+ NAME=$(mk_name) &&
161
+ HASH=$(echo testfile | ipfs add -q --mode=0600) &&
162
+ OUTFILE=$(mk_file "${NAME}") &&
163
+ ipfs files cp "/ipfs/$HASH" /$NAME &&
164
+ ipfs files chmod 444 /$NAME &&
165
+ HASH2=$(ipfs files stat /$NAME|head -1) &&
166
+ ipfs get -o "$OUTFILE" $HASH2 &&
167
+ test $(stat -c "%a" "$OUTFILE") = 444
168
+ '
169
+
170
+ # special case, because storing mode requires dag-pb envelope
171
+ # and when dealing with CIDv1 we can have 'raw' block instead of 'dag-pb'
172
+ # so it needs to be converted before adding attribute
173
+ test_expect_success "can add file mode to cidv1 raw block [$1]" '
174
+ NAME=$(mk_name) &&
175
+ HASH=$(date | ipfs add -q --cid-version 1 --raw-leaves=true) &&
176
+ OUTFILE=$(mk_file "${NAME}") &&
177
+ ipfs files cp "/ipfs/$HASH" /$NAME &&
178
+ ipfs files chmod 445 /$NAME &&
179
+ HASH2=$(ipfs files stat /$NAME|head -1) &&
180
+ ipfs get -o "$OUTFILE" $HASH2 &&
181
+ test $(stat -c "%a" "$OUTFILE") = 445
182
+ '
183
+
184
+ test_expect_success "can change file modification time [$1]" '
185
+ NAME=$(mk_name) &&
186
+ OUTFILE="$(mk_file "$NAME")" &&
187
+ NOW=$(date +%s) &&
188
+ HASH=$(echo testfile | ipfs add -q --mtime=$NOW) &&
189
+ ipfs files cp "/ipfs/$HASH" /$NAME &&
190
+ sleep 1 &&
191
+ ipfs files touch /$NAME &&
192
+ HASH=$(ipfs files stat /$NAME|head -1) &&
193
+ ipfs get -o "$OUTFILE" "$HASH" &&
194
+ test $(stat -c "%Y" "$OUTFILE") -gt $NOW
195
+ '
196
+
197
+ # special case, because storing mtime requires dag-pb envelope
198
+ # and when dealing with CIDv1 we can have 'raw' block instead of 'dag-pb'
199
+ # so it needs to be converted to dag-pb before adding attribute
200
+ test_expect_success "can add file modification time to cidv1 raw block [$1]" '
201
+ NAME=$(mk_name) &&
202
+ OUTFILE="$(mk_file "$NAME")" &&
203
+ EXPECTED="$CUSTOM_MTIME" &&
204
+ HASH=$(date | ipfs add -q --cid-version 1 --raw-leaves=true) &&
205
+ ipfs files cp "/ipfs/$HASH" /$NAME &&
206
+ ipfs files touch --mtime=$EXPECTED /$NAME &&
207
+ test $(ipfs files stat --format="<mtime-secs>" "/$NAME") -eq $EXPECTED &&
208
+ HASH=$(ipfs files stat /$NAME|head -1) &&
209
+ ipfs get -o "$OUTFILE" "$HASH" &&
210
+ test $(stat -c "%Y" "$OUTFILE") -eq $EXPECTED
211
+ '
212
+
213
+ test_expect_success "can change file modification time nanoseconds [$1]" '
214
+ NAME=$(mk_name) &&
215
+ echo test|ipfs files write --create /$NAME &&
216
+ EXPECTED=$(date --date="yesterday" +%s) &&
217
+ ipfs files touch --mtime=$EXPECTED --mtime-nsecs=55567 /$NAME &&
218
+ test $(ipfs files stat --format="<mtime-secs>" /$NAME) -eq $EXPECTED &&
219
+ test $(ipfs files stat --format="<mtime-nsecs>" /$NAME) -eq 55567
220
+ '
221
+
222
+ ## TODO: update these tests if/when symbolic links are fully supported in go-mfs
223
+ test_expect_success "can change symlink modification time [$1]" '
224
+ NAME=$(mk_name) &&
225
+ EXPECTED=$(date +%s) &&
226
+ ipfs files cp "/ipfs/$HASH_PRESERVE_LINK_MTIME" "/$NAME" ||
227
+ ipfs files touch --mtime=$EXPECTED "/$NAME" &&
228
+ test $(ipfs files stat --format="<mtime-secs>" "/$NAME") -eq $EXPECTED
229
+ '
230
+
231
+ test_expect_success "can change symlink modification time nanoseconds [$1]" '
232
+ NAME=$(mk_name) &&
233
+ EXPECTED=$(date +%s) &&
234
+ ipfs files cp "/ipfs/$HASH_PRESERVE_LINK_MTIME" "/$NAME" ||
235
+ ipfs files touch --mtime=$EXPECTED --mtime-nsecs=938475 "/$NAME" &&
236
+ test $(ipfs files stat --format="<mtime-secs>" "/$NAME") -eq $EXPECTED &&
237
+ test $(ipfs files stat --format="<mtime-nsecs>" "/$NAME") -eq 938475
238
+ '
239
+}
240
+
241
+DIR_TIME=1655158632
242
+
243
+setup_directory() {
244
+
245
+ local TESTDIR="$(mktemp -d -p "$FIXTURESDIR" "${1}XXXXXX")"
246
+ mkdir -p "$TESTDIR"/{dir1,dir2/sub1/sub2,dir3}
247
+ chmod 0755 "$TESTDIR/dir1"
248
+
249
+ touch -md @$(($DIR_TIME+10)) "$TESTDIR/dir2/sub1/sub2/file3"
250
+ ln -s ../sub2/file3 "$TESTDIR/dir2/sub1/link1"
251
+ touch -h -md @$(($DIR_TIME+20)) "$TESTDIR/dir2/sub1/link1"
252
+
253
+ touch -md @$(($DIR_TIME+30)) "$TESTDIR/dir2/sub1/sub2"
254
+ touch -md @$(($DIR_TIME+40)) "$TESTDIR/dir2/sub1"
255
+ touch -md @$(($DIR_TIME+50)) "$TESTDIR/dir2"
256
+
257
+ touch -md @$(($DIR_TIME+60)) "$TESTDIR/dir3/file2"
258
+ touch -md @$(($DIR_TIME+70)) "$TESTDIR/dir3"
259
+
260
+ touch -md @$(($DIR_TIME+80)) "$TESTDIR/file1"
261
+ touch -md @$(($DIR_TIME+90)) "$TESTDIR/dir1"
262
+ touch -md @$DIR_TIME "$TESTDIR"
263
+
264
+ echo "$TESTDIR"
265
+}
266
+
267
+test_directory() {
268
+ CUSTOM_DIR_MODE=0713
269
+ TESTDIR=$(setup_directory $1)
270
+ TESTDIR1="$TESTDIR/dir1"
271
+ OUTDIR="$(mk_dir "${1}")"
272
+ HASH_DIR_ROOT=QmSioyvQuXetxg7uo8FswGn9XKKEsisDq1HTMzGyWbw2R6
273
+ HASH_DIR1_NO_PRESERVE=QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
274
+ HASH_DIR1_PRESERVE_MODE=QmRviohgafvCsbkiTgfQFipbuXJ6k1YtoiaQW4quttJPKu
275
+ HASH_DIR1_PRESERVE_MTIME=QmYMy7CZGb498QFSQBF5ZFwv1FYbrAtYZMe4VxhDXxAcvf
276
+ HASH_DIR1_CUSTOM_MODE=QmQ1ABnw2iip7sj23EzzBZ9T77KyyfESP6SUboiXPyzNQe
277
+ HASH_DIR1_CUSTOM_MTIME=QmfWitW6F13WHFXLbJzXRYmwrS1p4gaAJAfucUSMytRPn3
278
+ HASH_DIR1_CUSTOM_MTIME_NSECS=QmZFdCLJay31hT3Tx1LygJ7XfiLEs3qLCXtbeBfhf38aZg
279
+ HASH_DIR_SUB1=QmeQwX5qAX18fcPDxDdkfM6ttuFCZetF5hgeUa6ov8D5oc
280
+
281
+ HASH_DIR_MODE_AND_MTIME=(
282
+ QmRCG3Pprg4jbhfYBzVzfJVyneFHnBquPGXwvXU3jSuf5j
283
+ QmReHCn4BSJJdtd6Le8Hd8Puai6TmgpPCYb13wyM7FD9AD
284
+ QmSioyvQuXetxg7uo8FswGn9XKKEsisDq1HTMzGyWbw2R6
285
+ QmTMoVgJKhPrz9DfkvT132mxyBXNae5azXQ42WbM9abdSE
286
+ QmVzXqpuQGCAgRwEbGuE9xe8Fidi1HEXaPKsQEFEbPJW9j
287
+ QmW6Nqy2nziduAp3UGx2a52gtSUsYzhVcZMuPdxBRnwCyP
288
+ QmeQwX5qAX18fcPDxDdkfM6ttuFCZetF5hgeUa6ov8D5oc
289
+ QmefofUNwC2U3Xp87rB1x8Aws6AdsDuoXR7B9u2RkEZ4dQ
290
+ Qmeu24TFarJwLzJgMTDYDJTr4BMGnzafoSnfxov1513abW
291
+ Qmf82bbFg2e8HmcqiewutVVw5NoMpiXZD57LpLdC1poBuH)
292
+ HASH_DIR_CUSTOM_MODE=(
293
+ QmNZ5cyx3f6maXkczwhh3ufjDCh9f3k9zrDhX218ZZGvoV
294
+ QmRqtFVLkXfWJuqWtYiCPthgomo3gouno8uvMeGAyCVaWS
295
+ QmSkrWNcyDA7s1qiT6Ps7ey4zcB7uBH3sqGcKRfW4UMKhM
296
+ QmSkrWNcyDA7s1qiT6Ps7ey4zcB7uBH3sqGcKRfW4UMKhM
297
+ QmSkrWNcyDA7s1qiT6Ps7ey4zcB7uBH3sqGcKRfW4UMKhM
298
+ QmZNAZXB6JyJ1cK9h1uJEK4XDo1CKsSuHMPGUUMrzDXCQz
299
+ QmbSz6GyS8MNR4M9xtCteuGVJQRYkCXLbW174Fdy8jtaoZ
300
+ QmccnAQQeJGtmtgZoi3hpEmgdxbuX1ao2hQmrKmmwQnCn9
301
+ QmeTZoiAiduFY2hXaNQP4ehiE71BrQFEnrqduBZ5ZjHuFy
302
+ Qmf13KNurvAHUfMBhMWvZuftmUikhhGY7ohWVaBDDndFMz)
303
+ HASH_DIR_CUSTOM_MTIME=(
304
+ QmPCGFZ8ZFowAwfWdCeGsr9wSbGXwZiHW3bZ7XSYcc1Zby
305
+ QmT3aY4avDcYXCWpU8CJzqUkW7YEuEsx36S8cTNoLcuK1B
306
+ QmT3aY4avDcYXCWpU8CJzqUkW7YEuEsx36S8cTNoLcuK1B
307
+ QmT3aY4avDcYXCWpU8CJzqUkW7YEuEsx36S8cTNoLcuK1B
308
+ QmUGMu9epCEz5HMsuJFgpJxxt3HoahsTQcC65Jje6LNqYF
309
+ QmXhzoPKuqmkqbyr4kJFznFRXtGwriCXKGFPr4vviyK3aV
310
+ QmZ5wKCcL11TckypuDTKLLNFP6JMCBJRCn385XKQQ6PCLt
311
+ Qmdw3hiAxn6R5MRkkdzLdFvZUa2WJeLCTXXCyB8byFsHSA
312
+ QmedF4m2Y8341azfkpvaHSkxbSrZa4fo6FT25h6sRUVkpq
313
+ QmfWitW6F13WHFXLbJzXRYmwrS1p4gaAJAfucUSMytRPn3)
314
+
315
+ test_expect_success "feature on directory has no effect when not used [$1]" '
316
+ HASH=$(ipfs add -qr "$TESTDIR1") &&
317
+ test "$HASH_DIR1_NO_PRESERVE" = "$HASH"
318
+ '
319
+
320
+ test_expect_success "can preserve directory mode [$1]" '
321
+ HASH=$(ipfs add -qr --preserve-mode "$TESTDIR1") &&
322
+ test "$HASH_DIR1_PRESERVE_MODE" = "$HASH"
323
+ '
324
+
325
+ test_expect_success "can preserve directory modification time [$1]" '
326
+ HASH=$(ipfs add -qr --preserve-mtime "$TESTDIR1") &&
327
+ test "$HASH_DIR1_PRESERVE_MTIME" = "$HASH"
328
+ '
329
+
330
+ test_expect_success "can set directory mode [$1]" '
331
+ HASH=$(ipfs add -qr --mode=$CUSTOM_DIR_MODE "$TESTDIR1") &&
332
+ test "$HASH_DIR1_CUSTOM_MODE" = "$HASH"
333
+ '
334
+
335
+ test_expect_success "can set directory modification time [$1]" '
336
+ HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME "$TESTDIR1") &&
337
+ test "$HASH_DIR1_CUSTOM_MTIME" = "$HASH"
338
+ '
339
+
340
+ test_expect_success "can set directory modification time nanoseconds [$1]" '
341
+ HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$TESTDIR1") &&
342
+ test "$HASH_DIR1_CUSTOM_MTIME_NSECS" = "$HASH"
343
+ '
344
+
345
+ test_expect_success "can recursively preserve mode and modification time [$1]" '
346
+ test "700:$DIR_TIME" = "$(stat -c "%a:%Y" "$TESTDIR")" &&
347
+ test "644:$((DIR_TIME+10))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/sub2/file3")" &&
348
+ test "777:$((DIR_TIME+20))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/link1")" &&
349
+ test "755:$((DIR_TIME+30))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/sub2")" &&
350
+ test "755:$((DIR_TIME+40))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1")" &&
351
+ test "755:$((DIR_TIME+50))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2")" &&
352
+ test "644:$((DIR_TIME+60))" = "$(stat -c "%a:%Y" "$TESTDIR/dir3/file2")" &&
353
+ test "755:$((DIR_TIME+70))" = "$(stat -c "%a:%Y" "$TESTDIR/dir3")" &&
354
+ test "644:$((DIR_TIME+80))" = "$(stat -c "%a:%Y" "$TESTDIR/file1")" &&
355
+ test "755:$((DIR_TIME+90))" = "$(stat -c "%a:%Y" "$TESTDIR/dir1")" &&
356
+ HASHES=($(ipfs add -qr --preserve-mode --preserve-mtime "$TESTDIR"|sort)) &&
357
+ test "${HASHES[*]}" = "${HASH_DIR_MODE_AND_MTIME[*]}"
358
+ '
359
+
360
+ test_expect_success "can recursively set directory mode [$1]" '
361
+ HASHES=($(ipfs add -qr --mode=0753 "$TESTDIR"|sort)) &&
362
+ test "${HASHES[*]}" = "${HASH_DIR_CUSTOM_MODE[*]}"
363
+ '
364
+
365
+ test_expect_success "can recursively set directory mtime [$1]" '
366
+ HASHES=($(ipfs add -qr --mtime=$CUSTOM_MTIME "$TESTDIR"|sort)) &&
367
+ test "${HASHES[*]}" = "${HASH_DIR_CUSTOM_MTIME[*]}"
368
+ '
369
+
370
+ test_expect_success "can recursively restore mode and mtime [$1]" '
371
+ ipfs get -o "$OUTDIR" $HASH_DIR_ROOT &&
372
+ test "700:$DIR_TIME" = "$(stat -c "%a:%Y" "$OUTDIR")" &&
373
+ test "644:$((DIR_TIME+10))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2/sub1/sub2/file3")" &&
374
+ test "777:$((DIR_TIME+20))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2/sub1/link1")" &&
375
+ test "755:$((DIR_TIME+30))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2/sub1/sub2")" &&
376
+ test "755:$((DIR_TIME+40))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2/sub1")" &&
377
+ test "755:$((DIR_TIME+50))" = "$(stat -c "%a:%Y" "$OUTDIR/dir2")" &&
378
+ test "644:$((DIR_TIME+60))" = "$(stat -c "%a:%Y" "$OUTDIR/dir3/file2")" &&
379
+ test "755:$((DIR_TIME+70))" = "$(stat -c "%a:%Y" "$OUTDIR/dir3")" &&
380
+ test "644:$((DIR_TIME+80))" = "$(stat -c "%a:%Y" "$OUTDIR/file1")" &&
381
+ test "755:$((DIR_TIME+90))" = "$(stat -c "%a:%Y" "$OUTDIR/dir1")"
382
+ '
383
+
384
+ # basic smoke-test for cidv1 (we dont care about CID, just care about
385
+ # mode/mtime surviving ipfs import and export if --cid-version 1 is at play)
386
+ test_expect_success "can recursively preserve and restore mode and mtime with CIDv1 [$1]" '
387
+ test "700:$DIR_TIME" = "$(stat -c "%a:%Y" "$TESTDIR")" &&
388
+ test "644:$((DIR_TIME+10))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/sub2/file3")" &&
389
+ test "777:$((DIR_TIME+20))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/link1")" &&
390
+ test "755:$((DIR_TIME+30))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1/sub2")" &&
391
+ test "755:$((DIR_TIME+40))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2/sub1")" &&
392
+ test "755:$((DIR_TIME+50))" = "$(stat -c "%a:%Y" "$TESTDIR/dir2")" &&
393
+ test "644:$((DIR_TIME+60))" = "$(stat -c "%a:%Y" "$TESTDIR/dir3/file2")" &&
394
+ test "755:$((DIR_TIME+70))" = "$(stat -c "%a:%Y" "$TESTDIR/dir3")" &&
395
+ test "644:$((DIR_TIME+80))" = "$(stat -c "%a:%Y" "$TESTDIR/file1")" &&
396
+ test "755:$((DIR_TIME+90))" = "$(stat -c "%a:%Y" "$TESTDIR/dir1")" &&
397
+ CIDV1DIR=$(ipfs add -Qr --preserve-mode --preserve-mtime --cid-version 1 "$TESTDIR") &&
398
+ OUTDIRV1=$(mk_dir cidv1roundtrip$1) &&
399
+ ipfs get -o "$OUTDIRV1" $CIDV1DIR &&
400
+ test "700:$DIR_TIME" = "$(stat -c "%a:%Y" "$OUTDIRV1")" &&
401
+ test "644:$((DIR_TIME+10))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2/sub1/sub2/file3")" &&
402
+ test "777:$((DIR_TIME+20))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2/sub1/link1")" &&
403
+ test "755:$((DIR_TIME+30))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2/sub1/sub2")" &&
404
+ test "755:$((DIR_TIME+40))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2/sub1")" &&
405
+ test "755:$((DIR_TIME+50))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir2")" &&
406
+ test "644:$((DIR_TIME+60))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir3/file2")" &&
407
+ test "755:$((DIR_TIME+70))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir3")" &&
408
+ test "644:$((DIR_TIME+80))" = "$(stat -c "%a:%Y" "$OUTDIRV1/file1")" &&
409
+ test "755:$((DIR_TIME+90))" = "$(stat -c "%a:%Y" "$OUTDIRV1/dir1")"
410
+ '
411
+
412
+ test_expect_success "can change directory mode [$1]" '
413
+ NAME=$(mk_name) &&
414
+ ipfs files cp "/ipfs/$HASH_DIR_SUB1" /$NAME &&
415
+ ipfs files chmod 0710 /$NAME &&
416
+ test $(ipfs files stat --format="<mode>" /$NAME) = "drwx--x---"
417
+ '
418
+
419
+ test_expect_success "can change directory modification time [$1]" '
420
+ NAME=$(mk_name) &&
421
+ ipfs files cp "/ipfs/$HASH_DIR_SUB1" /$NAME &&
422
+ ipfs files touch --mtime=$CUSTOM_MTIME /$NAME &&
423
+ test $(ipfs files stat --format="<mtime-secs>" /$NAME) -eq $CUSTOM_MTIME
424
+ '
425
+
426
+ test_expect_success "can change directory modification time nanoseconds [$1]" '
427
+ NAME=$(mk_name) &&
428
+ MTIME=$(date --date="yesterday" +%s) &&
429
+ ipfs files cp "/ipfs/$HASH_DIR_SUB1" /$NAME &&
430
+ ipfs files touch --mtime=$MTIME --mtime-nsecs=94783 /$NAME &&
431
+ test $(ipfs files stat --format="<mtime-secs>" /$NAME) -eq $MTIME &&
432
+ test $(ipfs files stat --format="<mtime-nsecs>" /$NAME) -eq 94783
433
+ '
434
+}
435
+
436
+test_stat_template() {
437
+ test_expect_success "can stat $2 string mode [$1]" '
438
+ touch "$STAT_TARGET" &&
439
+ HASH=$(ipfs add -qr --mode="$STAT_MODE_OCTAL" "$STAT_TARGET") &&
440
+ ACTUAL=$(ipfs files stat --format="<mode>" /ipfs/$HASH) &&
441
+ test "$ACTUAL" = "$STAT_MODE_STRING"
442
+ '
443
+ test_expect_success "can stat $2 octal mode [$1]" '
444
+ touch "$STAT_TARGET" &&
445
+ HASH=$(ipfs add -qr --mode="$STAT_MODE_OCTAL" "$STAT_TARGET") &&
446
+ ACTUAL=$(ipfs files stat --format="<mode-octal>" /ipfs/$HASH) &&
447
+ test "$ACTUAL" = "$STAT_MODE_OCTAL"
448
+ '
449
+
450
+ test_expect_success "can stat $2 modification time string [$1]" '
451
+ touch "$STAT_TARGET" &&
452
+ HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME "$STAT_TARGET") &&
453
+ ACTUAL=$(ipfs files stat --format="<mtime>" /ipfs/$HASH) &&
454
+ test "$ACTUAL" = "24 Oct 2020, 11:42:00 UTC"
455
+ '
456
+
457
+ test_expect_success "can stat $2 modification time seconds [$1]" '
458
+ touch "$STAT_TARGET" &&
459
+ HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME "$STAT_TARGET") &&
460
+ ACTUAL=$(ipfs files stat --format="<mtime-secs>" /ipfs/$HASH) &&
461
+ test $ACTUAL -eq $CUSTOM_MTIME
462
+ '
463
+
464
+ test_expect_success "can stat $2 modification time nanoseconds [$1]" '
465
+ touch "$STAT_TARGET" &&
466
+ HASH=$(ipfs add -qr --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$STAT_TARGET") &&
467
+ ACTUAL=$(ipfs files stat --format="<mtime-nsecs>" /ipfs/$HASH) &&
468
+ test $ACTUAL -eq $CUSTOM_MTIME_NSECS
469
+ '
470
+}
471
+
472
+test_stat() {
473
+ STAT_TARGET="$FIXTURESDIR/statfile$1"
474
+ STAT_MODE_OCTAL="$CUSTOM_MODE"
475
+ STAT_MODE_STRING="-rwxrw-r--"
476
+ test_stat_template "$1" "file"
477
+
478
+ STAT_TARGET="$FIXTURESDIR/statdir$1"
479
+ STAT_MODE_OCTAL="0731"
480
+ STAT_MODE_STRING="drwx-wx--x"
481
+ mkdir "$STAT_TARGET"
482
+ test_stat_template "$1" "directory"
483
+
484
+ STAT_TARGET="$FIXTURESDIR/statlink$1"
485
+ STAT_MODE_OCTAL="0777"
486
+ STAT_MODE_STRING="lrwxrwxrwx"
487
+ ln -s nothing "$STAT_TARGET"
488
+ test_stat_template "$1" "link"
489
+
490
+
491
+ STAT_TARGET="$FIXTURESDIR/statfile$1"
492
+ test_expect_success "can chain stat template [$1]" '
493
+ HASH=$(ipfs add -q --mode=0644 --mtime=$CUSTOM_MTIME --mtime-nsecs=$CUSTOM_MTIME_NSECS "$STAT_TARGET") &&
494
+ ACTUAL=$(ipfs files stat --format="<mtime> <mtime-secs> <mtime-nsecs> <mode> <mode-octal>" /ipfs/$HASH) &&
495
+ test "$ACTUAL" = "24 Oct 2020, 11:42:00 UTC 1603539720 54321 -rw-r--r-- 0644"
496
+ '
497
+}
498
+
499
+test_all() {
500
+test_stat "$1"
501
+test_file "$1"
502
+test_directory "$1"
503
+}
504
+
505
+# test direct
506
+test_all "direct"
507
+
508
+# test daemon
509
+test_launch_ipfs_daemon_without_network
510
+test_all "daemon"
511
+test_kill_ipfs_daemon
512
+
513
+test_done
test/sharness/t0250-files-api.sh
+4
@@ -230,6 +230,8 @@ test_files_api() {
230
echo "Size: 4" >> file1stat_expect &&
231
echo "ChildBlocks: 0" >> file1stat_expect &&
232
echo "Type: file" >> file1stat_expect &&
233
+ echo "Mode: not set (not set)" >> file1stat_expect &&
234
+ echo "Mtime: not set" >> file1stat_expect &&
235
test_cmp file1stat_expect file1stat_actual
236
'
237
@@ -243,6 +245,8 @@ test_files_api() {
245
echo "Size: 4" >> file1stat_expect &&
246
echo "ChildBlocks: 0" >> file1stat_expect &&
247
echo "Type: file" >> file1stat_expect &&
248
+ echo "Mode: not set (not set)" >> file1stat_expect &&
249
+ echo "Mtime: not set" >> file1stat_expect &&
250
test_cmp file1stat_expect file1stat_actual
251
'
252