diff --git a/internal/fs/fs_local.go b/internal/fs/fs_local.go index 034d1aa24..bc979dd56 100644 --- a/internal/fs/fs_local.go +++ b/internal/fs/fs_local.go @@ -32,20 +32,6 @@ func (fs Local) OpenFile(name string, flag int, perm os.FileMode) (File, error) return f, nil } -// Stat returns a FileInfo describing the named file. If there is an error, it -// will be of type *PathError. -func (fs Local) Stat(name string) (os.FileInfo, error) { - return os.Stat(fixpath(name)) -} - -// Lstat returns the FileInfo structure describing the named file. -// If the file is a symbolic link, the returned FileInfo -// describes the symbolic link. Lstat makes no attempt to follow the link. -// If there is an error, it will be of type *PathError. -func (fs Local) Lstat(name string) (os.FileInfo, error) { - return os.Lstat(fixpath(name)) -} - // DeviceID extracts the DeviceID from the given FileInfo. If the fs does // not support a DeviceID, it returns an error instead func (fs Local) DeviceID(fi os.FileInfo) (id uint64, err error) { diff --git a/internal/fs/node_test.go b/internal/fs/node_test.go index 2623513a8..0a93fd6b3 100644 --- a/internal/fs/node_test.go +++ b/internal/fs/node_test.go @@ -247,7 +247,7 @@ func TestNodeRestoreAt(t *testing.T) { rtest.OK(t, NodeCreateAt(&test, nodePath)) rtest.OK(t, NodeRestoreMetadata(&test, nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) })) - fi, err := os.Lstat(nodePath) + fi, err := Local{}.Lstat(nodePath) rtest.OK(t, err) n2, err := NodeFromFileInfo(nodePath, fi, false) diff --git a/internal/fs/node_unix_test.go b/internal/fs/node_unix_test.go index 4d01b6cc5..97084c1a1 100644 --- a/internal/fs/node_unix_test.go +++ b/internal/fs/node_unix_test.go @@ -1,5 +1,4 @@ -//go:build !windows -// +build !windows +//go:build unix && !linux package fs diff --git a/internal/fs/preallocate_test.go b/internal/fs/preallocate_test.go index 9dabd2f36..e446345b8 100644 --- a/internal/fs/preallocate_test.go +++ b/internal/fs/preallocate_test.go @@ -30,9 +30,7 @@ func TestPreallocate(t *testing.T) { fi, err := wr.Stat() test.OK(t, err) - - efi := ExtendedStat(fi) - test.Assert(t, efi.Size == i || efi.Blocks > 0, "Preallocated size of %v, got size %v block %v", i, efi.Size, efi.Blocks) + test.Assert(t, fi.Size() == i, "Preallocated size of %v, got size %v", i, fi.Size()) }) } } diff --git a/internal/fs/setflags_linux_test.go b/internal/fs/setflags_linux_test.go index 8fe14a5a6..d439f79eb 100644 --- a/internal/fs/setflags_linux_test.go +++ b/internal/fs/setflags_linux_test.go @@ -38,7 +38,7 @@ func TestNoatime(t *testing.T) { rtest.OK(t, err) getAtime := func() time.Time { - info, err := f.Stat() + info, err := Local{}.Stat(f.Name()) rtest.OK(t, err) return ExtendedStat(info).AccessTime } diff --git a/internal/fs/stat_linux.go b/internal/fs/stat_linux.go new file mode 100644 index 000000000..a71141227 --- /dev/null +++ b/internal/fs/stat_linux.go @@ -0,0 +1,104 @@ +package fs + +import ( + "os" + "path/filepath" + "time" + + "golang.org/x/sys/unix" +) + +// On Linux, we reimplement Stat and Lstat in terms of unix.Statx, +// which gives access to some interesting information that Stat doesn't provide. + +func (fs Local) Stat(name string) (os.FileInfo, error) { + return statx(name, 0) +} + +func (fs Local) Lstat(name string) (os.FileInfo, error) { + return statx(name, unix.AT_SYMLINK_NOFOLLOW) +} + +func statx(name string, flags int) (*statxFileInfo, error) { + const mask = unix.STATX_BASIC_STATS | unix.STATX_BTIME + fi := &statxFileInfo{} + // XXX We could pick FORCE_SYNC or DONT_SYNC instead of SYNC_AS_STAT, + // to influence the behavior when dealing with remote filesystems. + err := unix.Statx(unix.AT_FDCWD, name, flags|unix.AT_STATX_SYNC_AS_STAT, mask, &fi.st) + if err != nil { + return nil, &os.PathError{Path: name, Op: "statx", Err: err} + } + + fi.name = filepath.Base(name) + return fi, nil +} + +type statxFileInfo struct { + name string + st unix.Statx_t +} + +func (fi *statxFileInfo) Name() string { return fi.name } +func (fi *statxFileInfo) Size() int64 { return int64(fi.st.Size) } +func (fi *statxFileInfo) ModTime() time.Time { return timeFromStatx(fi.st.Mtime) } +func (fi *statxFileInfo) IsDir() bool { return fi.Mode().IsDir() } +func (fi *statxFileInfo) Sys() any { return &fi.st } + +// Adapted from os/stat_linux.go in the Go stdlib. +func (fi *statxFileInfo) Mode() os.FileMode { + mode := os.FileMode(fi.st.Mode & 0o777) + + switch fi.st.Mode & unix.S_IFMT { + case unix.S_IFBLK: + mode |= os.ModeDevice + case unix.S_IFCHR: + mode |= os.ModeDevice | os.ModeCharDevice + case unix.S_IFDIR: + mode |= os.ModeDir + case unix.S_IFIFO: + mode |= os.ModeNamedPipe + case unix.S_IFLNK: + mode |= os.ModeSymlink + case unix.S_IFREG: + // nothing to do + case unix.S_IFSOCK: + mode |= os.ModeSocket + } + + if fi.st.Mode&unix.S_ISGID != 0 { + mode |= os.ModeSetgid + } + if fi.st.Mode&unix.S_ISUID != 0 { + mode |= os.ModeSetuid + } + if fi.st.Mode&unix.S_ISVTX != 0 { + mode |= os.ModeSticky + } + + return mode +} + +func extendedStat(fi os.FileInfo) ExtendedFileInfo { + s := fi.Sys().(*unix.Statx_t) + + return ExtendedFileInfo{ + FileInfo: fi, + DeviceID: uint64(s.Rdev_major)<<8 | uint64(s.Rdev_minor), + Inode: s.Ino, + Links: uint64(s.Nlink), + UID: s.Uid, + GID: s.Gid, + Device: uint64(s.Dev_major)<<8 | uint64(s.Dev_minor), + BlockSize: int64(s.Blksize), + Blocks: int64(s.Blocks), + Size: int64(s.Size), + + AccessTime: timeFromStatx(s.Atime), + ModTime: timeFromStatx(s.Mtime), + ChangeTime: timeFromStatx(s.Ctime), + } +} + +func timeFromStatx(ts unix.StatxTimestamp) time.Time { + return time.Unix(ts.Sec, int64(ts.Nsec)) +} diff --git a/internal/fs/stat_notlinux.go b/internal/fs/stat_notlinux.go new file mode 100644 index 000000000..7374f0b21 --- /dev/null +++ b/internal/fs/stat_notlinux.go @@ -0,0 +1,19 @@ +//go:build !linux + +package fs + +import "os" + +// Stat returns a FileInfo describing the named file. If there is an error, it +// will be of type *PathError. +func (fs Local) Stat(name string) (os.FileInfo, error) { + return os.Stat(fixpath(name)) +} + +// Lstat returns the FileInfo structure describing the named file. +// If the file is a symbolic link, the returned FileInfo +// describes the symbolic link. Lstat makes no attempt to follow the link. +// If there is an error, it will be of type *PathError. +func (fs Local) Lstat(name string) (os.FileInfo, error) { + return os.Lstat(fixpath(name)) +} diff --git a/internal/fs/stat_test.go b/internal/fs/stat_test.go index d52415c1d..87d33c930 100644 --- a/internal/fs/stat_test.go +++ b/internal/fs/stat_test.go @@ -1,3 +1,5 @@ +//go:build !linux + package fs import ( diff --git a/internal/fs/stat_unix.go b/internal/fs/stat_unix.go index c55571031..dffe55772 100644 --- a/internal/fs/stat_unix.go +++ b/internal/fs/stat_unix.go @@ -1,5 +1,4 @@ -//go:build !windows && !darwin && !freebsd && !netbsd -// +build !windows,!darwin,!freebsd,!netbsd +//go:build unix && !darwin && !freebsd && !linux && !netbsd package fs