@cryptotaxi247 / kubo / commits / 263edb251

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