forked from TrueCloudLab/restic
Merge pull request #5146 from MichaelEischer/inline-extended-stat
fs: Inline ExtendedFileInfo
This commit is contained in:
commit
9a674ecc34
24 changed files with 170 additions and 351 deletions
|
@ -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)
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
||||||
|
@ -505,12 +505,12 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case fi.Mode().IsRegular():
|
case fi.Mode.IsRegular():
|
||||||
debug.Log(" %v regular file", target)
|
debug.Log(" %v regular file", target)
|
||||||
|
|
||||||
// check if the file has not changed before performing a fopen operation (more expensive, specially
|
// check if the file has not changed before performing a fopen operation (more expensive, specially
|
||||||
// in network filesystems)
|
// 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) {
|
if arch.allBlobsPresent(previous) {
|
||||||
debug.Log("%v hasn't changed, using old list of blobs", target)
|
debug.Log("%v hasn't changed, using old list of blobs", target)
|
||||||
arch.trackItem(snPath, previous, previous, ItemStats{}, time.Since(start))
|
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
|
// 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)
|
err = errors.Errorf("file %q changed type, refusing to archive", target)
|
||||||
return filterError(err)
|
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))
|
arch.trackItem(snPath, previous, node, stats, time.Since(start))
|
||||||
})
|
})
|
||||||
|
|
||||||
case fi.IsDir():
|
case fi.Mode.IsDir():
|
||||||
debug.Log(" %v dir", target)
|
debug.Log(" %v dir", target)
|
||||||
|
|
||||||
snItem := snPath + "/"
|
snItem := snPath + "/"
|
||||||
|
@ -592,7 +592,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
|
||||||
return futureNode{}, false, err
|
return futureNode{}, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
case fi.Mode()&os.ModeSocket > 0:
|
case fi.Mode&os.ModeSocket > 0:
|
||||||
debug.Log(" %v is a socket, ignoring", target)
|
debug.Log(" %v is a socket, ignoring", target)
|
||||||
return futureNode{}, true, nil
|
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
|
// 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(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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -683,10 +683,11 @@ func TestFileChanged(t *testing.T) {
|
||||||
save(t, filename, content)
|
save(t, filename, content)
|
||||||
|
|
||||||
fs := &fs.Local{}
|
fs := &fs.Local{}
|
||||||
fiBefore := lstat(t, filename)
|
fiBefore, err := fs.Lstat(filename)
|
||||||
|
rtest.OK(t, err)
|
||||||
node := nodeFromFile(t, fs, filename)
|
node := nodeFromFile(t, fs, filename)
|
||||||
|
|
||||||
if fileChanged(fs, fiBefore, node, 0) {
|
if fileChanged(fiBefore, node, 0) {
|
||||||
t.Fatalf("unchanged file detected as changed")
|
t.Fatalf("unchanged file detected as changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -696,12 +697,12 @@ func TestFileChanged(t *testing.T) {
|
||||||
|
|
||||||
if test.SameFile {
|
if test.SameFile {
|
||||||
// file should be detected as unchanged
|
// 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")
|
t.Fatalf("unmodified file detected as changed")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// file should be detected as changed
|
// 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")
|
t.Fatalf("modified file detected as unchanged")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -718,7 +719,7 @@ func TestFilChangedSpecialCases(t *testing.T) {
|
||||||
|
|
||||||
t.Run("nil-node", func(t *testing.T) {
|
t.Run("nil-node", func(t *testing.T) {
|
||||||
fi := lstat(t, filename)
|
fi := lstat(t, filename)
|
||||||
if !fileChanged(&fs.Local{}, fi, nil, 0) {
|
if !fileChanged(fi, nil, 0) {
|
||||||
t.Fatal("nil node detected as unchanged")
|
t.Fatal("nil node detected as unchanged")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -727,7 +728,7 @@ func TestFilChangedSpecialCases(t *testing.T) {
|
||||||
fi := lstat(t, filename)
|
fi := lstat(t, filename)
|
||||||
node := nodeFromFile(t, &fs.Local{}, filename)
|
node := nodeFromFile(t, &fs.Local{}, filename)
|
||||||
node.Type = restic.NodeTypeSymlink
|
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")
|
t.Fatal("node with changed type detected as unchanged")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1520,7 +1521,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 +1538,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 +1565,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 +1589,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 +1598,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 +2203,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 +2226,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()
|
||||||
}
|
}
|
||||||
|
@ -2302,19 +2303,26 @@ func TestMetadataChanged(t *testing.T) {
|
||||||
t.Fatalf("metadata does not match:\n%v", cmp.Diff(want, node2))
|
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
|
// modify the mode and UID/GID
|
||||||
fs.overrideFI = wrapFileInfo(fi)
|
modFI := *fi
|
||||||
rtest.Assert(t, !fileChanged(fs, fs.overrideFI, node2, 0), "testfile must not be considered as changed")
|
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
|
// set the override values in the 'want' node which
|
||||||
want.Mode = 0400
|
want.Mode = mockFileInfoMode
|
||||||
// ignore UID and GID on Windows
|
// ignore UID and GID on Windows
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
want.UID = 51234
|
want.UID = mockFileInfoUID
|
||||||
want.GID = 51235
|
want.GID = mockFileInfoGID
|
||||||
}
|
}
|
||||||
// update mock node accordingly
|
// update mock node accordingly
|
||||||
fs.overrideNode.Mode = 0400
|
fs.overrideNode.Mode = want.Mode
|
||||||
fs.overrideNode.UID = want.UID
|
fs.overrideNode.UID = want.UID
|
||||||
fs.overrideNode.GID = want.GID
|
fs.overrideNode.GID = want.GID
|
||||||
|
|
||||||
|
@ -2455,10 +2463,12 @@ func TestIrregularFile(t *testing.T) {
|
||||||
|
|
||||||
tempfile := filepath.Join(tempdir, "testfile")
|
tempfile := filepath.Join(tempdir, "testfile")
|
||||||
fi := lstat(t, "testfile")
|
fi := lstat(t, "testfile")
|
||||||
|
// patch mode to irregular
|
||||||
|
fi.Mode = (fi.Mode &^ os.ModeType) | os.ModeIrregular
|
||||||
|
|
||||||
override := &overrideFS{
|
override := &overrideFS{
|
||||||
FS: fs.Local{},
|
FS: fs.Local{},
|
||||||
overrideFI: wrapIrregularFileInfo(fi),
|
overrideFI: fi,
|
||||||
overrideNode: &restic.Node{
|
overrideNode: &restic.Node{
|
||||||
Type: restic.NodeTypeIrregular,
|
Type: restic.NodeTypeIrregular,
|
||||||
},
|
},
|
||||||
|
@ -2497,7 +2507,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
package archiver
|
package archiver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/feature"
|
"github.com/restic/restic/internal/feature"
|
||||||
|
@ -14,48 +12,6 @@ import (
|
||||||
rtest "github.com/restic/restic/internal/test"
|
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) {
|
func statAndSnapshot(t *testing.T, repo archiverRepo, name string) (*restic.Node, *restic.Node) {
|
||||||
want := nodeFromFile(t, &fs.Local{}, name)
|
want := nodeFromFile(t, &fs.Local{}, name)
|
||||||
_, node := snapshot(t, repo, &fs.Local{}, nil, name)
|
_, node := snapshot(t, repo, &fs.Local{}, nil, name)
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
||||||
|
@ -274,7 +267,7 @@ func RejectByDevice(samples []string, filesystem fs.FS) (RejectFunc, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// reject everything except directories
|
// reject everything except directories
|
||||||
if !fi.IsDir() {
|
if !fi.Mode.IsDir() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.Mode.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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {},
|
||||||
}
|
}
|
||||||
|
@ -119,10 +118,10 @@ 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 {
|
||||||
return stats, s.Error(target, err)
|
return stats, s.Error(target, err)
|
||||||
|
|
|
@ -56,8 +56,8 @@ 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.Mode.IsDir() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -90,17 +90,16 @@ func checkMetadata(t *testing.T, f File, path string, follow bool, nodeType rest
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertFIEqual(t *testing.T, want os.FileInfo, got os.FileInfo) {
|
func assertFIEqual(t *testing.T, want os.FileInfo, got *ExtendedFileInfo) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
rtest.Equals(t, want.Name(), got.Name(), "Name")
|
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.ModTime(), got.ModTime(), "ModTime")
|
rtest.Equals(t, want.Mode(), got.Mode, "Mode")
|
||||||
rtest.Equals(t, want.Mode(), got.Mode(), "Mode")
|
rtest.Equals(t, want.Size(), got.Size, "Size")
|
||||||
rtest.Equals(t, want.Size(), got.Size(), "Size")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFSLocalRead(t *testing.T) {
|
func TestFSLocalRead(t *testing.T) {
|
||||||
|
@ -206,7 +205,7 @@ func TestFSLocalTypeChange(t *testing.T) {
|
||||||
|
|
||||||
fi, err := f.Stat()
|
fi, err := f.Stat()
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
if !fi.IsDir() {
|
if !fi.Mode.IsDir() {
|
||||||
// a file handle based implementation should still reference the file
|
// a file handle based implementation should still reference the file
|
||||||
checkMetadata(t, f, pathNew, false, restic.NodeTypeFile)
|
checkMetadata(t, f, pathNew, false, restic.NodeTypeFile)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -325,7 +325,7 @@ func TestVSSFS(t *testing.T) {
|
||||||
|
|
||||||
lstatFi, err := localVss.Lstat(tempfile)
|
lstatFi, err := localVss.Lstat(tempfile)
|
||||||
rtest.OK(t, err)
|
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)
|
f, err := localVss.OpenFile(tempfile, os.O_RDONLY, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
@ -335,7 +335,7 @@ func TestVSSFS(t *testing.T) {
|
||||||
|
|
||||||
node, err := f.ToNode(false)
|
node, err := f.ToNode(false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
rtest.Equals(t, node.Mode, lstatFi.Mode())
|
rtest.Equals(t, node.Mode, lstatFi.Mode)
|
||||||
|
|
||||||
rtest.OK(t, f.Close())
|
rtest.OK(t, f.Close())
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
@ -40,12 +41,12 @@ 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,
|
Name: fs.Name,
|
||||||
size: fs.Size,
|
Mode: fs.Mode,
|
||||||
mode: fs.Mode,
|
ModTime: fs.ModTime,
|
||||||
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
|
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 +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
|
// 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{
|
return &ExtendedFileInfo{
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch name {
|
switch name {
|
||||||
|
@ -112,16 +112,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 +155,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 +203,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 +225,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)
|
||||||
|
|
||||||
// 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 +242,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,44 +250,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.
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathError(op, name string, err error) *os.PathError {
|
func pathError(op, name string, err error) *os.PathError {
|
||||||
|
|
|
@ -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) {
|
func checkFileInfo(t testing.TB, fi *ExtendedFileInfo, filename string, modtime time.Time, mode os.FileMode, isdir bool) {
|
||||||
if fi.IsDir() != isdir {
|
if fi.Mode.IsDir() != isdir {
|
||||||
t.Errorf("IsDir returned %t, want %t", fi.IsDir(), isdir)
|
t.Errorf("IsDir returned %t, want %t", fi.Mode.IsDir(), isdir)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fi.Mode() != mode {
|
if fi.Mode != mode {
|
||||||
t.Errorf("Mode() returned wrong value, want 0%o, got 0%o", mode, fi.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) {
|
if !modtime.Equal(time.Time{}) && !fi.ModTime.Equal(modtime) {
|
||||||
t.Errorf("ModTime() returned wrong value, want %v, got %v", modtime, fi.ModTime())
|
t.Errorf("ModTime has wrong value, want %v, got %v", modtime, fi.ModTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
if path.Base(fi.Name()) != fi.Name() {
|
if path.Base(fi.Name) != fi.Name {
|
||||||
t.Errorf("Name() returned is not base, want %q, got %q", 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) {
|
if fi.Name != path.Base(filename) {
|
||||||
t.Errorf("Name() returned wrong value, want %q, got %q", path.Base(filename), fi.Name())
|
t.Errorf("Name has wrong value, want %q, got %q", path.Base(filename), fi.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -15,37 +15,36 @@ 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)
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
mask := os.ModePerm | os.ModeType | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
|
||||||
node := &restic.Node{
|
node := &restic.Node{
|
||||||
Path: path,
|
Path: path,
|
||||||
Name: fi.Name(),
|
Name: fi.Name,
|
||||||
Mode: fi.Mode() & mask,
|
Mode: fi.Mode & mask,
|
||||||
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:
|
||||||
|
|
|
@ -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
|
// Add Windows attributes
|
||||||
node.GenericAttributes, err = restic.WindowsAttrsToGenericAttributes(restic.WindowsAttributes{
|
node.GenericAttributes, err = restic.WindowsAttrsToGenericAttributes(restic.WindowsAttributes{
|
||||||
|
|
|
@ -8,7 +8,8 @@ import (
|
||||||
// ExtendedFileInfo is an extended stat_t, filled with attributes that are
|
// ExtendedFileInfo is an extended stat_t, filled with attributes that are
|
||||||
// supported by most operating systems. The original FileInfo is embedded.
|
// supported by most operating systems. The original FileInfo is embedded.
|
||||||
type ExtendedFileInfo struct {
|
type ExtendedFileInfo struct {
|
||||||
os.FileInfo
|
Name string
|
||||||
|
Mode os.FileMode
|
||||||
|
|
||||||
DeviceID uint64 // ID of device containing the file
|
DeviceID uint64 // ID of device containing the file
|
||||||
Inode uint64 // Inode number
|
Inode uint64 // Inode number
|
||||||
|
@ -23,10 +24,13 @@ type ExtendedFileInfo struct {
|
||||||
AccessTime time.Time // last access time stamp
|
AccessTime time.Time // last access time stamp
|
||||||
ModTime time.Time // last (content) modification time stamp
|
ModTime time.Time // last (content) modification time stamp
|
||||||
ChangeTime time.Time // last status change 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.
|
// 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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,13 @@ 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,
|
Name: fi.Name(),
|
||||||
|
Mode: fi.Mode(),
|
||||||
|
|
||||||
DeviceID: uint64(s.Dev),
|
DeviceID: uint64(s.Dev),
|
||||||
Inode: uint64(s.Ino),
|
Inode: uint64(s.Ino),
|
||||||
Links: uint64(s.Nlink),
|
Links: uint64(s.Nlink),
|
||||||
|
@ -29,6 +31,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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,13 @@ 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,
|
Name: fi.Name(),
|
||||||
|
Mode: fi.Mode(),
|
||||||
|
|
||||||
DeviceID: uint64(s.Dev),
|
DeviceID: uint64(s.Dev),
|
||||||
Inode: s.Ino,
|
Inode: s.Ino,
|
||||||
Links: uint64(s.Nlink),
|
Links: uint64(s.Nlink),
|
||||||
|
@ -29,6 +31,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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,15 +11,18 @@ 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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
extFI := ExtendedFileInfo{
|
extFI := ExtendedFileInfo{
|
||||||
FileInfo: fi,
|
Name: fi.Name(),
|
||||||
Size: int64(s.FileSizeLow) | (int64(s.FileSizeHigh) << 32),
|
Mode: fi.Mode(),
|
||||||
|
|
||||||
|
Size: int64(s.FileSizeLow) | (int64(s.FileSizeHigh) << 32),
|
||||||
|
sys: fi.Sys(),
|
||||||
}
|
}
|
||||||
|
|
||||||
atime := syscall.NsecToTimespec(s.LastAccessTime.Nanoseconds())
|
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.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue