Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support storing UnixFS 1.5 Mode and ModTime #10478

Merged
merged 30 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
095c18e
preliminary support for unixfs Mode and ModTime
gammazero Aug 13, 2024
60f3470
mod tidy examples
gammazero Aug 13, 2024
beae0b3
lint fix
gammazero Aug 13, 2024
633f28f
Update changelog
gammazero Aug 13, 2024
31b8024
test(mode): expect empty octal as well
lidel Aug 13, 2024
b170c90
chore: latest commit
lidel Aug 13, 2024
8e0308b
docs(changelong): mode/mtime cli example
lidel Aug 13, 2024
4817ac7
fix(rpc): cli error instead of daemon println
lidel Aug 13, 2024
f0fea76
chore(files): explicit 'not set' in missing stats
lidel Aug 13, 2024
0c55944
test(ci): enable running t0047-add-mode-mtime.sh
lidel Aug 13, 2024
988597a
Fix RPC decoding
gammazero Aug 14, 2024
8d75eb2
Use latest boxo PR version
gammazero Aug 14, 2024
97e10db
Merge branch 'master' into feat/unixfs1.5-mode-mtime
lidel Aug 14, 2024
b47e56a
chore(ci): verbose log of mod tests
lidel Aug 14, 2024
60fd454
chore(ci): log ci filesystem attributes
lidel Aug 14, 2024
2e8afc0
chore(ci): inspect stat of unecpected permissions
lidel Aug 14, 2024
0312a99
Implement symlink support in client/rpc
gammazero Aug 14, 2024
0987496
refactor(test): isolate outputs from fixtures
lidel Aug 14, 2024
5d23bcb
fix(ci): force umask 022
lidel Aug 14, 2024
d6c6999
Made mode and mod time work for items within a directory
gammazero Aug 16, 2024
7ac1c81
Explicitly disable raw leaves if preserving mode or modification time
gammazero Aug 19, 2024
b549434
Merge branch 'master' into feat/unixfs1.5-mode-mtime
gammazero Aug 19, 2024
577677c
docs: note raw-leaves impact, mark experimental
lidel Aug 19, 2024
01e6823
refactor: error in user provided raw-leaves=true
lidel Aug 19, 2024
1c3cc4c
chore: hardcode import defaults used for tests
lidel Aug 20, 2024
32c6a1c
test: mode/mtime roundtrip with cidv1
lidel Aug 20, 2024
55dad60
test: files chmod|touch with cidv1
lidel Aug 20, 2024
826aae1
update boxo
gammazero Aug 20, 2024
2235051
Update boxo to latest unixfs PR version
gammazero Aug 20, 2024
9043c55
chore: latest boxo main
lidel Aug 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 67 additions & 9 deletions client/rpc/apifile.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"time"

"github.com/ipfs/boxo/files"
unixfs "github.com/ipfs/boxo/ipld/unixfs"
Expand All @@ -24,9 +27,12 @@ func (api *UnixfsAPI) Get(ctx context.Context, p path.Path) (files.Node, error)
}

var stat struct {
Hash string
Type string
Size int64 // unixfs size
Hash string
Type string
Size int64 // unixfs size
Mode string
Mtime int64
MtimeNsecs int
}
err := api.core().Request("files/stat", p.String()).Exec(ctx, &stat)
if err != nil {
Expand All @@ -35,9 +41,9 @@ func (api *UnixfsAPI) Get(ctx context.Context, p path.Path) (files.Node, error)

switch stat.Type {
case "file":
return api.getFile(ctx, p, stat.Size)
return api.getFile(ctx, p, stat.Size, stat.Mode, stat.Mtime, stat.MtimeNsecs)
case "directory":
return api.getDir(ctx, p, stat.Size)
return api.getDir(ctx, p, stat.Size, stat.Mode, stat.Mtime, stat.MtimeNsecs)
default:
return nil, fmt.Errorf("unsupported file type '%s'", stat.Type)
}
Expand All @@ -49,6 +55,9 @@ type apiFile struct {
size int64
path path.Path

mode os.FileMode
mtime time.Time

r *Response
at int64
}
Expand Down Expand Up @@ -128,16 +137,44 @@ func (f *apiFile) Close() error {
return nil
}

func (f *apiFile) Mode() os.FileMode {
return f.mode
}

func (f *apiFile) ModTime() time.Time {
return f.mtime
}

func (f *apiFile) Size() (int64, error) {
return f.size, nil
}

func (api *UnixfsAPI) getFile(ctx context.Context, p path.Path, size int64) (files.Node, error) {
func stringToFileMode(mode string) (os.FileMode, error) {
if mode == "" {
return 0, nil
}
mode64, err := strconv.ParseUint(mode, 8, 32)
if err != nil {
return 0, fmt.Errorf("cannot parse mode %s: %s", mode, err)
}
return os.FileMode(uint32(mode64)), nil
}

func (api *UnixfsAPI) getFile(ctx context.Context, p path.Path, size int64, mode string, mtime int64, mtimeNsecs int) (files.Node, error) {
fm, err := stringToFileMode(mode)
if err != nil {
return nil, err
}

f := &apiFile{
ctx: ctx,
core: api.core(),
size: size,
path: p,
mode: fm,
}
if mtime != 0 {
f.mtime = time.Unix(mtime, int64(mtimeNsecs))
}

return f, f.reset()
Expand Down Expand Up @@ -195,13 +232,13 @@ func (it *apiIter) Next() bool {

switch it.cur.Type {
case unixfs.THAMTShard, unixfs.TMetadata, unixfs.TDirectory:
it.curFile, err = it.core.getDir(it.ctx, path.FromCid(c), int64(it.cur.Size))
it.curFile, err = it.core.getDir(it.ctx, path.FromCid(c), int64(it.cur.Size), it.cur.Mode, it.cur.Mtime, it.cur.MtimeNsecs)
if err != nil {
it.err = err
return false
}
case unixfs.TFile:
it.curFile, err = it.core.getFile(it.ctx, path.FromCid(c), int64(it.cur.Size))
it.curFile, err = it.core.getFile(it.ctx, path.FromCid(c), int64(it.cur.Size), it.cur.Mode, it.cur.Mtime, it.cur.MtimeNsecs)
if err != nil {
it.err = err
return false
Expand All @@ -223,13 +260,24 @@ type apiDir struct {
size int64
path path.Path

mode os.FileMode
mtime time.Time

dec *json.Decoder
}

func (d *apiDir) Close() error {
return nil
}

func (d *apiDir) Mode() os.FileMode {
return d.mode
}

func (d *apiDir) ModTime() time.Time {
return d.mtime
}

func (d *apiDir) Size() (int64, error) {
return d.size, nil
}
Expand All @@ -242,7 +290,7 @@ func (d *apiDir) Entries() files.DirIterator {
}
}

func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64) (files.Node, error) {
func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64, mode string, mtime int64, mtimeNsecs int) (files.Node, error) {
resp, err := api.core().Request("ls", p.String()).
Option("resolve-size", true).
Option("stream", true).Send(ctx)
Expand All @@ -253,15 +301,25 @@ func (api *UnixfsAPI) getDir(ctx context.Context, p path.Path, size int64) (file
return nil, resp.Error
}

fm, err := stringToFileMode(mode)
if err != nil {
return nil, err
}

d := &apiDir{
ctx: ctx,
core: api,
size: size,
path: p,
mode: fm,

dec: json.NewDecoder(resp.Output),
}

if mtime != 0 {
d.mtime = time.Unix(mtime, int64(mtimeNsecs)).UTC()
}

return d, nil
}

Expand Down
29 changes: 20 additions & 9 deletions client/rpc/unixfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"os"

"github.com/ipfs/boxo/files"
unixfs "github.com/ipfs/boxo/ipld/unixfs"
Expand All @@ -22,6 +23,10 @@ type addEvent struct {
Hash string `json:",omitempty"`
Bytes int64 `json:",omitempty"`
Size string `json:",omitempty"`

Mode os.FileMode `json:",omitempty"`
Mtime int64 `json:",omitempty"`
MtimeNsecs int `json:",omitempty"`
}

type UnixfsAPI HttpApi
Expand Down Expand Up @@ -80,23 +85,25 @@ func (api *UnixfsAPI) Add(ctx context.Context, f files.Node, opts ...caopts.Unix
}
defer resp.Output.Close()
dec := json.NewDecoder(resp.Output)
loop:

for {
var evt addEvent
switch err := dec.Decode(&evt); err {
case nil:
case io.EOF:
break loop
default:
if err := dec.Decode(&evt); err != nil {
if errors.Is(err, io.EOF) {
break
}
return path.ImmutablePath{}, err
}
out = evt

if options.Events != nil {
ifevt := &iface.AddEvent{
Name: out.Name,
Size: out.Size,
Bytes: out.Bytes,
Name: out.Name,
Size: out.Size,
Bytes: out.Bytes,
Mode: out.Mode,
Mtime: out.Mtime,
MtimeNsecs: out.MtimeNsecs,
}

if out.Hash != "" {
Expand Down Expand Up @@ -129,6 +136,10 @@ type lsLink struct {
Size uint64
Type unixfs_pb.Data_DataType
Target string

Mode string
Mtime int64
MtimeNsecs int
}

type lsObject struct {
Expand Down
93 changes: 83 additions & 10 deletions core/commands/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"io"
"os"
gopath "path"
"strconv"
"strings"
"time"

"github.com/ipfs/kubo/config"
"github.com/ipfs/kubo/core/commands/cmdenv"
Expand All @@ -25,11 +27,31 @@ import (
// ErrDepthLimitExceeded indicates that the max depth has been exceeded.
var ErrDepthLimitExceeded = fmt.Errorf("depth limit exceeded")

type TimeParts struct {
t *time.Time
}

func (t TimeParts) MarshalJSON() ([]byte, error) {
return t.t.MarshalJSON()
}

// UnmarshalJSON implements the json.Unmarshaler interface.
// The time is expected to be a quoted string in RFC 3339 format.
func (t *TimeParts) UnmarshalJSON(data []byte) (err error) {
// Fractional seconds are handled implicitly by Parse.
tt, err := time.Parse("\"2006-01-02T15:04:05Z\"", string(data))
*t = TimeParts{&tt}
return
}

type AddEvent struct {
Name string
Hash string `json:",omitempty"`
Bytes int64 `json:",omitempty"`
Size string `json:",omitempty"`
Name string
Hash string `json:",omitempty"`
Bytes int64 `json:",omitempty"`
Size string `json:",omitempty"`
Mode string `json:",omitempty"`
Mtime int64 `json:",omitempty"`
MtimeNsecs int `json:",omitempty"`
}

const (
Expand All @@ -50,6 +72,12 @@ const (
inlineOptionName = "inline"
inlineLimitOptionName = "inline-limit"
toFilesOptionName = "to-files"

preserveModeOptionName = "preserve-mode"
preserveMtimeOptionName = "preserve-mtime"
modeOptionName = "mode"
mtimeOptionName = "mtime"
mtimeNsecsOptionName = "mtime-nsecs"
)

const adderOutChanSize = 8
Expand Down Expand Up @@ -166,6 +194,12 @@ See 'dag export' and 'dag import' for more information.
cmds.IntOption(inlineLimitOptionName, "Maximum block size to inline. (experimental)").WithDefault(32),
cmds.BoolOption(pinOptionName, "Pin locally to protect added files from garbage collection.").WithDefault(true),
cmds.StringOption(toFilesOptionName, "Add reference to Files API (MFS) at the provided path."),

cmds.BoolOption(preserveModeOptionName, "Apply existing POSIX permissions to created UnixFS entries"),
cmds.BoolOption(preserveMtimeOptionName, "Apply existing POSIX modification time to created UnixFS entries"),
cmds.UintOption(modeOptionName, "Custom POSIX file mode to store in created UnixFS entries"),
cmds.Int64Option(mtimeOptionName, "Custom POSIX modification time to store in created UnixFS entries (seconds before or after the Unix Epoch)"),
cmds.UintOption(mtimeNsecsOptionName, "Custom POSIX modification time (optional time fraction in nanoseconds)"),
},
PreRun: func(req *cmds.Request, env cmds.Environment) error {
quiet, _ := req.Options[quietOptionName].(bool)
Expand Down Expand Up @@ -217,6 +251,11 @@ See 'dag export' and 'dag import' for more information.
inline, _ := req.Options[inlineOptionName].(bool)
inlineLimit, _ := req.Options[inlineLimitOptionName].(int)
toFilesStr, toFilesSet := req.Options[toFilesOptionName].(string)
preserveMode, _ := req.Options[preserveModeOptionName].(bool)
preserveMtime, _ := req.Options[preserveMtimeOptionName].(bool)
mode, _ := req.Options[modeOptionName].(uint)
mtime, _ := req.Options[mtimeOptionName].(int64)
mtimeNsecs, _ := req.Options[mtimeNsecsOptionName].(uint)

if chunker == "" {
chunker = cfg.Import.UnixFSChunker.WithDefault(config.DefaultUnixFSChunker)
Expand Down Expand Up @@ -272,6 +311,19 @@ See 'dag export' and 'dag import' for more information.

options.Unixfs.Progress(progress),
options.Unixfs.Silent(silent),

options.Unixfs.PreserveMode(preserveMode),
options.Unixfs.PreserveMtime(preserveMtime),
}

if mode != 0 {
opts = append(opts, options.Unixfs.Mode(os.FileMode(mode)))
}

if mtime != 0 {
opts = append(opts, options.Unixfs.Mtime(mtime, uint32(mtimeNsecs)))
} else if mtimeNsecs != 0 {
return fmt.Errorf("option %q requires %q to be provided as well", mtimeNsecsOptionName, mtimeOptionName)
}

if cidVerSet {
Expand Down Expand Up @@ -383,12 +435,33 @@ See 'dag export' and 'dag import' for more information.
output.Name = gopath.Join(addit.Name(), output.Name)
}

if err := res.Emit(&AddEvent{
Name: output.Name,
Hash: h,
Bytes: output.Bytes,
Size: output.Size,
}); err != nil {
output.Mode = addit.Node().Mode()
if ts := addit.Node().ModTime(); !ts.IsZero() {
output.Mtime = addit.Node().ModTime().Unix()
output.MtimeNsecs = addit.Node().ModTime().Nanosecond()
}

addEvent := AddEvent{
Name: output.Name,
Hash: h,
Bytes: output.Bytes,
Size: output.Size,
Mtime: output.Mtime,
MtimeNsecs: output.MtimeNsecs,
}

if output.Mode != 0 {
addEvent.Mode = "0" + strconv.FormatUint(uint64(output.Mode), 8)
}

if output.Mtime > 0 {
addEvent.Mtime = output.Mtime
if output.MtimeNsecs > 0 {
addEvent.MtimeNsecs = output.MtimeNsecs
}
}

if err := res.Emit(&addEvent); err != nil {
return err
}
}
Expand Down
2 changes: 2 additions & 0 deletions core/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ func TestCommands(t *testing.T) {
"/files/rm",
"/files/stat",
"/files/write",
"/files/chmod",
"/files/touch",
"/filestore",
"/filestore/dups",
"/filestore/ls",
Expand Down
Loading
Loading