fs: inline ExtendedStat

This commit is contained in:
Michael Eischer 2024-11-03 16:01:59 +01:00
parent 806fa534ce
commit 641390103d
22 changed files with 121 additions and 205 deletions

View file

@ -132,7 +132,7 @@ type vssDeleteOriginalFS struct {
hasRemoved bool hasRemoved bool
} }
func (f *vssDeleteOriginalFS) Lstat(name string) (os.FileInfo, error) { func (f *vssDeleteOriginalFS) Lstat(name string) (*fs.ExtendedFileInfo, error) {
if !f.hasRemoved { if !f.hasRemoved {
// call Lstat to trigger snapshot creation // call Lstat to trigger snapshot creation
_, _ = f.FS.Lstat(name) _, _ = f.FS.Lstat(name)

View file

@ -25,7 +25,7 @@ type SelectByNameFunc func(item string) bool
// SelectFunc returns true for all items that should be included (files and // SelectFunc returns true for all items that should be included (files and
// dirs). If false is returned, files are ignored and dirs are not even walked. // dirs). If false is returned, files are ignored and dirs are not even walked.
type SelectFunc func(item string, fi os.FileInfo, fs fs.FS) bool type SelectFunc func(item string, fi *fs.ExtendedFileInfo, fs fs.FS) bool
// ErrorFunc is called when an error during archiving occurs. When nil is // ErrorFunc is called when an error during archiving occurs. When nil is
// returned, the archiver continues, otherwise it aborts and passes the error // returned, the archiver continues, otherwise it aborts and passes the error
@ -189,7 +189,7 @@ func New(repo archiverRepo, filesystem fs.FS, opts Options) *Archiver {
arch := &Archiver{ arch := &Archiver{
Repo: repo, Repo: repo,
SelectByName: func(_ string) bool { return true }, SelectByName: func(_ string) bool { return true },
Select: func(_ string, _ os.FileInfo, _ fs.FS) bool { return true }, Select: func(_ string, _ *fs.ExtendedFileInfo, _ fs.FS) bool { return true },
FS: filesystem, FS: filesystem,
Options: opts.ApplyDefaults(), Options: opts.ApplyDefaults(),
@ -618,27 +618,26 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
// fileChanged tries to detect whether a file's content has changed compared // fileChanged tries to detect whether a file's content has changed compared
// to the contents of node, which describes the same path in the parent backup. // to the contents of node, which describes the same path in the parent backup.
// It should only be run for regular files. // It should only be run for regular files.
func fileChanged(fs fs.FS, fi os.FileInfo, node *restic.Node, ignoreFlags uint) bool { func fileChanged(fs fs.FS, fi *fs.ExtendedFileInfo, node *restic.Node, ignoreFlags uint) bool {
switch { switch {
case node == nil: case node == nil:
return true return true
case node.Type != restic.NodeTypeFile: case node.Type != restic.NodeTypeFile:
// We're only called for regular files, so this is a type change. // We're only called for regular files, so this is a type change.
return true return true
case uint64(fi.Size()) != node.Size: case uint64(fi.Size) != node.Size:
return true return true
case !fi.ModTime().Equal(node.ModTime): case !fi.ModTime.Equal(node.ModTime):
return true return true
} }
checkCtime := ignoreFlags&ChangeIgnoreCtime == 0 checkCtime := ignoreFlags&ChangeIgnoreCtime == 0
checkInode := ignoreFlags&ChangeIgnoreInode == 0 checkInode := ignoreFlags&ChangeIgnoreInode == 0
extFI := fs.ExtendedStat(fi)
switch { switch {
case checkCtime && !extFI.ChangeTime.Equal(node.ChangeTime): case checkCtime && !fi.ChangeTime.Equal(node.ChangeTime):
return true return true
case checkInode && node.Inode != extFI.Inode: case checkInode && node.Inode != fi.Inode:
return true return true
} }

View file

@ -516,13 +516,13 @@ func chmodTwice(t testing.TB, name string) {
rtest.OK(t, err) rtest.OK(t, err)
} }
func lstat(t testing.TB, name string) os.FileInfo { func lstat(t testing.TB, name string) *fs.ExtendedFileInfo {
fi, err := os.Lstat(name) fi, err := os.Lstat(name)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return fi return fs.ExtendedStat(fi)
} }
func setTimestamp(t testing.TB, filename string, atime, mtime time.Time) { func setTimestamp(t testing.TB, filename string, atime, mtime time.Time) {
@ -660,7 +660,7 @@ func TestFileChanged(t *testing.T) {
rename(t, filename, tempname) rename(t, filename, tempname)
save(t, filename, defaultContent) save(t, filename, defaultContent)
remove(t, tempname) remove(t, tempname)
setTimestamp(t, filename, fi.ModTime(), fi.ModTime()) setTimestamp(t, filename, fi.ModTime, fi.ModTime)
}, },
ChangeIgnore: ChangeIgnoreCtime | ChangeIgnoreInode, ChangeIgnore: ChangeIgnoreCtime | ChangeIgnoreInode,
SameFile: true, SameFile: true,
@ -1520,7 +1520,7 @@ func TestArchiverSnapshotSelect(t *testing.T) {
}, },
"other": TestFile{Content: "another file"}, "other": TestFile{Content: "another file"},
}, },
selFn: func(item string, fi os.FileInfo, _ fs.FS) bool { selFn: func(item string, fi *fs.ExtendedFileInfo, _ fs.FS) bool {
return true return true
}, },
}, },
@ -1537,7 +1537,7 @@ func TestArchiverSnapshotSelect(t *testing.T) {
}, },
"other": TestFile{Content: "another file"}, "other": TestFile{Content: "another file"},
}, },
selFn: func(item string, fi os.FileInfo, _ fs.FS) bool { selFn: func(item string, fi *fs.ExtendedFileInfo, _ fs.FS) bool {
return false return false
}, },
err: "snapshot is empty", err: "snapshot is empty",
@ -1564,7 +1564,7 @@ func TestArchiverSnapshotSelect(t *testing.T) {
}, },
"other": TestFile{Content: "another file"}, "other": TestFile{Content: "another file"},
}, },
selFn: func(item string, fi os.FileInfo, _ fs.FS) bool { selFn: func(item string, fi *fs.ExtendedFileInfo, _ fs.FS) bool {
return filepath.Ext(item) != ".txt" return filepath.Ext(item) != ".txt"
}, },
}, },
@ -1588,7 +1588,7 @@ func TestArchiverSnapshotSelect(t *testing.T) {
}, },
"other": TestFile{Content: "another file"}, "other": TestFile{Content: "another file"},
}, },
selFn: func(item string, fi os.FileInfo, fs fs.FS) bool { selFn: func(item string, fi *fs.ExtendedFileInfo, fs fs.FS) bool {
return fs.Base(item) != "subdir" return fs.Base(item) != "subdir"
}, },
}, },
@ -1597,7 +1597,7 @@ func TestArchiverSnapshotSelect(t *testing.T) {
src: TestDir{ src: TestDir{
"foo": TestFile{Content: "foo"}, "foo": TestFile{Content: "foo"},
}, },
selFn: func(item string, fi os.FileInfo, fs fs.FS) bool { selFn: func(item string, fi *fs.ExtendedFileInfo, fs fs.FS) bool {
return fs.IsAbs(item) return fs.IsAbs(item)
}, },
}, },
@ -2202,7 +2202,7 @@ func snapshot(t testing.TB, repo archiverRepo, fs fs.FS, parent *restic.Snapshot
type overrideFS struct { type overrideFS struct {
fs.FS fs.FS
overrideFI os.FileInfo overrideFI *fs.ExtendedFileInfo
resetFIOnRead bool resetFIOnRead bool
overrideNode *restic.Node overrideNode *restic.Node
overrideErr error overrideErr error
@ -2225,7 +2225,7 @@ type overrideFile struct {
ofs *overrideFS ofs *overrideFS
} }
func (f overrideFile) Stat() (os.FileInfo, error) { func (f overrideFile) Stat() (*fs.ExtendedFileInfo, error) {
if f.ofs.overrideFI == nil { if f.ofs.overrideFI == nil {
return f.File.Stat() return f.File.Stat()
} }
@ -2497,7 +2497,7 @@ type missingFile struct {
fs.File fs.File
} }
func (f *missingFile) Stat() (os.FileInfo, error) { func (f *missingFile) Stat() (*fs.ExtendedFileInfo, error) {
return nil, os.ErrNotExist return nil, os.ErrNotExist
} }

View file

@ -29,7 +29,7 @@ func (fi wrappedFileInfo) Mode() os.FileMode {
} }
// wrapFileInfo returns a new os.FileInfo with the mode, owner, and group fields changed. // wrapFileInfo returns a new os.FileInfo with the mode, owner, and group fields changed.
func wrapFileInfo(fi os.FileInfo) os.FileInfo { func wrapFileInfo(fi *fs.ExtendedFileInfo) *fs.ExtendedFileInfo {
// get the underlying stat_t and modify the values // get the underlying stat_t and modify the values
stat := fi.Sys().(*syscall.Stat_t) stat := fi.Sys().(*syscall.Stat_t)
stat.Mode = mockFileInfoMode stat.Mode = mockFileInfoMode
@ -37,22 +37,22 @@ func wrapFileInfo(fi os.FileInfo) os.FileInfo {
stat.Gid = mockFileInfoGID stat.Gid = mockFileInfoGID
// wrap the os.FileInfo so we can return a modified stat_t // wrap the os.FileInfo so we can return a modified stat_t
res := wrappedFileInfo{ return fs.ExtendedStat(wrappedFileInfo{
FileInfo: fi, FileInfo: fi.FileInfo,
sys: stat, sys: stat,
mode: mockFileInfoMode, mode: mockFileInfoMode,
} })
return res
} }
// wrapIrregularFileInfo returns a new os.FileInfo with the mode changed to irregular file // wrapIrregularFileInfo returns a new os.FileInfo with the mode changed to irregular file
func wrapIrregularFileInfo(fi os.FileInfo) os.FileInfo { func wrapIrregularFileInfo(fi *fs.ExtendedFileInfo) *fs.ExtendedFileInfo {
// wrap the os.FileInfo so we can return a modified stat_t // wrap the os.FileInfo so we can return a modified stat_t
return wrappedFileInfo{ return &fs.ExtendedFileInfo{
FileInfo: fi, FileInfo: wrappedFileInfo{
sys: fi.Sys().(*syscall.Stat_t), FileInfo: fi.FileInfo,
mode: (fi.Mode() &^ os.ModeType) | os.ModeIrregular, sys: fi.Sys(),
mode: (fi.Mode() &^ os.ModeType) | os.ModeIrregular,
},
} }
} }

View file

@ -5,6 +5,8 @@ package archiver
import ( import (
"os" "os"
"github.com/restic/restic/internal/fs"
) )
type wrappedFileInfo struct { type wrappedFileInfo struct {
@ -17,20 +19,20 @@ func (fi wrappedFileInfo) Mode() os.FileMode {
} }
// wrapFileInfo returns a new os.FileInfo with the mode, owner, and group fields changed. // wrapFileInfo returns a new os.FileInfo with the mode, owner, and group fields changed.
func wrapFileInfo(fi os.FileInfo) os.FileInfo { func wrapFileInfo(fi *fs.ExtendedFileInfo) *fs.ExtendedFileInfo {
// wrap the os.FileInfo and return the modified mode, uid and gid are ignored on Windows // wrap the os.FileInfo and return the modified mode, uid and gid are ignored on Windows
res := wrappedFileInfo{ return fs.ExtendedStat(wrappedFileInfo{
FileInfo: fi, FileInfo: fi.FileInfo,
mode: mockFileInfoMode, mode: mockFileInfoMode,
} })
return res
} }
// wrapIrregularFileInfo returns a new os.FileInfo with the mode changed to irregular file // wrapIrregularFileInfo returns a new os.FileInfo with the mode changed to irregular file
func wrapIrregularFileInfo(fi os.FileInfo) os.FileInfo { func wrapIrregularFileInfo(fi *fs.ExtendedFileInfo) *fs.ExtendedFileInfo {
return wrappedFileInfo{ return &fs.ExtendedFileInfo{
FileInfo: fi, FileInfo: wrappedFileInfo{
mode: (fi.Mode() &^ os.ModeType) | os.ModeIrregular, FileInfo: fi.FileInfo,
mode: (fi.Mode() &^ os.ModeType) | os.ModeIrregular,
},
} }
} }

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"runtime"
"strings" "strings"
"sync" "sync"
@ -21,7 +22,7 @@ type RejectByNameFunc func(path string) bool
// RejectFunc is a function that takes a filename and os.FileInfo of a // RejectFunc is a function that takes a filename and os.FileInfo of a
// file that would be included in the backup. The function returns true if it // file that would be included in the backup. The function returns true if it
// should be excluded (rejected) from the backup. // should be excluded (rejected) from the backup.
type RejectFunc func(path string, fi os.FileInfo, fs fs.FS) bool type RejectFunc func(path string, fi *fs.ExtendedFileInfo, fs fs.FS) bool
func CombineRejectByNames(funcs []RejectByNameFunc) SelectByNameFunc { func CombineRejectByNames(funcs []RejectByNameFunc) SelectByNameFunc {
return func(item string) bool { return func(item string) bool {
@ -35,7 +36,7 @@ func CombineRejectByNames(funcs []RejectByNameFunc) SelectByNameFunc {
} }
func CombineRejects(funcs []RejectFunc) SelectFunc { func CombineRejects(funcs []RejectFunc) SelectFunc {
return func(item string, fi os.FileInfo, fs fs.FS) bool { return func(item string, fi *fs.ExtendedFileInfo, fs fs.FS) bool {
for _, reject := range funcs { for _, reject := range funcs {
if reject(item, fi, fs) { if reject(item, fi, fs) {
return false return false
@ -104,7 +105,7 @@ func RejectIfPresent(excludeFileSpec string, warnf func(msg string, args ...inte
} }
debug.Log("using %q as exclusion tagfile", tf) debug.Log("using %q as exclusion tagfile", tf)
rc := newRejectionCache() rc := newRejectionCache()
return func(filename string, _ os.FileInfo, fs fs.FS) bool { return func(filename string, _ *fs.ExtendedFileInfo, fs fs.FS) bool {
return isExcludedByFile(filename, tf, tc, rc, fs, warnf) return isExcludedByFile(filename, tf, tc, rc, fs, warnf)
}, nil }, nil
} }
@ -186,6 +187,10 @@ type deviceMap map[string]uint64
// newDeviceMap creates a new device map from the list of source paths. // newDeviceMap creates a new device map from the list of source paths.
func newDeviceMap(allowedSourcePaths []string, fs fs.FS) (deviceMap, error) { func newDeviceMap(allowedSourcePaths []string, fs fs.FS) (deviceMap, error) {
if runtime.GOOS == "windows" {
return nil, errors.New("Device IDs are not supported on Windows")
}
deviceMap := make(map[string]uint64) deviceMap := make(map[string]uint64)
for _, item := range allowedSourcePaths { for _, item := range allowedSourcePaths {
@ -199,12 +204,7 @@ func newDeviceMap(allowedSourcePaths []string, fs fs.FS) (deviceMap, error) {
return nil, err return nil, err
} }
id, err := fs.DeviceID(fi) deviceMap[item] = fi.DeviceID
if err != nil {
return nil, err
}
deviceMap[item] = id
} }
if len(deviceMap) == 0 { if len(deviceMap) == 0 {
@ -254,15 +254,8 @@ func RejectByDevice(samples []string, filesystem fs.FS) (RejectFunc, error) {
} }
debug.Log("allowed devices: %v\n", deviceMap) debug.Log("allowed devices: %v\n", deviceMap)
return func(item string, fi os.FileInfo, fs fs.FS) bool { return func(item string, fi *fs.ExtendedFileInfo, fs fs.FS) bool {
id, err := fs.DeviceID(fi) allowed, err := deviceMap.IsAllowed(fs.Clean(item), fi.DeviceID, fs)
if err != nil {
// This should never happen because gatherDevices() would have
// errored out earlier. If it still does that's a reason to panic.
panic(err)
}
allowed, err := deviceMap.IsAllowed(fs.Clean(item), id, fs)
if err != nil { if err != nil {
// this should not happen // this should not happen
panic(fmt.Sprintf("error checking device ID of %v: %v", item, err)) panic(fmt.Sprintf("error checking device ID of %v: %v", item, err))
@ -290,14 +283,7 @@ func RejectByDevice(samples []string, filesystem fs.FS) (RejectFunc, error) {
return true return true
} }
parentDeviceID, err := fs.DeviceID(parentFI) parentAllowed, err := deviceMap.IsAllowed(parentDir, parentFI.DeviceID, fs)
if err != nil {
debug.Log("item %v: getting device ID of parent directory: %v", item, err)
// if in doubt, reject
return true
}
parentAllowed, err := deviceMap.IsAllowed(parentDir, parentDeviceID, fs)
if err != nil { if err != nil {
debug.Log("item %v: error checking parent directory: %v", item, err) debug.Log("item %v: error checking parent directory: %v", item, err)
// if in doubt, reject // if in doubt, reject
@ -315,13 +301,13 @@ func RejectByDevice(samples []string, filesystem fs.FS) (RejectFunc, error) {
} }
func RejectBySize(maxSize int64) (RejectFunc, error) { func RejectBySize(maxSize int64) (RejectFunc, error) {
return func(item string, fi os.FileInfo, _ fs.FS) bool { return func(item string, fi *fs.ExtendedFileInfo, _ fs.FS) bool {
// directory will be ignored // directory will be ignored
if fi.IsDir() { if fi.IsDir() {
return false return false
} }
filesize := fi.Size() filesize := fi.Size
if filesize > maxSize { if filesize > maxSize {
debug.Log("file %s is oversize: %d", item, filesize) debug.Log("file %s is oversize: %d", item, filesize)
return true return true

View file

@ -193,7 +193,7 @@ func TestIsExcludedByFileSize(t *testing.T) {
return err return err
} }
excluded := sizeExclude(p, fi, nil) excluded := sizeExclude(p, fs.ExtendedStat(fi), nil)
// the log message helps debugging in case the test fails // the log message helps debugging in case the test fails
t.Logf("%q: dir:%t; size:%d; excluded:%v", p, fi.IsDir(), fi.Size(), excluded) t.Logf("%q: dir:%t; size:%d; excluded:%v", p, fi.IsDir(), fi.Size(), excluded)
m[p] = !excluded m[p] = !excluded

View file

@ -2,7 +2,6 @@ package archiver
import ( import (
"context" "context"
"os"
"sort" "sort"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
@ -25,7 +24,7 @@ func NewScanner(filesystem fs.FS) *Scanner {
return &Scanner{ return &Scanner{
FS: filesystem, FS: filesystem,
SelectByName: func(_ string) bool { return true }, SelectByName: func(_ string) bool { return true },
Select: func(_ string, _ os.FileInfo, _ fs.FS) bool { return true }, Select: func(_ string, _ *fs.ExtendedFileInfo, _ fs.FS) bool { return true },
Error: func(_ string, err error) error { return err }, Error: func(_ string, err error) error { return err },
Result: func(_ string, _ ScanStats) {}, Result: func(_ string, _ ScanStats) {},
} }
@ -121,7 +120,7 @@ func (s *Scanner) scan(ctx context.Context, stats ScanStats, target string) (Sca
switch { switch {
case fi.Mode().IsRegular(): case fi.Mode().IsRegular():
stats.Files++ stats.Files++
stats.Bytes += uint64(fi.Size()) stats.Bytes += uint64(fi.Size)
case fi.Mode().IsDir(): case fi.Mode().IsDir():
names, err := fs.Readdirnames(s.FS, target, fs.O_NOFOLLOW) names, err := fs.Readdirnames(s.FS, target, fs.O_NOFOLLOW)
if err != nil { if err != nil {

View file

@ -56,7 +56,7 @@ func TestScanner(t *testing.T) {
}, },
}, },
}, },
selFn: func(item string, fi os.FileInfo, fs fs.FS) bool { selFn: func(item string, fi *fs.ExtendedFileInfo, fs fs.FS) bool {
if fi.IsDir() { if fi.IsDir() {
return true return true
} }

View file

@ -1,31 +0,0 @@
//go:build !windows
// +build !windows
package fs
import (
"os"
"syscall"
"github.com/restic/restic/internal/errors"
)
// deviceID extracts the device ID from an os.FileInfo object by casting it
// to syscall.Stat_t
func deviceID(fi os.FileInfo) (deviceID uint64, err error) {
if fi == nil {
return 0, errors.New("unable to determine device: fi is nil")
}
if fi.Sys() == nil {
return 0, errors.New("unable to determine device: fi.Sys() is nil")
}
if st, ok := fi.Sys().(*syscall.Stat_t); ok {
// st.Dev is uint32 on Darwin and uint64 on Linux. Just cast
// everything to uint64.
return uint64(st.Dev), nil
}
return 0, errors.New("Could not cast to syscall.Stat_t")
}

View file

@ -1,16 +0,0 @@
//go:build windows
// +build windows
package fs
import (
"os"
"github.com/restic/restic/internal/errors"
)
// deviceID extracts the device ID from an os.FileInfo object by casting it
// to syscall.Stat_t
func deviceID(_ os.FileInfo) (deviceID uint64, err error) {
return 0, errors.New("Device IDs are not supported on Windows")
}

View file

@ -36,19 +36,12 @@ func (fs Local) OpenFile(name string, flag int, metadataOnly bool) (File, error)
// If the file is a symbolic link, the returned FileInfo // If the file is a symbolic link, the returned FileInfo
// describes the symbolic link. Lstat makes no attempt to follow the link. // describes the symbolic link. Lstat makes no attempt to follow the link.
// If there is an error, it will be of type *PathError. // If there is an error, it will be of type *PathError.
func (fs Local) Lstat(name string) (os.FileInfo, error) { func (fs Local) Lstat(name string) (*ExtendedFileInfo, error) {
return os.Lstat(fixpath(name)) fi, err := os.Lstat(fixpath(name))
} if err != nil {
return nil, err
// DeviceID extracts the DeviceID from the given FileInfo. If the fs does }
// not support a DeviceID, it returns an error instead return extendedStat(fi), nil
func (fs Local) DeviceID(fi os.FileInfo) (id uint64, err error) {
return deviceID(fi)
}
// ExtendedStat converts the give FileInfo into ExtendedFileInfo.
func (fs Local) ExtendedStat(fi os.FileInfo) ExtendedFileInfo {
return ExtendedStat(fi)
} }
// Join joins any number of path elements into a single path, adding a // Join joins any number of path elements into a single path, adding a
@ -96,7 +89,7 @@ type localFile struct {
name string name string
flag int flag int
f *os.File f *os.File
fi os.FileInfo fi *ExtendedFileInfo
} }
// See the File interface for a description of each method // See the File interface for a description of each method
@ -137,18 +130,23 @@ func (f *localFile) cacheFI() error {
if f.fi != nil { if f.fi != nil {
return nil return nil
} }
var fi os.FileInfo
var err error var err error
if f.f != nil { if f.f != nil {
f.fi, err = f.f.Stat() fi, err = f.f.Stat()
} else if f.flag&O_NOFOLLOW != 0 { } else if f.flag&O_NOFOLLOW != 0 {
f.fi, err = os.Lstat(f.name) fi, err = os.Lstat(f.name)
} else { } else {
f.fi, err = os.Stat(f.name) fi, err = os.Stat(f.name)
} }
return err if err != nil {
return err
}
f.fi = extendedStat(fi)
return nil
} }
func (f *localFile) Stat() (os.FileInfo, error) { func (f *localFile) Stat() (*ExtendedFileInfo, error) {
err := f.cacheFI() err := f.cacheFI()
// the call to cacheFI MUST happen before reading from f.fi // the call to cacheFI MUST happen before reading from f.fi
return f.fi, err return f.fi, err

View file

@ -84,13 +84,13 @@ func checkMetadata(t *testing.T, f File, path string, follow bool, nodeType rest
fi2, err = os.Lstat(path) fi2, err = os.Lstat(path)
} }
rtest.OK(t, err) rtest.OK(t, err)
assertFIEqual(t, fi2, fi) assertFIEqual(t, fi2, fi.FileInfo)
node, err := f.ToNode(false) node, err := f.ToNode(false)
rtest.OK(t, err) rtest.OK(t, err)
// ModTime is likely unique per file, thus it provides a good indication that it is from the correct file // ModTime is likely unique per file, thus it provides a good indication that it is from the correct file
rtest.Equals(t, fi.ModTime(), node.ModTime, "node ModTime") rtest.Equals(t, fi.ModTime, node.ModTime, "node ModTime")
rtest.Equals(t, nodeType, node.Type, "node Type") rtest.Equals(t, nodeType, node.Type, "node Type")
} }

View file

@ -1,7 +1,6 @@
package fs package fs
import ( import (
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@ -131,7 +130,7 @@ func (fs *LocalVss) OpenFile(name string, flag int, metadataOnly bool) (File, er
} }
// Lstat wraps the Lstat method of the underlying file system. // Lstat wraps the Lstat method of the underlying file system.
func (fs *LocalVss) Lstat(name string) (os.FileInfo, error) { func (fs *LocalVss) Lstat(name string) (*ExtendedFileInfo, error) {
return fs.FS.Lstat(fs.snapshotPath(name)) return fs.FS.Lstat(fs.snapshotPath(name))
} }

View file

@ -5,6 +5,7 @@ import (
"io" "io"
"os" "os"
"path" "path"
"slices"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@ -40,12 +41,14 @@ func (fs *Reader) VolumeName(_ string) string {
return "" return ""
} }
func (fs *Reader) fi() os.FileInfo { func (fs *Reader) fi() *ExtendedFileInfo {
return fakeFileInfo{ return &ExtendedFileInfo{
name: fs.Name, FileInfo: fakeFileInfo{
size: fs.Size, name: fs.Name,
mode: fs.Mode, size: fs.Size,
modtime: fs.ModTime, mode: fs.Mode,
modtime: fs.ModTime,
},
} }
} }
@ -68,7 +71,7 @@ func (fs *Reader) OpenFile(name string, flag int, _ bool) (f File, err error) {
return f, nil return f, nil
case "/", ".": case "/", ".":
f = fakeDir{ f = fakeDir{
entries: []os.FileInfo{fs.fi()}, entries: []string{fs.fi().Name()},
} }
return f, nil return f, nil
} }
@ -80,15 +83,15 @@ func (fs *Reader) OpenFile(name string, flag int, _ bool) (f File, err error) {
// If the file is a symbolic link, the returned FileInfo // If the file is a symbolic link, the returned FileInfo
// describes the symbolic link. Lstat makes no attempt to follow the link. // describes the symbolic link. Lstat makes no attempt to follow the link.
// If there is an error, it will be of type *os.PathError. // If there is an error, it will be of type *os.PathError.
func (fs *Reader) Lstat(name string) (os.FileInfo, error) { func (fs *Reader) Lstat(name string) (*ExtendedFileInfo, error) {
getDirInfo := func(name string) os.FileInfo { getDirInfo := func(name string) *ExtendedFileInfo {
fi := fakeFileInfo{ fi := fakeFileInfo{
name: fs.Base(name), name: fs.Base(name),
size: 0, size: 0,
mode: os.ModeDir | 0755, mode: os.ModeDir | 0755,
modtime: time.Now(), modtime: time.Now(),
} }
return fi return &ExtendedFileInfo{FileInfo: fi}
} }
switch name { switch name {
@ -112,16 +115,6 @@ func (fs *Reader) Lstat(name string) (os.FileInfo, error) {
return nil, pathError("lstat", name, os.ErrNotExist) return nil, pathError("lstat", name, os.ErrNotExist)
} }
func (fs *Reader) DeviceID(_ os.FileInfo) (deviceID uint64, err error) {
return 0, errors.New("Device IDs are not supported")
}
func (fs *Reader) ExtendedStat(fi os.FileInfo) ExtendedFileInfo {
return ExtendedFileInfo{
FileInfo: fi,
}
}
// Join joins any number of path elements into a single path, adding a // Join joins any number of path elements into a single path, adding a
// Separator if necessary. Join calls Clean on the result; in particular, all // Separator if necessary. Join calls Clean on the result; in particular, all
// empty strings are ignored. On Windows, the result is a UNC path if and only // empty strings are ignored. On Windows, the result is a UNC path if and only
@ -165,13 +158,13 @@ func (fs *Reader) Dir(p string) string {
return path.Dir(p) return path.Dir(p)
} }
func newReaderFile(rd io.ReadCloser, fi os.FileInfo, allowEmptyFile bool) *readerFile { func newReaderFile(rd io.ReadCloser, fi *ExtendedFileInfo, allowEmptyFile bool) *readerFile {
return &readerFile{ return &readerFile{
ReadCloser: rd, ReadCloser: rd,
AllowEmptyFile: allowEmptyFile, AllowEmptyFile: allowEmptyFile,
fakeFile: fakeFile{ fakeFile: fakeFile{
FileInfo: fi, fi: fi,
name: fi.Name(), name: fi.Name(),
}, },
} }
} }
@ -213,7 +206,7 @@ var _ File = &readerFile{}
// except Stat() // except Stat()
type fakeFile struct { type fakeFile struct {
name string name string
os.FileInfo fi *ExtendedFileInfo
} }
// ensure that fakeFile implements File // ensure that fakeFile implements File
@ -235,12 +228,12 @@ func (f fakeFile) Close() error {
return nil return nil
} }
func (f fakeFile) Stat() (os.FileInfo, error) { func (f fakeFile) Stat() (*ExtendedFileInfo, error) {
return f.FileInfo, nil return f.fi, nil
} }
func (f fakeFile) ToNode(_ bool) (*restic.Node, error) { func (f fakeFile) ToNode(_ bool) (*restic.Node, error) {
node := buildBasicNode(f.name, f.FileInfo) node := buildBasicNode(f.name, f.fi.FileInfo)
// fill minimal info with current values for uid, gid // fill minimal info with current values for uid, gid
node.UID = uint32(os.Getuid()) node.UID = uint32(os.Getuid())
@ -252,7 +245,7 @@ func (f fakeFile) ToNode(_ bool) (*restic.Node, error) {
// fakeDir implements Readdirnames and Readdir, everything else is delegated to fakeFile. // fakeDir implements Readdirnames and Readdir, everything else is delegated to fakeFile.
type fakeDir struct { type fakeDir struct {
entries []os.FileInfo entries []string
fakeFile fakeFile
} }
@ -260,12 +253,7 @@ func (d fakeDir) Readdirnames(n int) ([]string, error) {
if n > 0 { if n > 0 {
return nil, pathError("readdirnames", d.name, errors.New("not implemented")) return nil, pathError("readdirnames", d.name, errors.New("not implemented"))
} }
names := make([]string, 0, len(d.entries)) return slices.Clone(d.entries), nil
for _, entry := range d.entries {
names = append(names, entry.Name())
}
return names, nil
} }
// fakeFileInfo implements the bare minimum of os.FileInfo. // fakeFileInfo implements the bare minimum of os.FileInfo.

View file

@ -60,7 +60,7 @@ func verifyDirectoryContents(t testing.TB, fs FS, dir string, want []string) {
} }
} }
func checkFileInfo(t testing.TB, fi os.FileInfo, filename string, modtime time.Time, mode os.FileMode, isdir bool) { func checkFileInfo(t testing.TB, fi *ExtendedFileInfo, filename string, modtime time.Time, mode os.FileMode, isdir bool) {
if fi.IsDir() != isdir { if fi.IsDir() != isdir {
t.Errorf("IsDir returned %t, want %t", fi.IsDir(), isdir) t.Errorf("IsDir returned %t, want %t", fi.IsDir(), isdir)
} }
@ -69,8 +69,8 @@ func checkFileInfo(t testing.TB, fi os.FileInfo, filename string, modtime time.T
t.Errorf("Mode() returned wrong value, want 0%o, got 0%o", mode, fi.Mode()) t.Errorf("Mode() returned wrong value, want 0%o, got 0%o", mode, fi.Mode())
} }
if !modtime.Equal(time.Time{}) && !fi.ModTime().Equal(modtime) { if !modtime.Equal(time.Time{}) && !fi.FileInfo.ModTime().Equal(modtime) {
t.Errorf("ModTime() returned wrong value, want %v, got %v", modtime, fi.ModTime()) t.Errorf("ModTime() returned wrong value, want %v, got %v", modtime, fi.FileInfo.ModTime())
} }
if path.Base(fi.Name()) != fi.Name() { if path.Base(fi.Name()) != fi.Name() {

View file

@ -2,7 +2,6 @@ package fs
import ( import (
"io" "io"
"os"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
) )
@ -18,9 +17,7 @@ type FS interface {
// //
// Only the O_NOFOLLOW and O_DIRECTORY flags are supported. // Only the O_NOFOLLOW and O_DIRECTORY flags are supported.
OpenFile(name string, flag int, metadataOnly bool) (File, error) OpenFile(name string, flag int, metadataOnly bool) (File, error)
Lstat(name string) (os.FileInfo, error) Lstat(name string) (*ExtendedFileInfo, error)
DeviceID(fi os.FileInfo) (deviceID uint64, err error)
ExtendedStat(fi os.FileInfo) ExtendedFileInfo
Join(elem ...string) string Join(elem ...string) string
Separator() string Separator() string
@ -47,7 +44,7 @@ type File interface {
io.Closer io.Closer
Readdirnames(n int) ([]string, error) Readdirnames(n int) ([]string, error)
Stat() (os.FileInfo, error) Stat() (*ExtendedFileInfo, error)
// ToNode returns a restic.Node for the File. The internally used os.FileInfo // ToNode returns a restic.Node for the File. The internally used os.FileInfo
// must be consistent with that returned by Stat(). In particular, the metadata // must be consistent with that returned by Stat(). In particular, the metadata
// returned by consecutive calls to Stat() and ToNode() must match. // returned by consecutive calls to Stat() and ToNode() must match.

View file

@ -15,15 +15,14 @@ import (
// nodeFromFileInfo returns a new node from the given path and FileInfo. It // nodeFromFileInfo returns a new node from the given path and FileInfo. It
// returns the first error that is encountered, together with a node. // returns the first error that is encountered, together with a node.
func nodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) { func nodeFromFileInfo(path string, fi *ExtendedFileInfo, ignoreXattrListError bool) (*restic.Node, error) {
node := buildBasicNode(path, fi) node := buildBasicNode(path, fi.FileInfo)
stat := ExtendedStat(fi) if err := nodeFillExtendedStat(node, path, fi); err != nil {
if err := nodeFillExtendedStat(node, path, &stat); err != nil {
return node, err return node, err
} }
err := nodeFillGenericAttributes(node, path, &stat) err := nodeFillGenericAttributes(node, path, fi)
err = errors.Join(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError)) err = errors.Join(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError))
return node, err return node, err
} }
@ -37,15 +36,15 @@ func buildBasicNode(path string, fi os.FileInfo) *restic.Node {
ModTime: fi.ModTime(), ModTime: fi.ModTime(),
} }
node.Type = nodeTypeFromFileInfo(fi) node.Type = nodeTypeFromFileInfo(fi.Mode())
if node.Type == restic.NodeTypeFile { if node.Type == restic.NodeTypeFile {
node.Size = uint64(fi.Size()) node.Size = uint64(fi.Size())
} }
return node return node
} }
func nodeTypeFromFileInfo(fi os.FileInfo) restic.NodeType { func nodeTypeFromFileInfo(mode os.FileMode) restic.NodeType {
switch fi.Mode() & os.ModeType { switch mode & os.ModeType {
case 0: case 0:
return restic.NodeTypeFile return restic.NodeTypeFile
case os.ModeDir: case os.ModeDir:

View file

@ -26,7 +26,7 @@ type ExtendedFileInfo struct {
} }
// ExtendedStat returns an ExtendedFileInfo constructed from the os.FileInfo. // ExtendedStat returns an ExtendedFileInfo constructed from the os.FileInfo.
func ExtendedStat(fi os.FileInfo) ExtendedFileInfo { func ExtendedStat(fi os.FileInfo) *ExtendedFileInfo {
if fi == nil { if fi == nil {
panic("os.FileInfo is nil") panic("os.FileInfo is nil")
} }

View file

@ -10,10 +10,10 @@ import (
) )
// extendedStat extracts info into an ExtendedFileInfo for unix based operating systems. // extendedStat extracts info into an ExtendedFileInfo for unix based operating systems.
func extendedStat(fi os.FileInfo) ExtendedFileInfo { func extendedStat(fi os.FileInfo) *ExtendedFileInfo {
s := fi.Sys().(*syscall.Stat_t) s := fi.Sys().(*syscall.Stat_t)
extFI := ExtendedFileInfo{ return &ExtendedFileInfo{
FileInfo: fi, FileInfo: fi,
DeviceID: uint64(s.Dev), DeviceID: uint64(s.Dev),
Inode: uint64(s.Ino), Inode: uint64(s.Ino),
@ -29,6 +29,4 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo {
ModTime: time.Unix(s.Mtimespec.Unix()), ModTime: time.Unix(s.Mtimespec.Unix()),
ChangeTime: time.Unix(s.Ctimespec.Unix()), ChangeTime: time.Unix(s.Ctimespec.Unix()),
} }
return extFI
} }

View file

@ -10,10 +10,10 @@ import (
) )
// extendedStat extracts info into an ExtendedFileInfo for unix based operating systems. // extendedStat extracts info into an ExtendedFileInfo for unix based operating systems.
func extendedStat(fi os.FileInfo) ExtendedFileInfo { func extendedStat(fi os.FileInfo) *ExtendedFileInfo {
s := fi.Sys().(*syscall.Stat_t) s := fi.Sys().(*syscall.Stat_t)
extFI := ExtendedFileInfo{ return &ExtendedFileInfo{
FileInfo: fi, FileInfo: fi,
DeviceID: uint64(s.Dev), DeviceID: uint64(s.Dev),
Inode: s.Ino, Inode: s.Ino,
@ -29,6 +29,4 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo {
ModTime: time.Unix(s.Mtim.Unix()), ModTime: time.Unix(s.Mtim.Unix()),
ChangeTime: time.Unix(s.Ctim.Unix()), ChangeTime: time.Unix(s.Ctim.Unix()),
} }
return extFI
} }

View file

@ -11,7 +11,7 @@ import (
) )
// extendedStat extracts info into an ExtendedFileInfo for Windows. // extendedStat extracts info into an ExtendedFileInfo for Windows.
func extendedStat(fi os.FileInfo) ExtendedFileInfo { func extendedStat(fi os.FileInfo) *ExtendedFileInfo {
s, ok := fi.Sys().(*syscall.Win32FileAttributeData) s, ok := fi.Sys().(*syscall.Win32FileAttributeData)
if !ok { if !ok {
panic(fmt.Sprintf("conversion to syscall.Win32FileAttributeData failed, type is %T", fi.Sys())) panic(fmt.Sprintf("conversion to syscall.Win32FileAttributeData failed, type is %T", fi.Sys()))
@ -31,5 +31,5 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo {
// Windows does not have the concept of a "change time" in the sense Unix uses it, so we're using the LastWriteTime here. // Windows does not have the concept of a "change time" in the sense Unix uses it, so we're using the LastWriteTime here.
extFI.ChangeTime = extFI.ModTime extFI.ChangeTime = extFI.ModTime
return extFI return &extFI
} }