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..55b6ee4b3 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(), @@ -505,12 +505,12 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous } switch { - case fi.Mode().IsRegular(): + case fi.Mode.IsRegular(): debug.Log(" %v regular file", target) // check if the file has not changed before performing a fopen operation (more expensive, specially // in network filesystems) - if previous != nil && !fileChanged(arch.FS, fi, previous, arch.ChangeIgnoreFlags) { + if previous != nil && !fileChanged(fi, previous, arch.ChangeIgnoreFlags) { if arch.allBlobsPresent(previous) { debug.Log("%v hasn't changed, using old list of blobs", target) arch.trackItem(snPath, previous, previous, ItemStats{}, time.Since(start)) @@ -555,7 +555,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous } // make sure it's still a file - if !fi.Mode().IsRegular() { + if !fi.Mode.IsRegular() { err = errors.Errorf("file %q changed type, refusing to archive", target) return filterError(err) } @@ -571,7 +571,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous arch.trackItem(snPath, previous, node, stats, time.Since(start)) }) - case fi.IsDir(): + case fi.Mode.IsDir(): debug.Log(" %v dir", target) snItem := snPath + "/" @@ -592,7 +592,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous return futureNode{}, false, err } - case fi.Mode()&os.ModeSocket > 0: + case fi.Mode&os.ModeSocket > 0: debug.Log(" %v is a socket, ignoring", target) return futureNode{}, true, nil @@ -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(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..fcc3d465d 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, @@ -683,10 +683,11 @@ func TestFileChanged(t *testing.T) { save(t, filename, content) fs := &fs.Local{} - fiBefore := lstat(t, filename) + fiBefore, err := fs.Lstat(filename) + rtest.OK(t, err) node := nodeFromFile(t, fs, filename) - if fileChanged(fs, fiBefore, node, 0) { + if fileChanged(fiBefore, node, 0) { t.Fatalf("unchanged file detected as changed") } @@ -696,12 +697,12 @@ func TestFileChanged(t *testing.T) { if test.SameFile { // file should be detected as unchanged - if fileChanged(fs, fiAfter, node, test.ChangeIgnore) { + if fileChanged(fiAfter, node, test.ChangeIgnore) { t.Fatalf("unmodified file detected as changed") } } else { // file should be detected as changed - if !fileChanged(fs, fiAfter, node, test.ChangeIgnore) && !test.SameFile { + if !fileChanged(fiAfter, node, test.ChangeIgnore) && !test.SameFile { t.Fatalf("modified file detected as unchanged") } } @@ -718,7 +719,7 @@ func TestFilChangedSpecialCases(t *testing.T) { t.Run("nil-node", func(t *testing.T) { fi := lstat(t, filename) - if !fileChanged(&fs.Local{}, fi, nil, 0) { + if !fileChanged(fi, nil, 0) { t.Fatal("nil node detected as unchanged") } }) @@ -727,7 +728,7 @@ func TestFilChangedSpecialCases(t *testing.T) { fi := lstat(t, filename) node := nodeFromFile(t, &fs.Local{}, filename) node.Type = restic.NodeTypeSymlink - if !fileChanged(&fs.Local{}, fi, node, 0) { + if !fileChanged(fi, node, 0) { t.Fatal("node with changed type detected as unchanged") } }) @@ -1520,7 +1521,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 +1538,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 +1565,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 +1589,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 +1598,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 +2203,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 +2226,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() } @@ -2302,19 +2303,26 @@ func TestMetadataChanged(t *testing.T) { t.Fatalf("metadata does not match:\n%v", cmp.Diff(want, node2)) } - // modify the mode by wrapping it in a new struct, uses the consts defined above - fs.overrideFI = wrapFileInfo(fi) - rtest.Assert(t, !fileChanged(fs, fs.overrideFI, node2, 0), "testfile must not be considered as changed") + // modify the mode and UID/GID + modFI := *fi + modFI.Mode = mockFileInfoMode + if runtime.GOOS != "windows" { + modFI.UID = mockFileInfoUID + modFI.GID = mockFileInfoGID + } + + fs.overrideFI = &modFI + rtest.Assert(t, !fileChanged(fs.overrideFI, node2, 0), "testfile must not be considered as changed") // set the override values in the 'want' node which - want.Mode = 0400 + want.Mode = mockFileInfoMode // ignore UID and GID on Windows if runtime.GOOS != "windows" { - want.UID = 51234 - want.GID = 51235 + want.UID = mockFileInfoUID + want.GID = mockFileInfoGID } // update mock node accordingly - fs.overrideNode.Mode = 0400 + fs.overrideNode.Mode = want.Mode fs.overrideNode.UID = want.UID fs.overrideNode.GID = want.GID @@ -2455,10 +2463,12 @@ func TestIrregularFile(t *testing.T) { tempfile := filepath.Join(tempdir, "testfile") fi := lstat(t, "testfile") + // patch mode to irregular + fi.Mode = (fi.Mode &^ os.ModeType) | os.ModeIrregular override := &overrideFS{ FS: fs.Local{}, - overrideFI: wrapIrregularFileInfo(fi), + overrideFI: fi, overrideNode: &restic.Node{ Type: restic.NodeTypeIrregular, }, @@ -2497,7 +2507,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..b6cc1ba4e 100644 --- a/internal/archiver/archiver_unix_test.go +++ b/internal/archiver/archiver_unix_test.go @@ -4,8 +4,6 @@ package archiver import ( - "os" - "syscall" "testing" "github.com/restic/restic/internal/feature" @@ -14,48 +12,6 @@ import ( rtest "github.com/restic/restic/internal/test" ) -type wrappedFileInfo struct { - os.FileInfo - sys interface{} - mode os.FileMode -} - -func (fi wrappedFileInfo) Sys() interface{} { - return fi.sys -} - -func (fi wrappedFileInfo) Mode() os.FileMode { - return fi.mode -} - -// wrapFileInfo returns a new os.FileInfo with the mode, owner, and group fields changed. -func wrapFileInfo(fi os.FileInfo) os.FileInfo { - // get the underlying stat_t and modify the values - stat := fi.Sys().(*syscall.Stat_t) - stat.Mode = mockFileInfoMode - stat.Uid = mockFileInfoUID - stat.Gid = mockFileInfoGID - - // wrap the os.FileInfo so we can return a modified stat_t - res := wrappedFileInfo{ - FileInfo: fi, - 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 { - // 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, - } -} - func statAndSnapshot(t *testing.T, repo archiverRepo, name string) (*restic.Node, *restic.Node) { want := nodeFromFile(t, &fs.Local{}, name) _, node := snapshot(t, repo, &fs.Local{}, nil, name) diff --git a/internal/archiver/archiver_windows_test.go b/internal/archiver/archiver_windows_test.go deleted file mode 100644 index ac8a67f2b..000000000 --- a/internal/archiver/archiver_windows_test.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build windows -// +build windows - -package archiver - -import ( - "os" -) - -type wrappedFileInfo struct { - os.FileInfo - mode os.FileMode -} - -func (fi wrappedFileInfo) Mode() os.FileMode { - return fi.mode -} - -// wrapFileInfo returns a new os.FileInfo with the mode, owner, and group fields changed. -func wrapFileInfo(fi os.FileInfo) os.FileInfo { - // wrap the os.FileInfo and return the modified mode, uid and gid are ignored on Windows - res := wrappedFileInfo{ - FileInfo: fi, - 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, - } -} diff --git a/internal/archiver/exclude.go b/internal/archiver/exclude.go index 418517fd9..6db62aa20 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)) @@ -274,7 +267,7 @@ func RejectByDevice(samples []string, filesystem fs.FS) (RejectFunc, error) { } // reject everything except directories - if !fi.IsDir() { + if !fi.Mode.IsDir() { return true } @@ -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() { + if fi.Mode.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..2e6b7210c 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) {}, } @@ -119,10 +118,10 @@ func (s *Scanner) scan(ctx context.Context, stats ScanStats, target string) (Sca } switch { - case fi.Mode().IsRegular(): + case fi.Mode.IsRegular(): stats.Files++ - stats.Bytes += uint64(fi.Size()) - case fi.Mode().IsDir(): + stats.Bytes += uint64(fi.Size) + case fi.Mode.IsDir(): names, err := fs.Readdirnames(s.FS, target, fs.O_NOFOLLOW) if err != nil { return stats, s.Error(target, err) diff --git a/internal/archiver/scanner_test.go b/internal/archiver/scanner_test.go index e4e2c9f59..a47952388 100644 --- a/internal/archiver/scanner_test.go +++ b/internal/archiver/scanner_test.go @@ -56,8 +56,8 @@ func TestScanner(t *testing.T) { }, }, }, - selFn: func(item string, fi os.FileInfo, fs fs.FS) bool { - if fi.IsDir() { + selFn: func(item string, fi *fs.ExtendedFileInfo, fs fs.FS) bool { + if fi.Mode.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..8fd8eb136 100644 --- a/internal/fs/fs_local_test.go +++ b/internal/fs/fs_local_test.go @@ -90,17 +90,16 @@ func checkMetadata(t *testing.T, f File, path string, follow bool, nodeType rest 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") } -func assertFIEqual(t *testing.T, want os.FileInfo, got os.FileInfo) { +func assertFIEqual(t *testing.T, want os.FileInfo, got *ExtendedFileInfo) { t.Helper() - rtest.Equals(t, want.Name(), got.Name(), "Name") - rtest.Equals(t, want.IsDir(), got.IsDir(), "IsDir") - rtest.Equals(t, want.ModTime(), got.ModTime(), "ModTime") - rtest.Equals(t, want.Mode(), got.Mode(), "Mode") - rtest.Equals(t, want.Size(), got.Size(), "Size") + rtest.Equals(t, want.Name(), got.Name, "Name") + rtest.Equals(t, want.ModTime(), got.ModTime, "ModTime") + rtest.Equals(t, want.Mode(), got.Mode, "Mode") + rtest.Equals(t, want.Size(), got.Size, "Size") } func TestFSLocalRead(t *testing.T) { @@ -206,7 +205,7 @@ func TestFSLocalTypeChange(t *testing.T) { fi, err := f.Stat() rtest.OK(t, err) - if !fi.IsDir() { + if !fi.Mode.IsDir() { // a file handle based implementation should still reference the file checkMetadata(t, f, pathNew, false, restic.NodeTypeFile) 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_local_vss_test.go b/internal/fs/fs_local_vss_test.go index 33c412fe9..b64897d1c 100644 --- a/internal/fs/fs_local_vss_test.go +++ b/internal/fs/fs_local_vss_test.go @@ -325,7 +325,7 @@ func TestVSSFS(t *testing.T) { lstatFi, err := localVss.Lstat(tempfile) rtest.OK(t, err) - rtest.Equals(t, origFi.Mode(), lstatFi.Mode()) + rtest.Equals(t, origFi.Mode, lstatFi.Mode) f, err := localVss.OpenFile(tempfile, os.O_RDONLY, false) rtest.OK(t, err) @@ -335,7 +335,7 @@ func TestVSSFS(t *testing.T) { node, err := f.ToNode(false) rtest.OK(t, err) - rtest.Equals(t, node.Mode, lstatFi.Mode()) + rtest.Equals(t, node.Mode, lstatFi.Mode) rtest.OK(t, f.Close()) } diff --git a/internal/fs/fs_reader.go b/internal/fs/fs_reader.go index 8728b274c..bbe5c95ab 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,12 @@ 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{ + Name: fs.Name, + Mode: fs.Mode, + ModTime: fs.ModTime, + Size: fs.Size, } } @@ -68,7 +69,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 +81,14 @@ 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 { - fi := fakeFileInfo{ - name: fs.Base(name), - size: 0, - mode: os.ModeDir | 0755, - modtime: time.Now(), +func (fs *Reader) Lstat(name string) (*ExtendedFileInfo, error) { + getDirInfo := func(name string) *ExtendedFileInfo { + return &ExtendedFileInfo{ + Name: fs.Base(name), + Size: 0, + Mode: os.ModeDir | 0755, + ModTime: time.Now(), } - return fi } switch name { @@ -112,16 +112,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 +155,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 +203,7 @@ var _ File = &readerFile{} // except Stat() type fakeFile struct { name string - os.FileInfo + fi *ExtendedFileInfo } // ensure that fakeFile implements File @@ -235,12 +225,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) // fill minimal info with current values for uid, gid node.UID = uint32(os.Getuid()) @@ -252,7 +242,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,44 +250,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 -} - -// fakeFileInfo implements the bare minimum of os.FileInfo. -type fakeFileInfo struct { - name string - size int64 - mode os.FileMode - modtime time.Time -} - -func (fi fakeFileInfo) Name() string { - return fi.name -} - -func (fi fakeFileInfo) Size() int64 { - return fi.size -} - -func (fi fakeFileInfo) Mode() os.FileMode { - return fi.mode -} - -func (fi fakeFileInfo) ModTime() time.Time { - return fi.modtime -} - -func (fi fakeFileInfo) IsDir() bool { - return fi.mode&os.ModeDir > 0 -} - -func (fi fakeFileInfo) Sys() interface{} { - return nil + return slices.Clone(d.entries), nil } func pathError(op, name string, err error) *os.PathError { diff --git a/internal/fs/fs_reader_test.go b/internal/fs/fs_reader_test.go index 7e7f6e77c..257bfbbac 100644 --- a/internal/fs/fs_reader_test.go +++ b/internal/fs/fs_reader_test.go @@ -60,25 +60,25 @@ 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) { - if fi.IsDir() != isdir { - t.Errorf("IsDir returned %t, want %t", fi.IsDir(), isdir) +func checkFileInfo(t testing.TB, fi *ExtendedFileInfo, filename string, modtime time.Time, mode os.FileMode, isdir bool) { + if fi.Mode.IsDir() != isdir { + t.Errorf("IsDir returned %t, want %t", fi.Mode.IsDir(), isdir) } - if fi.Mode() != mode { - t.Errorf("Mode() returned wrong value, want 0%o, got 0%o", mode, fi.Mode()) + if fi.Mode != mode { + t.Errorf("Mode has 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.ModTime.Equal(modtime) { + t.Errorf("ModTime has wrong value, want %v, got %v", modtime, fi.ModTime) } - if path.Base(fi.Name()) != fi.Name() { - t.Errorf("Name() returned is not base, want %q, got %q", path.Base(fi.Name()), fi.Name()) + if path.Base(fi.Name) != fi.Name { + t.Errorf("Name is not base, want %q, got %q", path.Base(fi.Name), fi.Name) } - if fi.Name() != path.Base(filename) { - t.Errorf("Name() returned wrong value, want %q, got %q", path.Base(filename), fi.Name()) + if fi.Name != path.Base(filename) { + t.Errorf("Name has wrong value, want %q, got %q", path.Base(filename), 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..058d9cc7b 100644 --- a/internal/fs/node.go +++ b/internal/fs/node.go @@ -15,37 +15,36 @@ 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) { +func nodeFromFileInfo(path string, fi *ExtendedFileInfo, ignoreXattrListError bool) (*restic.Node, error) { node := buildBasicNode(path, fi) - 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 } -func buildBasicNode(path string, fi os.FileInfo) *restic.Node { +func buildBasicNode(path string, fi *ExtendedFileInfo) *restic.Node { mask := os.ModePerm | os.ModeType | os.ModeSetuid | os.ModeSetgid | os.ModeSticky node := &restic.Node{ Path: path, - Name: fi.Name(), - Mode: fi.Mode() & mask, - ModTime: fi.ModTime(), + Name: fi.Name, + Mode: fi.Mode & mask, + ModTime: fi.ModTime, } - node.Type = nodeTypeFromFileInfo(fi) + node.Type = nodeTypeFromFileInfo(fi.Mode) if node.Type == restic.NodeTypeFile { - node.Size = uint64(fi.Size()) + 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/node_windows.go b/internal/fs/node_windows.go index c0f8b08b0..74cf6c0e5 100644 --- a/internal/fs/node_windows.go +++ b/internal/fs/node_windows.go @@ -361,7 +361,7 @@ func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFil } } - winFI := stat.Sys().(*syscall.Win32FileAttributeData) + winFI := stat.sys.(*syscall.Win32FileAttributeData) // Add Windows attributes node.GenericAttributes, err = restic.WindowsAttrsToGenericAttributes(restic.WindowsAttributes{ diff --git a/internal/fs/stat.go b/internal/fs/stat.go index e1006fd61..bd3993f41 100644 --- a/internal/fs/stat.go +++ b/internal/fs/stat.go @@ -8,7 +8,8 @@ import ( // ExtendedFileInfo is an extended stat_t, filled with attributes that are // supported by most operating systems. The original FileInfo is embedded. type ExtendedFileInfo struct { - os.FileInfo + Name string + Mode os.FileMode DeviceID uint64 // ID of device containing the file Inode uint64 // Inode number @@ -23,10 +24,13 @@ type ExtendedFileInfo struct { AccessTime time.Time // last access time stamp ModTime time.Time // last (content) modification time stamp ChangeTime time.Time // last status change time stamp + + // nolint:unused // only used on Windows + sys any // Value returned by os.FileInfo.Sys() } // 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..165064153 100644 --- a/internal/fs/stat_bsd.go +++ b/internal/fs/stat_bsd.go @@ -10,11 +10,13 @@ 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{ - FileInfo: fi, + return &ExtendedFileInfo{ + Name: fi.Name(), + Mode: fi.Mode(), + DeviceID: uint64(s.Dev), Inode: uint64(s.Ino), Links: uint64(s.Nlink), @@ -29,6 +31,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..723ac8b19 100644 --- a/internal/fs/stat_unix.go +++ b/internal/fs/stat_unix.go @@ -10,11 +10,13 @@ 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{ - FileInfo: fi, + return &ExtendedFileInfo{ + Name: fi.Name(), + Mode: fi.Mode(), + DeviceID: uint64(s.Dev), Inode: s.Ino, Links: uint64(s.Nlink), @@ -29,6 +31,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..a2dfa5f6d 100644 --- a/internal/fs/stat_windows.go +++ b/internal/fs/stat_windows.go @@ -11,15 +11,18 @@ 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())) } extFI := ExtendedFileInfo{ - FileInfo: fi, - Size: int64(s.FileSizeLow) | (int64(s.FileSizeHigh) << 32), + Name: fi.Name(), + Mode: fi.Mode(), + + Size: int64(s.FileSizeLow) | (int64(s.FileSizeHigh) << 32), + sys: fi.Sys(), } atime := syscall.NsecToTimespec(s.LastAccessTime.Nanoseconds()) @@ -31,5 +34,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 }