diff --git a/cmd/restic/cmd_backup_integration_test.go b/cmd/restic/cmd_backup_integration_test.go index 4278f07ca..06d71e345 100644 --- a/cmd/restic/cmd_backup_integration_test.go +++ b/cmd/restic/cmd_backup_integration_test.go @@ -132,7 +132,7 @@ type vssDeleteOriginalFS struct { hasRemoved bool } -func (f *vssDeleteOriginalFS) Lstat(name string) (os.FileInfo, error) { +func (f *vssDeleteOriginalFS) Lstat(name string) (*fs.ExtendedFileInfo, error) { if !f.hasRemoved { // call Lstat to trigger snapshot creation _, _ = f.FS.Lstat(name) diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index 5d4648e03..f730fe0a5 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -25,7 +25,7 @@ type SelectByNameFunc func(item string) bool // 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. -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 // 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{ Repo: repo, 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, 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 // to the contents of node, which describes the same path in the parent backup. // 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 { case node == nil: return true case node.Type != restic.NodeTypeFile: // We're only called for regular files, so this is a type change. return true - case uint64(fi.Size()) != node.Size: + case uint64(fi.Size) != node.Size: return true - case !fi.ModTime().Equal(node.ModTime): + case !fi.ModTime.Equal(node.ModTime): return true } checkCtime := ignoreFlags&ChangeIgnoreCtime == 0 checkInode := ignoreFlags&ChangeIgnoreInode == 0 - extFI := fs.ExtendedStat(fi) switch { - case checkCtime && !extFI.ChangeTime.Equal(node.ChangeTime): + case checkCtime && !fi.ChangeTime.Equal(node.ChangeTime): return true - case checkInode && node.Inode != extFI.Inode: + case checkInode && node.Inode != fi.Inode: return true } diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go index f57c4894b..038afd11d 100644 --- a/internal/archiver/archiver_test.go +++ b/internal/archiver/archiver_test.go @@ -516,13 +516,13 @@ func chmodTwice(t testing.TB, name string) { 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) if err != nil { t.Fatal(err) } - return fi + return fs.ExtendedStat(fi) } func setTimestamp(t testing.TB, filename string, atime, mtime time.Time) { @@ -660,7 +660,7 @@ func TestFileChanged(t *testing.T) { rename(t, filename, tempname) save(t, filename, defaultContent) remove(t, tempname) - setTimestamp(t, filename, fi.ModTime(), fi.ModTime()) + setTimestamp(t, filename, fi.ModTime, fi.ModTime) }, ChangeIgnore: ChangeIgnoreCtime | ChangeIgnoreInode, SameFile: true, @@ -1520,7 +1520,7 @@ func TestArchiverSnapshotSelect(t *testing.T) { }, "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 }, }, @@ -1537,7 +1537,7 @@ func TestArchiverSnapshotSelect(t *testing.T) { }, "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 }, err: "snapshot is empty", @@ -1564,7 +1564,7 @@ func TestArchiverSnapshotSelect(t *testing.T) { }, "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" }, }, @@ -1588,7 +1588,7 @@ func TestArchiverSnapshotSelect(t *testing.T) { }, "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" }, }, @@ -1597,7 +1597,7 @@ func TestArchiverSnapshotSelect(t *testing.T) { src: TestDir{ "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) }, }, @@ -2202,7 +2202,7 @@ func snapshot(t testing.TB, repo archiverRepo, fs fs.FS, parent *restic.Snapshot type overrideFS struct { fs.FS - overrideFI os.FileInfo + overrideFI *fs.ExtendedFileInfo resetFIOnRead bool overrideNode *restic.Node overrideErr error @@ -2225,7 +2225,7 @@ type overrideFile struct { ofs *overrideFS } -func (f overrideFile) Stat() (os.FileInfo, error) { +func (f overrideFile) Stat() (*fs.ExtendedFileInfo, error) { if f.ofs.overrideFI == nil { return f.File.Stat() } @@ -2497,7 +2497,7 @@ type missingFile struct { fs.File } -func (f *missingFile) Stat() (os.FileInfo, error) { +func (f *missingFile) Stat() (*fs.ExtendedFileInfo, error) { return nil, os.ErrNotExist } diff --git a/internal/archiver/archiver_unix_test.go b/internal/archiver/archiver_unix_test.go index deeab6459..d3e87b57e 100644 --- a/internal/archiver/archiver_unix_test.go +++ b/internal/archiver/archiver_unix_test.go @@ -29,7 +29,7 @@ func (fi wrappedFileInfo) Mode() os.FileMode { } // 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 stat := fi.Sys().(*syscall.Stat_t) stat.Mode = mockFileInfoMode @@ -37,22 +37,22 @@ func wrapFileInfo(fi os.FileInfo) os.FileInfo { stat.Gid = mockFileInfoGID // wrap the os.FileInfo so we can return a modified stat_t - res := wrappedFileInfo{ - FileInfo: fi, + return fs.ExtendedStat(wrappedFileInfo{ + FileInfo: fi.FileInfo, sys: stat, mode: mockFileInfoMode, - } - - return res + }) } // 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 - return wrappedFileInfo{ - FileInfo: fi, - sys: fi.Sys().(*syscall.Stat_t), - mode: (fi.Mode() &^ os.ModeType) | os.ModeIrregular, + return &fs.ExtendedFileInfo{ + FileInfo: wrappedFileInfo{ + FileInfo: fi.FileInfo, + sys: fi.Sys(), + mode: (fi.Mode() &^ os.ModeType) | os.ModeIrregular, + }, } } diff --git a/internal/archiver/archiver_windows_test.go b/internal/archiver/archiver_windows_test.go index ac8a67f2b..2e873c1b7 100644 --- a/internal/archiver/archiver_windows_test.go +++ b/internal/archiver/archiver_windows_test.go @@ -5,6 +5,8 @@ package archiver import ( "os" + + "github.com/restic/restic/internal/fs" ) 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. -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 - res := wrappedFileInfo{ - FileInfo: fi, + return fs.ExtendedStat(wrappedFileInfo{ + FileInfo: fi.FileInfo, mode: mockFileInfoMode, - } - - return res + }) } // wrapIrregularFileInfo returns a new os.FileInfo with the mode changed to irregular file -func wrapIrregularFileInfo(fi os.FileInfo) os.FileInfo { - return wrappedFileInfo{ - FileInfo: fi, - mode: (fi.Mode() &^ os.ModeType) | os.ModeIrregular, +func wrapIrregularFileInfo(fi *fs.ExtendedFileInfo) *fs.ExtendedFileInfo { + return &fs.ExtendedFileInfo{ + FileInfo: wrappedFileInfo{ + FileInfo: fi.FileInfo, + mode: (fi.Mode() &^ os.ModeType) | os.ModeIrregular, + }, } } diff --git a/internal/archiver/exclude.go b/internal/archiver/exclude.go index 418517fd9..e1939d292 100644 --- a/internal/archiver/exclude.go +++ b/internal/archiver/exclude.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "runtime" "strings" "sync" @@ -21,7 +22,7 @@ type RejectByNameFunc func(path string) bool // 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 // 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 { return func(item string) bool { @@ -35,7 +36,7 @@ func CombineRejectByNames(funcs []RejectByNameFunc) SelectByNameFunc { } 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 { if reject(item, fi, fs) { return false @@ -104,7 +105,7 @@ func RejectIfPresent(excludeFileSpec string, warnf func(msg string, args ...inte } debug.Log("using %q as exclusion tagfile", tf) 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) }, nil } @@ -186,6 +187,10 @@ type deviceMap map[string]uint64 // newDeviceMap creates a new device map from the list of source paths. 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) for _, item := range allowedSourcePaths { @@ -199,12 +204,7 @@ func newDeviceMap(allowedSourcePaths []string, fs fs.FS) (deviceMap, error) { return nil, err } - id, err := fs.DeviceID(fi) - if err != nil { - return nil, err - } - - deviceMap[item] = id + deviceMap[item] = fi.DeviceID } if len(deviceMap) == 0 { @@ -254,15 +254,8 @@ func RejectByDevice(samples []string, filesystem fs.FS) (RejectFunc, error) { } debug.Log("allowed devices: %v\n", deviceMap) - return func(item string, fi os.FileInfo, fs fs.FS) bool { - id, err := fs.DeviceID(fi) - 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) + return func(item string, fi *fs.ExtendedFileInfo, fs fs.FS) bool { + allowed, err := deviceMap.IsAllowed(fs.Clean(item), fi.DeviceID, fs) if err != nil { // this should not happen 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 } - parentDeviceID, err := fs.DeviceID(parentFI) - 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) + parentAllowed, err := deviceMap.IsAllowed(parentDir, parentFI.DeviceID, fs) if err != nil { debug.Log("item %v: error checking parent directory: %v", item, err) // if in doubt, reject @@ -315,13 +301,13 @@ func RejectByDevice(samples []string, filesystem fs.FS) (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 if fi.IsDir() { return false } - filesize := fi.Size() + filesize := fi.Size if filesize > maxSize { debug.Log("file %s is oversize: %d", item, filesize) return true diff --git a/internal/archiver/exclude_test.go b/internal/archiver/exclude_test.go index 7eb24b08b..9bfa5d83f 100644 --- a/internal/archiver/exclude_test.go +++ b/internal/archiver/exclude_test.go @@ -193,7 +193,7 @@ func TestIsExcludedByFileSize(t *testing.T) { return err } - excluded := sizeExclude(p, fi, nil) + excluded := sizeExclude(p, fs.ExtendedStat(fi), nil) // 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) m[p] = !excluded diff --git a/internal/archiver/scanner.go b/internal/archiver/scanner.go index debd09aa3..ebcca0df1 100644 --- a/internal/archiver/scanner.go +++ b/internal/archiver/scanner.go @@ -2,7 +2,6 @@ package archiver import ( "context" - "os" "sort" "github.com/restic/restic/internal/debug" @@ -25,7 +24,7 @@ func NewScanner(filesystem fs.FS) *Scanner { return &Scanner{ FS: filesystem, 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 }, Result: func(_ string, _ ScanStats) {}, } @@ -121,7 +120,7 @@ func (s *Scanner) scan(ctx context.Context, stats ScanStats, target string) (Sca switch { case fi.Mode().IsRegular(): stats.Files++ - stats.Bytes += uint64(fi.Size()) + stats.Bytes += uint64(fi.Size) case fi.Mode().IsDir(): names, err := fs.Readdirnames(s.FS, target, fs.O_NOFOLLOW) if err != nil { diff --git a/internal/archiver/scanner_test.go b/internal/archiver/scanner_test.go index e4e2c9f59..0504cb8bd 100644 --- a/internal/archiver/scanner_test.go +++ b/internal/archiver/scanner_test.go @@ -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() { return true } diff --git a/internal/fs/deviceid_unix.go b/internal/fs/deviceid_unix.go deleted file mode 100644 index 4d5593335..000000000 --- a/internal/fs/deviceid_unix.go +++ /dev/null @@ -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") -} diff --git a/internal/fs/deviceid_windows.go b/internal/fs/deviceid_windows.go deleted file mode 100644 index bfb22dc9a..000000000 --- a/internal/fs/deviceid_windows.go +++ /dev/null @@ -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") -} diff --git a/internal/fs/fs_local.go b/internal/fs/fs_local.go index 5e6c72d0a..fc6c69cf2 100644 --- a/internal/fs/fs_local.go +++ b/internal/fs/fs_local.go @@ -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 // 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) { - return deviceID(fi) -} - -// ExtendedStat converts the give FileInfo into ExtendedFileInfo. -func (fs Local) ExtendedStat(fi os.FileInfo) ExtendedFileInfo { - return ExtendedStat(fi) +func (fs Local) Lstat(name string) (*ExtendedFileInfo, error) { + fi, err := os.Lstat(fixpath(name)) + if err != nil { + return nil, err + } + return extendedStat(fi), nil } // Join joins any number of path elements into a single path, adding a @@ -96,7 +89,7 @@ type localFile struct { name string flag int f *os.File - fi os.FileInfo + fi *ExtendedFileInfo } // See the File interface for a description of each method @@ -137,18 +130,23 @@ func (f *localFile) cacheFI() error { if f.fi != nil { return nil } + var fi os.FileInfo var err error if f.f != nil { - f.fi, err = f.f.Stat() + fi, err = f.f.Stat() } else if f.flag&O_NOFOLLOW != 0 { - f.fi, err = os.Lstat(f.name) + fi, err = os.Lstat(f.name) } 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() // the call to cacheFI MUST happen before reading from f.fi return f.fi, err diff --git a/internal/fs/fs_local_test.go b/internal/fs/fs_local_test.go index b1e85de0a..74cc8b48c 100644 --- a/internal/fs/fs_local_test.go +++ b/internal/fs/fs_local_test.go @@ -84,13 +84,13 @@ func checkMetadata(t *testing.T, f File, path string, follow bool, nodeType rest fi2, err = os.Lstat(path) } rtest.OK(t, err) - assertFIEqual(t, fi2, fi) + assertFIEqual(t, fi2, fi.FileInfo) node, err := f.ToNode(false) rtest.OK(t, err) // 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") } diff --git a/internal/fs/fs_local_vss.go b/internal/fs/fs_local_vss.go index fe82b85e1..dfee31779 100644 --- a/internal/fs/fs_local_vss.go +++ b/internal/fs/fs_local_vss.go @@ -1,7 +1,6 @@ package fs import ( - "os" "path/filepath" "runtime" "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. -func (fs *LocalVss) Lstat(name string) (os.FileInfo, error) { +func (fs *LocalVss) Lstat(name string) (*ExtendedFileInfo, error) { return fs.FS.Lstat(fs.snapshotPath(name)) } diff --git a/internal/fs/fs_reader.go b/internal/fs/fs_reader.go index 8728b274c..8b7668730 100644 --- a/internal/fs/fs_reader.go +++ b/internal/fs/fs_reader.go @@ -5,6 +5,7 @@ import ( "io" "os" "path" + "slices" "sync" "syscall" "time" @@ -40,12 +41,14 @@ func (fs *Reader) VolumeName(_ string) string { return "" } -func (fs *Reader) fi() os.FileInfo { - return fakeFileInfo{ - name: fs.Name, - size: fs.Size, - mode: fs.Mode, - modtime: fs.ModTime, +func (fs *Reader) fi() *ExtendedFileInfo { + return &ExtendedFileInfo{ + FileInfo: fakeFileInfo{ + name: fs.Name, + size: fs.Size, + 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 case "/", ".": f = fakeDir{ - entries: []os.FileInfo{fs.fi()}, + entries: []string{fs.fi().Name()}, } 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 // describes the symbolic link. Lstat makes no attempt to follow the link. // If there is an error, it will be of type *os.PathError. -func (fs *Reader) Lstat(name string) (os.FileInfo, error) { - getDirInfo := func(name string) os.FileInfo { +func (fs *Reader) Lstat(name string) (*ExtendedFileInfo, error) { + getDirInfo := func(name string) *ExtendedFileInfo { fi := fakeFileInfo{ name: fs.Base(name), size: 0, mode: os.ModeDir | 0755, modtime: time.Now(), } - return fi + return &ExtendedFileInfo{FileInfo: fi} } switch name { @@ -112,16 +115,6 @@ func (fs *Reader) Lstat(name string) (os.FileInfo, error) { 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 // 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 @@ -165,13 +158,13 @@ func (fs *Reader) Dir(p string) string { 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{ ReadCloser: rd, AllowEmptyFile: allowEmptyFile, fakeFile: fakeFile{ - FileInfo: fi, - name: fi.Name(), + fi: fi, + name: fi.Name(), }, } } @@ -213,7 +206,7 @@ var _ File = &readerFile{} // except Stat() type fakeFile struct { name string - os.FileInfo + fi *ExtendedFileInfo } // ensure that fakeFile implements File @@ -235,12 +228,12 @@ func (f fakeFile) Close() error { return nil } -func (f fakeFile) Stat() (os.FileInfo, error) { - return f.FileInfo, nil +func (f fakeFile) Stat() (*ExtendedFileInfo, error) { + return f.fi, nil } 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 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. type fakeDir struct { - entries []os.FileInfo + entries []string fakeFile } @@ -260,12 +253,7 @@ func (d fakeDir) Readdirnames(n int) ([]string, error) { if n > 0 { return nil, pathError("readdirnames", d.name, errors.New("not implemented")) } - names := make([]string, 0, len(d.entries)) - for _, entry := range d.entries { - names = append(names, entry.Name()) - } - - return names, nil + return slices.Clone(d.entries), nil } // fakeFileInfo implements the bare minimum of os.FileInfo. diff --git a/internal/fs/fs_reader_test.go b/internal/fs/fs_reader_test.go index 7e7f6e77c..f2e8b2013 100644 --- a/internal/fs/fs_reader_test.go +++ b/internal/fs/fs_reader_test.go @@ -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 { 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()) } - if !modtime.Equal(time.Time{}) && !fi.ModTime().Equal(modtime) { - t.Errorf("ModTime() returned wrong value, want %v, got %v", modtime, fi.ModTime()) + if !modtime.Equal(time.Time{}) && !fi.FileInfo.ModTime().Equal(modtime) { + t.Errorf("ModTime() returned wrong value, want %v, got %v", modtime, fi.FileInfo.ModTime()) } if path.Base(fi.Name()) != fi.Name() { diff --git a/internal/fs/interface.go b/internal/fs/interface.go index 7ff777138..d75b0a91d 100644 --- a/internal/fs/interface.go +++ b/internal/fs/interface.go @@ -2,7 +2,6 @@ package fs import ( "io" - "os" "github.com/restic/restic/internal/restic" ) @@ -18,9 +17,7 @@ type FS interface { // // Only the O_NOFOLLOW and O_DIRECTORY flags are supported. OpenFile(name string, flag int, metadataOnly bool) (File, error) - Lstat(name string) (os.FileInfo, error) - DeviceID(fi os.FileInfo) (deviceID uint64, err error) - ExtendedStat(fi os.FileInfo) ExtendedFileInfo + Lstat(name string) (*ExtendedFileInfo, error) Join(elem ...string) string Separator() string @@ -47,7 +44,7 @@ type File interface { io.Closer 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 // must be consistent with that returned by Stat(). In particular, the metadata // returned by consecutive calls to Stat() and ToNode() must match. diff --git a/internal/fs/node.go b/internal/fs/node.go index 065969537..be91562a4 100644 --- a/internal/fs/node.go +++ b/internal/fs/node.go @@ -15,15 +15,14 @@ import ( // nodeFromFileInfo returns a new node from the given path and FileInfo. It // returns the first error that is encountered, together with a node. -func nodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) { - node := buildBasicNode(path, fi) +func nodeFromFileInfo(path string, fi *ExtendedFileInfo, ignoreXattrListError bool) (*restic.Node, error) { + node := buildBasicNode(path, fi.FileInfo) - stat := ExtendedStat(fi) - if err := nodeFillExtendedStat(node, path, &stat); err != nil { + if err := nodeFillExtendedStat(node, path, fi); err != nil { return node, err } - err := nodeFillGenericAttributes(node, path, &stat) + err := nodeFillGenericAttributes(node, path, fi) err = errors.Join(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError)) return node, err } @@ -37,15 +36,15 @@ func buildBasicNode(path string, fi os.FileInfo) *restic.Node { ModTime: fi.ModTime(), } - node.Type = nodeTypeFromFileInfo(fi) + node.Type = nodeTypeFromFileInfo(fi.Mode()) if node.Type == restic.NodeTypeFile { node.Size = uint64(fi.Size()) } return node } -func nodeTypeFromFileInfo(fi os.FileInfo) restic.NodeType { - switch fi.Mode() & os.ModeType { +func nodeTypeFromFileInfo(mode os.FileMode) restic.NodeType { + switch mode & os.ModeType { case 0: return restic.NodeTypeFile case os.ModeDir: diff --git a/internal/fs/stat.go b/internal/fs/stat.go index e1006fd61..9e5be51e1 100644 --- a/internal/fs/stat.go +++ b/internal/fs/stat.go @@ -26,7 +26,7 @@ type ExtendedFileInfo struct { } // ExtendedStat returns an ExtendedFileInfo constructed from the os.FileInfo. -func ExtendedStat(fi os.FileInfo) ExtendedFileInfo { +func ExtendedStat(fi os.FileInfo) *ExtendedFileInfo { if fi == nil { panic("os.FileInfo is nil") } diff --git a/internal/fs/stat_bsd.go b/internal/fs/stat_bsd.go index 11e075b50..de2254d24 100644 --- a/internal/fs/stat_bsd.go +++ b/internal/fs/stat_bsd.go @@ -10,10 +10,10 @@ import ( ) // 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) - extFI := ExtendedFileInfo{ + return &ExtendedFileInfo{ FileInfo: fi, DeviceID: uint64(s.Dev), Inode: uint64(s.Ino), @@ -29,6 +29,4 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo { ModTime: time.Unix(s.Mtimespec.Unix()), ChangeTime: time.Unix(s.Ctimespec.Unix()), } - - return extFI } diff --git a/internal/fs/stat_unix.go b/internal/fs/stat_unix.go index c55571031..46077402f 100644 --- a/internal/fs/stat_unix.go +++ b/internal/fs/stat_unix.go @@ -10,10 +10,10 @@ import ( ) // 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) - extFI := ExtendedFileInfo{ + return &ExtendedFileInfo{ FileInfo: fi, DeviceID: uint64(s.Dev), Inode: s.Ino, @@ -29,6 +29,4 @@ func extendedStat(fi os.FileInfo) ExtendedFileInfo { ModTime: time.Unix(s.Mtim.Unix()), ChangeTime: time.Unix(s.Ctim.Unix()), } - - return extFI } diff --git a/internal/fs/stat_windows.go b/internal/fs/stat_windows.go index 57f330fb5..0dbc429fb 100644 --- a/internal/fs/stat_windows.go +++ b/internal/fs/stat_windows.go @@ -11,7 +11,7 @@ import ( ) // 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) if !ok { 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. extFI.ChangeTime = extFI.ModTime - return extFI + return &extFI }