Merge pull request #5143 from MichaelEischer/fs-handle-interface

fs: rework FS interface to be handle based
This commit is contained in:
Michael Eischer 2024-11-30 15:29:31 +01:00 committed by GitHub
commit 8642049532
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 817 additions and 485 deletions

View file

@ -66,6 +66,11 @@ func (s *ItemStats) Add(other ItemStats) {
s.TreeSizeInRepo += other.TreeSizeInRepo s.TreeSizeInRepo += other.TreeSizeInRepo
} }
// ToNoder returns a restic.Node for a File.
type ToNoder interface {
ToNode(ignoreXattrListError bool) (*restic.Node, error)
}
type archiverRepo interface { type archiverRepo interface {
restic.Loader restic.Loader
restic.BlobSaver restic.BlobSaver
@ -257,8 +262,8 @@ func (arch *Archiver) trackItem(item string, previous, current *restic.Node, s I
} }
// nodeFromFileInfo returns the restic node from an os.FileInfo. // nodeFromFileInfo returns the restic node from an os.FileInfo.
func (arch *Archiver) nodeFromFileInfo(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) { func (arch *Archiver) nodeFromFileInfo(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*restic.Node, error) {
node, err := arch.FS.NodeFromFileInfo(filename, fi, ignoreXattrListError) node, err := meta.ToNode(ignoreXattrListError)
if !arch.WithAtime { if !arch.WithAtime {
node.AccessTime = node.ModTime node.AccessTime = node.ModTime
} }
@ -308,20 +313,14 @@ func (arch *Archiver) wrapLoadTreeError(id restic.ID, err error) error {
// saveDir stores a directory in the repo and returns the node. snPath is the // saveDir stores a directory in the repo and returns the node. snPath is the
// path within the current snapshot. // path within the current snapshot.
func (arch *Archiver) saveDir(ctx context.Context, snPath string, dir string, fi os.FileInfo, previous *restic.Tree, complete fileCompleteFunc) (d futureNode, err error) { func (arch *Archiver) saveDir(ctx context.Context, snPath string, dir string, meta fs.File, previous *restic.Tree, complete fileCompleteFunc) (d futureNode, err error) {
debug.Log("%v %v", snPath, dir) debug.Log("%v %v", snPath, dir)
treeNode, err := arch.nodeFromFileInfo(snPath, dir, fi, false) treeNode, names, err := arch.dirToNodeAndEntries(snPath, dir, meta)
if err != nil { if err != nil {
return futureNode{}, err return futureNode{}, err
} }
names, err := fs.Readdirnames(arch.FS, dir, fs.O_NOFOLLOW)
if err != nil {
return futureNode{}, err
}
sort.Strings(names)
nodes := make([]futureNode, 0, len(names)) nodes := make([]futureNode, 0, len(names))
for _, name := range names { for _, name := range names {
@ -359,6 +358,29 @@ func (arch *Archiver) saveDir(ctx context.Context, snPath string, dir string, fi
return fn, nil return fn, nil
} }
func (arch *Archiver) dirToNodeAndEntries(snPath, dir string, meta fs.File) (node *restic.Node, names []string, err error) {
err = meta.MakeReadable()
if err != nil {
return nil, nil, fmt.Errorf("openfile for readdirnames failed: %w", err)
}
node, err = arch.nodeFromFileInfo(snPath, dir, meta, false)
if err != nil {
return nil, nil, err
}
if node.Type != restic.NodeTypeDir {
return nil, nil, fmt.Errorf("directory %q changed type, refusing to archive", snPath)
}
names, err = meta.Readdirnames(-1)
if err != nil {
return nil, nil, fmt.Errorf("readdirnames %v failed: %w", dir, err)
}
sort.Strings(names)
return node, names, nil
}
// futureNode holds a reference to a channel that returns a FutureNodeResult // futureNode holds a reference to a channel that returns a FutureNodeResult
// or a reference to an already existing result. If the result is available // or a reference to an already existing result. If the result is available
// immediately, then storing a reference directly requires less memory than // immediately, then storing a reference directly requires less memory than
@ -435,21 +457,39 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
return futureNode{}, false, err return futureNode{}, false, err
} }
filterError := func(err error) (futureNode, bool, error) {
err = arch.error(abstarget, err)
if err != nil {
return futureNode{}, false, errors.WithStack(err)
}
return futureNode{}, true, nil
}
// exclude files by path before running Lstat to reduce number of lstat calls // exclude files by path before running Lstat to reduce number of lstat calls
if !arch.SelectByName(abstarget) { if !arch.SelectByName(abstarget) {
debug.Log("%v is excluded by path", target) debug.Log("%v is excluded by path", target)
return futureNode{}, true, nil return futureNode{}, true, nil
} }
meta, err := arch.FS.OpenFile(target, fs.O_NOFOLLOW, true)
if err != nil {
debug.Log("open metadata for %v returned error: %v", target, err)
return filterError(err)
}
closeFile := true
defer func() {
if closeFile {
cerr := meta.Close()
if err == nil {
err = cerr
}
}
}()
// get file info and run remaining select functions that require file information // get file info and run remaining select functions that require file information
fi, err := arch.FS.Lstat(target) fi, err := meta.Stat()
if err != nil { if err != nil {
debug.Log("lstat() for %v returned error: %v", target, err) debug.Log("lstat() for %v returned error: %v", target, err)
err = arch.error(abstarget, err) return filterError(err)
if err != nil {
return futureNode{}, false, errors.WithStack(err)
}
return futureNode{}, true, nil
} }
if !arch.Select(abstarget, fi, arch.FS) { if !arch.Select(abstarget, fi, arch.FS) {
debug.Log("%v is excluded", target) debug.Log("%v is excluded", target)
@ -467,7 +507,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, 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))
arch.CompleteBlob(previous.Size) arch.CompleteBlob(previous.Size)
node, err := arch.nodeFromFileInfo(snPath, target, fi, false) node, err := arch.nodeFromFileInfo(snPath, target, meta, false)
if err != nil { if err != nil {
return futureNode{}, false, err return futureNode{}, false, err
} }
@ -494,40 +534,28 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
// reopen file and do an fstat() on the open file to check it is still // reopen file and do an fstat() on the open file to check it is still
// a file (and has not been exchanged for e.g. a symlink) // a file (and has not been exchanged for e.g. a symlink)
file, err := arch.FS.OpenFile(target, fs.O_RDONLY|fs.O_NOFOLLOW, 0) err := meta.MakeReadable()
if err != nil { if err != nil {
debug.Log("Openfile() for %v returned error: %v", target, err) debug.Log("MakeReadable() for %v returned error: %v", target, err)
err = arch.error(abstarget, err) return filterError(err)
if err != nil {
return futureNode{}, false, errors.WithStack(err)
}
return futureNode{}, true, nil
} }
fi, err = file.Stat() fi, err := meta.Stat()
if err != nil { if err != nil {
debug.Log("stat() on opened file %v returned error: %v", target, err) debug.Log("stat() on opened file %v returned error: %v", target, err)
_ = file.Close() return filterError(err)
err = arch.error(abstarget, err)
if err != nil {
return futureNode{}, false, errors.WithStack(err)
}
return futureNode{}, true, nil
} }
// 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 %v changed type, refusing to archive", fi.Name()) err = errors.Errorf("file %q changed type, refusing to archive", target)
_ = file.Close() return filterError(err)
err = arch.error(abstarget, err)
if err != nil {
return futureNode{}, false, err
}
return futureNode{}, true, nil
} }
closeFile = false
// Save will close the file, we don't need to do that // Save will close the file, we don't need to do that
fn = arch.fileSaver.Save(ctx, snPath, target, file, fi, func() { fn = arch.fileSaver.Save(ctx, snPath, target, meta, func() {
arch.StartFile(snPath) arch.StartFile(snPath)
}, func() { }, func() {
arch.trackItem(snPath, nil, nil, ItemStats{}, 0) arch.trackItem(snPath, nil, nil, ItemStats{}, 0)
@ -547,7 +575,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
return futureNode{}, false, err return futureNode{}, false, err
} }
fn, err = arch.saveDir(ctx, snPath, target, fi, oldSubtree, fn, err = arch.saveDir(ctx, snPath, target, meta, oldSubtree,
func(node *restic.Node, stats ItemStats) { func(node *restic.Node, stats ItemStats) {
arch.trackItem(snItem, previous, node, stats, time.Since(start)) arch.trackItem(snItem, previous, node, stats, time.Since(start))
}) })
@ -563,7 +591,7 @@ func (arch *Archiver) save(ctx context.Context, snPath, target string, previous
default: default:
debug.Log(" %v other", target) debug.Log(" %v other", target)
node, err := arch.nodeFromFileInfo(snPath, target, fi, false) node, err := arch.nodeFromFileInfo(snPath, target, meta, false)
if err != nil { if err != nil {
return futureNode{}, false, err return futureNode{}, false, err
} }
@ -614,22 +642,6 @@ func join(elem ...string) string {
return path.Join(elem...) return path.Join(elem...)
} }
// statDir returns the file info for the directory. Symbolic links are
// resolved. If the target directory is not a directory, an error is returned.
func (arch *Archiver) statDir(dir string) (os.FileInfo, error) {
fi, err := arch.FS.Stat(dir)
if err != nil {
return nil, errors.WithStack(err)
}
tpe := fi.Mode() & (os.ModeType | os.ModeCharDevice)
if tpe != os.ModeDir {
return fi, errors.Errorf("path is not a directory: %v", dir)
}
return fi, nil
}
// saveTree stores a Tree in the repo, returned is the tree. snPath is the path // saveTree stores a Tree in the repo, returned is the tree. snPath is the path
// within the current snapshot. // within the current snapshot.
func (arch *Archiver) saveTree(ctx context.Context, snPath string, atree *tree, previous *restic.Tree, complete fileCompleteFunc) (futureNode, int, error) { func (arch *Archiver) saveTree(ctx context.Context, snPath string, atree *tree, previous *restic.Tree, complete fileCompleteFunc) (futureNode, int, error) {
@ -640,15 +652,8 @@ func (arch *Archiver) saveTree(ctx context.Context, snPath string, atree *tree,
return futureNode{}, 0, errors.Errorf("FileInfoPath for %v is empty", snPath) return futureNode{}, 0, errors.Errorf("FileInfoPath for %v is empty", snPath)
} }
fi, err := arch.statDir(atree.FileInfoPath) var err error
if err != nil { node, err = arch.dirPathToNode(snPath, atree.FileInfoPath)
return futureNode{}, 0, err
}
debug.Log("%v, dir node data loaded from %v", snPath, atree.FileInfoPath)
// in some cases reading xattrs for directories above the backup source is not allowed
// thus ignore errors for such folders.
node, err = arch.nodeFromFileInfo(snPath, atree.FileInfoPath, fi, true)
if err != nil { if err != nil {
return futureNode{}, 0, err return futureNode{}, 0, err
} }
@ -719,6 +724,31 @@ func (arch *Archiver) saveTree(ctx context.Context, snPath string, atree *tree,
return fn, len(nodes), nil return fn, len(nodes), nil
} }
func (arch *Archiver) dirPathToNode(snPath, target string) (node *restic.Node, err error) {
meta, err := arch.FS.OpenFile(target, 0, true)
if err != nil {
return nil, err
}
defer func() {
cerr := meta.Close()
if err == nil {
err = cerr
}
}()
debug.Log("%v, reading dir node data from %v", snPath, target)
// in some cases reading xattrs for directories above the backup source is not allowed
// thus ignore errors for such folders.
node, err = arch.nodeFromFileInfo(snPath, target, meta, true)
if err != nil {
return nil, err
}
if node.Type != restic.NodeTypeDir {
return nil, errors.Errorf("path is not a directory: %v", target)
}
return node, err
}
// resolveRelativeTargets replaces targets that only contain relative // resolveRelativeTargets replaces targets that only contain relative
// directories ("." or "../../") with the contents of the directory. Each // directories ("." or "../../") with the contents of the directory. Each
// element of target is processed with fs.Clean(). // element of target is processed with fs.Clean().

View file

@ -76,17 +76,12 @@ func saveFile(t testing.TB, repo archiverRepo, filename string, filesystem fs.FS
startCallback = true startCallback = true
} }
file, err := arch.FS.OpenFile(filename, fs.O_RDONLY|fs.O_NOFOLLOW, 0) file, err := arch.FS.OpenFile(filename, fs.O_NOFOLLOW, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fi, err := file.Stat() res := arch.fileSaver.Save(ctx, "/", filename, file, start, completeReading, complete)
if err != nil {
t.Fatal(err)
}
res := arch.fileSaver.Save(ctx, "/", filename, file, fi, start, completeReading, complete)
fnr := res.take(ctx) fnr := res.take(ctx)
if fnr.err != nil { if fnr.err != nil {
@ -556,11 +551,12 @@ func rename(t testing.TB, oldname, newname string) {
} }
} }
func nodeFromFI(t testing.TB, fs fs.FS, filename string, fi os.FileInfo) *restic.Node { func nodeFromFile(t testing.TB, localFs fs.FS, filename string) *restic.Node {
node, err := fs.NodeFromFileInfo(filename, fi, false) meta, err := localFs.OpenFile(filename, fs.O_NOFOLLOW, true)
if err != nil { rtest.OK(t, err)
t.Fatal(err) node, err := meta.ToNode(false)
} rtest.OK(t, err)
rtest.OK(t, meta.Close())
return node return node
} }
@ -688,7 +684,7 @@ func TestFileChanged(t *testing.T) {
fs := &fs.Local{} fs := &fs.Local{}
fiBefore := lstat(t, filename) fiBefore := lstat(t, filename)
node := nodeFromFI(t, fs, filename, fiBefore) node := nodeFromFile(t, fs, filename)
if fileChanged(fs, fiBefore, node, 0) { if fileChanged(fs, fiBefore, node, 0) {
t.Fatalf("unchanged file detected as changed") t.Fatalf("unchanged file detected as changed")
@ -729,8 +725,8 @@ func TestFilChangedSpecialCases(t *testing.T) {
t.Run("type-change", func(t *testing.T) { t.Run("type-change", func(t *testing.T) {
fi := lstat(t, filename) fi := lstat(t, filename)
node := nodeFromFI(t, &fs.Local{}, filename, fi) node := nodeFromFile(t, &fs.Local{}, filename)
node.Type = "restic.NodeTypeSymlink" node.Type = restic.NodeTypeSymlink
if !fileChanged(&fs.Local{}, fi, node, 0) { if !fileChanged(&fs.Local{}, fi, node, 0) {
t.Fatal("node with changed type detected as unchanged") t.Fatal("node with changed type detected as unchanged")
} }
@ -834,7 +830,8 @@ func TestArchiverSaveDir(t *testing.T) {
wg, ctx := errgroup.WithContext(context.Background()) wg, ctx := errgroup.WithContext(context.Background())
repo.StartPackUploader(ctx, wg) repo.StartPackUploader(ctx, wg)
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{}) testFS := fs.Track{FS: fs.Local{}}
arch := New(repo, testFS, Options{})
arch.runWorkers(ctx, wg) arch.runWorkers(ctx, wg)
arch.summary = &Summary{} arch.summary = &Summary{}
@ -846,15 +843,11 @@ func TestArchiverSaveDir(t *testing.T) {
back := rtest.Chdir(t, chdir) back := rtest.Chdir(t, chdir)
defer back() defer back()
fi, err := os.Lstat(test.target) meta, err := testFS.OpenFile(test.target, fs.O_NOFOLLOW, true)
if err != nil { rtest.OK(t, err)
t.Fatal(err) ft, err := arch.saveDir(ctx, "/", test.target, meta, nil, nil)
} rtest.OK(t, err)
rtest.OK(t, meta.Close())
ft, err := arch.saveDir(ctx, "/", test.target, fi, nil, nil)
if err != nil {
t.Fatal(err)
}
fnr := ft.take(ctx) fnr := ft.take(ctx)
node, stats := fnr.node, fnr.stats node, stats := fnr.node, fnr.stats
@ -916,19 +909,16 @@ func TestArchiverSaveDirIncremental(t *testing.T) {
wg, ctx := errgroup.WithContext(context.TODO()) wg, ctx := errgroup.WithContext(context.TODO())
repo.StartPackUploader(ctx, wg) repo.StartPackUploader(ctx, wg)
arch := New(repo, fs.Track{FS: fs.Local{}}, Options{}) testFS := fs.Track{FS: fs.Local{}}
arch := New(repo, testFS, Options{})
arch.runWorkers(ctx, wg) arch.runWorkers(ctx, wg)
arch.summary = &Summary{} arch.summary = &Summary{}
fi, err := os.Lstat(tempdir) meta, err := testFS.OpenFile(tempdir, fs.O_NOFOLLOW, true)
if err != nil { rtest.OK(t, err)
t.Fatal(err) ft, err := arch.saveDir(ctx, "/", tempdir, meta, nil, nil)
} rtest.OK(t, err)
rtest.OK(t, meta.Close())
ft, err := arch.saveDir(ctx, "/", tempdir, fi, nil, nil)
if err != nil {
t.Fatal(err)
}
fnr := ft.take(ctx) fnr := ft.take(ctx)
node, stats := fnr.node, fnr.stats node, stats := fnr.node, fnr.stats
@ -1665,8 +1655,8 @@ type MockFS struct {
bytesRead map[string]int // tracks bytes read from all opened files bytesRead map[string]int // tracks bytes read from all opened files
} }
func (m *MockFS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) { func (m *MockFS) OpenFile(name string, flag int, metadataOnly bool) (fs.File, error) {
f, err := m.FS.OpenFile(name, flag, perm) f, err := m.FS.OpenFile(name, flag, metadataOnly)
if err != nil { if err != nil {
return f, err return f, err
} }
@ -2056,12 +2046,12 @@ type TrackFS struct {
m sync.Mutex m sync.Mutex
} }
func (m *TrackFS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) { func (m *TrackFS) OpenFile(name string, flag int, metadataOnly bool) (fs.File, error) {
m.m.Lock() m.m.Lock()
m.opened[name]++ m.opened[name]++
m.m.Unlock() m.m.Unlock()
return m.FS.OpenFile(name, flag, perm) return m.FS.OpenFile(name, flag, metadataOnly)
} }
type failSaveRepo struct { type failSaveRepo struct {
@ -2210,48 +2200,51 @@ func snapshot(t testing.TB, repo archiverRepo, fs fs.FS, parent *restic.Snapshot
return snapshot, node return snapshot, node
} }
// StatFS allows overwriting what is returned by the Lstat function. type overrideFS struct {
type StatFS struct {
fs.FS fs.FS
overrideFI os.FileInfo
OverrideLstat map[string]os.FileInfo resetFIOnRead bool
OnlyOverrideStat bool overrideNode *restic.Node
overrideErr error
} }
func (fs *StatFS) Lstat(name string) (os.FileInfo, error) { func (m *overrideFS) OpenFile(name string, flag int, metadataOnly bool) (fs.File, error) {
if !fs.OnlyOverrideStat { f, err := m.FS.OpenFile(name, flag, metadataOnly)
if fi, ok := fs.OverrideLstat[fixpath(name)]; ok { if err != nil {
return fi, nil return f, err
}
} }
return fs.FS.Lstat(name) if filepath.Base(name) == "testfile" || filepath.Base(name) == "testdir" {
} return &overrideFile{f, m}, nil
func (fs *StatFS) OpenFile(name string, flags int, perm os.FileMode) (fs.File, error) {
if fi, ok := fs.OverrideLstat[fixpath(name)]; ok {
f, err := fs.FS.OpenFile(name, flags, perm)
if err != nil {
return nil, err
}
wrappedFile := fileStat{
File: f,
fi: fi,
}
return wrappedFile, nil
} }
return f, nil
return fs.FS.OpenFile(name, flags, perm)
} }
type fileStat struct { type overrideFile struct {
fs.File fs.File
fi os.FileInfo ofs *overrideFS
} }
func (f fileStat) Stat() (os.FileInfo, error) { func (f overrideFile) Stat() (os.FileInfo, error) {
return f.fi, nil if f.ofs.overrideFI == nil {
return f.File.Stat()
}
return f.ofs.overrideFI, nil
}
func (f overrideFile) MakeReadable() error {
if f.ofs.resetFIOnRead {
f.ofs.overrideFI = nil
}
return f.File.MakeReadable()
}
func (f overrideFile) ToNode(ignoreXattrListError bool) (*restic.Node, error) {
if f.ofs.overrideNode == nil {
return f.File.ToNode(ignoreXattrListError)
}
return f.ofs.overrideNode, f.ofs.overrideErr
} }
// used by wrapFileInfo, use untyped const in order to avoid having a version // used by wrapFileInfo, use untyped const in order to avoid having a version
@ -2279,17 +2272,18 @@ func TestMetadataChanged(t *testing.T) {
// get metadata // get metadata
fi := lstat(t, "testfile") fi := lstat(t, "testfile")
localFS := &fs.Local{} localFS := &fs.Local{}
want, err := localFS.NodeFromFileInfo("testfile", fi, false) meta, err := localFS.OpenFile("testfile", fs.O_NOFOLLOW, true)
if err != nil { rtest.OK(t, err)
t.Fatal(err) want, err := meta.ToNode(false)
} rtest.OK(t, err)
rtest.OK(t, meta.Close())
fs := &StatFS{ fs := &overrideFS{
FS: localFS, FS: localFS,
OverrideLstat: map[string]os.FileInfo{ overrideFI: fi,
"testfile": fi, overrideNode: &restic.Node{},
},
} }
*fs.overrideNode = *want
sn, node2 := snapshot(t, repo, fs, nil, "testfile") sn, node2 := snapshot(t, repo, fs, nil, "testfile")
@ -2309,7 +2303,8 @@ func TestMetadataChanged(t *testing.T) {
} }
// modify the mode by wrapping it in a new struct, uses the consts defined above // modify the mode by wrapping it in a new struct, uses the consts defined above
fs.OverrideLstat["testfile"] = wrapFileInfo(fi) fs.overrideFI = wrapFileInfo(fi)
rtest.Assert(t, !fileChanged(fs, 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 = 0400
@ -2318,16 +2313,13 @@ func TestMetadataChanged(t *testing.T) {
want.UID = 51234 want.UID = 51234
want.GID = 51235 want.GID = 51235
} }
// no user and group name // update mock node accordingly
want.User = "" fs.overrideNode.Mode = 0400
want.Group = "" fs.overrideNode.UID = want.UID
fs.overrideNode.GID = want.GID
// make another snapshot // make another snapshot
_, node3 := snapshot(t, repo, fs, sn, "testfile") _, node3 := snapshot(t, repo, fs, sn, "testfile")
// Override username and group to empty string - in case underlying system has user with UID 51234
// See https://github.com/restic/restic/issues/2372
node3.User = ""
node3.Group = ""
// make sure that metadata was recorded successfully // make sure that metadata was recorded successfully
if !cmp.Equal(want, node3) { if !cmp.Equal(want, node3) {
@ -2340,62 +2332,83 @@ func TestMetadataChanged(t *testing.T) {
checker.TestCheckRepo(t, repo, false) checker.TestCheckRepo(t, repo, false)
} }
func TestRacyFileSwap(t *testing.T) { func TestRacyFileTypeSwap(t *testing.T) {
files := TestDir{ files := TestDir{
"file": TestFile{ "testfile": TestFile{
Content: "foo bar test file", Content: "foo bar test file",
}, },
"testdir": TestDir{},
} }
tempdir, repo := prepareTempdirRepoSrc(t, files) for _, dirError := range []bool{false, true} {
desc := "file changed type"
if dirError {
desc = "dir changed type"
}
t.Run(desc, func(t *testing.T) {
tempdir, repo := prepareTempdirRepoSrc(t, files)
back := rtest.Chdir(t, tempdir) back := rtest.Chdir(t, tempdir)
defer back() defer back()
// get metadata of current folder // get metadata of current folder
fi := lstat(t, ".") var fakeName, realName string
tempfile := filepath.Join(tempdir, "file") if dirError {
// lstat claims this is a directory, but it's actually a file
fakeName = "testdir"
realName = "testfile"
} else {
fakeName = "testfile"
realName = "testdir"
}
fakeFI := lstat(t, fakeName)
tempfile := filepath.Join(tempdir, realName)
statfs := &StatFS{ statfs := &overrideFS{
FS: fs.Local{}, FS: fs.Local{},
OverrideLstat: map[string]os.FileInfo{ overrideFI: fakeFI,
tempfile: fi, resetFIOnRead: true,
}, }
OnlyOverrideStat: true,
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
wg, ctx := errgroup.WithContext(ctx)
repo.StartPackUploader(ctx, wg)
arch := New(repo, fs.Track{FS: statfs}, Options{})
arch.Error = func(item string, err error) error {
t.Logf("archiver error as expected for %v: %v", item, err)
return err
}
arch.runWorkers(ctx, wg)
// fs.Track will panic if the file was not closed
_, excluded, err := arch.save(ctx, "/", tempfile, nil)
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "changed type, refusing to archive"), "save() returned wrong error: %v", err)
tpe := "file"
if dirError {
tpe = "directory"
}
rtest.Assert(t, strings.Contains(err.Error(), tpe+" "), "unexpected item type in error: %v", err)
rtest.Assert(t, !excluded, "Save() excluded the node, that's unexpected")
})
} }
}
ctx, cancel := context.WithCancel(context.Background()) type mockToNoder struct {
defer cancel() node *restic.Node
err error
}
wg, ctx := errgroup.WithContext(ctx) func (m *mockToNoder) ToNode(_ bool) (*restic.Node, error) {
repo.StartPackUploader(ctx, wg) return m.node, m.err
arch := New(repo, fs.Track{FS: statfs}, Options{})
arch.Error = func(item string, err error) error {
t.Logf("archiver error as expected for %v: %v", item, err)
return err
}
arch.runWorkers(ctx, wg)
// fs.Track will panic if the file was not closed
_, excluded, err := arch.save(ctx, "/", tempfile, nil)
if err == nil {
t.Errorf("Save() should have failed")
}
if excluded {
t.Errorf("Save() excluded the node, that's unexpected")
}
} }
func TestMetadataBackupErrorFiltering(t *testing.T) { func TestMetadataBackupErrorFiltering(t *testing.T) {
tempdir := t.TempDir() tempdir := t.TempDir()
repo := repository.TestRepository(t)
filename := filepath.Join(tempdir, "file") filename := filepath.Join(tempdir, "file")
rtest.OK(t, os.WriteFile(filename, []byte("example"), 0o600)) repo := repository.TestRepository(t)
fi, err := os.Stat(filename)
rtest.OK(t, err)
arch := New(repo, fs.Local{}, Options{}) arch := New(repo, fs.Local{}, Options{})
@ -2406,15 +2419,24 @@ func TestMetadataBackupErrorFiltering(t *testing.T) {
return replacementErr return replacementErr
} }
nonExistNoder := &mockToNoder{
node: &restic.Node{Type: restic.NodeTypeFile},
err: fmt.Errorf("not found"),
}
// check that errors from reading extended metadata are properly filtered // check that errors from reading extended metadata are properly filtered
node, err := arch.nodeFromFileInfo("file", filename+"invalid", fi, false) node, err := arch.nodeFromFileInfo("file", filename+"invalid", nonExistNoder, false)
rtest.Assert(t, node != nil, "node is missing") rtest.Assert(t, node != nil, "node is missing")
rtest.Assert(t, err == replacementErr, "expected %v got %v", replacementErr, err) rtest.Assert(t, err == replacementErr, "expected %v got %v", replacementErr, err)
rtest.Assert(t, filteredErr != nil, "missing inner error") rtest.Assert(t, filteredErr != nil, "missing inner error")
// check that errors from reading irregular file are not filtered // check that errors from reading irregular file are not filtered
filteredErr = nil filteredErr = nil
node, err = arch.nodeFromFileInfo("file", filename, wrapIrregularFileInfo(fi), false) nonExistNoder = &mockToNoder{
node: &restic.Node{Type: restic.NodeTypeIrregular},
err: fmt.Errorf(`unsupported file type "irregular"`),
}
node, err = arch.nodeFromFileInfo("file", filename, nonExistNoder, false)
rtest.Assert(t, node != nil, "node is missing") rtest.Assert(t, node != nil, "node is missing")
rtest.Assert(t, filteredErr == nil, "error for irregular node should not have been filtered") rtest.Assert(t, filteredErr == nil, "error for irregular node should not have been filtered")
rtest.Assert(t, strings.Contains(err.Error(), "irregular"), "unexpected error %q does not warn about irregular file mode", err) rtest.Assert(t, strings.Contains(err.Error(), "irregular"), "unexpected error %q does not warn about irregular file mode", err)
@ -2434,17 +2456,19 @@ func TestIrregularFile(t *testing.T) {
tempfile := filepath.Join(tempdir, "testfile") tempfile := filepath.Join(tempdir, "testfile")
fi := lstat(t, "testfile") fi := lstat(t, "testfile")
statfs := &StatFS{ override := &overrideFS{
FS: fs.Local{}, FS: fs.Local{},
OverrideLstat: map[string]os.FileInfo{ overrideFI: wrapIrregularFileInfo(fi),
tempfile: wrapIrregularFileInfo(fi), overrideNode: &restic.Node{
Type: restic.NodeTypeIrregular,
}, },
overrideErr: fmt.Errorf(`unsupported file type "irregular"`),
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
arch := New(repo, fs.Track{FS: statfs}, Options{}) arch := New(repo, fs.Track{FS: override}, Options{})
_, excluded, err := arch.save(ctx, "/", tempfile, nil) _, excluded, err := arch.save(ctx, "/", tempfile, nil)
if err == nil { if err == nil {
t.Fatalf("Save() should have failed") t.Fatalf("Save() should have failed")

View file

@ -57,12 +57,8 @@ func wrapIrregularFileInfo(fi os.FileInfo) os.FileInfo {
} }
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) {
fi := lstat(t, name) want := nodeFromFile(t, &fs.Local{}, name)
fs := &fs.Local{} _, node := snapshot(t, repo, &fs.Local{}, nil, name)
want, err := fs.NodeFromFileInfo(name, fi, false)
rtest.OK(t, err)
_, node := snapshot(t, repo, fs, nil, name)
return want, node return want, node
} }

View file

@ -135,9 +135,9 @@ func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache,
return rejected return rejected
} }
func isDirExcludedByFile(dir, tagFilename, header string, fs fs.FS, warnf func(msg string, args ...interface{})) bool { func isDirExcludedByFile(dir, tagFilename, header string, fsInst fs.FS, warnf func(msg string, args ...interface{})) bool {
tf := fs.Join(dir, tagFilename) tf := fsInst.Join(dir, tagFilename)
_, err := fs.Lstat(tf) _, err := fsInst.Lstat(tf)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
return false return false
} }
@ -153,7 +153,7 @@ func isDirExcludedByFile(dir, tagFilename, header string, fs fs.FS, warnf func(m
// From this stage, errors mean tagFilename exists but it is malformed. // From this stage, errors mean tagFilename exists but it is malformed.
// Warnings will be generated so that the user is informed that the // Warnings will be generated so that the user is informed that the
// indented ignore-action is not performed. // indented ignore-action is not performed.
f, err := fs.OpenFile(tf, os.O_RDONLY, 0) f, err := fsInst.OpenFile(tf, fs.O_RDONLY, false)
if err != nil { if err != nil {
warnf("could not open exclusion tagfile: %v", err) warnf("could not open exclusion tagfile: %v", err)
return false return false

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"os"
"sync" "sync"
"github.com/restic/chunker" "github.com/restic/chunker"
@ -29,7 +28,7 @@ type fileSaver struct {
CompleteBlob func(bytes uint64) CompleteBlob func(bytes uint64)
NodeFromFileInfo func(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) NodeFromFileInfo func(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*restic.Node, error)
} }
// newFileSaver returns a new file saver. A worker pool with fileWorkers is // newFileSaver returns a new file saver. A worker pool with fileWorkers is
@ -71,13 +70,12 @@ type fileCompleteFunc func(*restic.Node, ItemStats)
// file is closed by Save. completeReading is only called if the file was read // file is closed by Save. completeReading is only called if the file was read
// successfully. complete is always called. If completeReading is called, then // successfully. complete is always called. If completeReading is called, then
// this will always happen before calling complete. // this will always happen before calling complete.
func (s *fileSaver) Save(ctx context.Context, snPath string, target string, file fs.File, fi os.FileInfo, start func(), completeReading func(), complete fileCompleteFunc) futureNode { func (s *fileSaver) Save(ctx context.Context, snPath string, target string, file fs.File, start func(), completeReading func(), complete fileCompleteFunc) futureNode {
fn, ch := newFutureNode() fn, ch := newFutureNode()
job := saveFileJob{ job := saveFileJob{
snPath: snPath, snPath: snPath,
target: target, target: target,
file: file, file: file,
fi: fi,
ch: ch, ch: ch,
start: start, start: start,
@ -100,7 +98,6 @@ type saveFileJob struct {
snPath string snPath string
target string target string
file fs.File file fs.File
fi os.FileInfo
ch chan<- futureNodeResult ch chan<- futureNodeResult
start func() start func()
@ -109,7 +106,7 @@ type saveFileJob struct {
} }
// saveFile stores the file f in the repo, then closes it. // saveFile stores the file f in the repo, then closes it.
func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPath string, target string, f fs.File, fi os.FileInfo, start func(), finishReading func(), finish func(res futureNodeResult)) { func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPath string, target string, f fs.File, start func(), finishReading func(), finish func(res futureNodeResult)) {
start() start()
fnr := futureNodeResult{ fnr := futureNodeResult{
@ -156,7 +153,7 @@ func (s *fileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPat
debug.Log("%v", snPath) debug.Log("%v", snPath)
node, err := s.NodeFromFileInfo(snPath, target, fi, false) node, err := s.NodeFromFileInfo(snPath, target, f, false)
if err != nil { if err != nil {
_ = f.Close() _ = f.Close()
completeError(err) completeError(err)
@ -262,7 +259,7 @@ func (s *fileSaver) worker(ctx context.Context, jobs <-chan saveFileJob) {
} }
} }
s.saveFile(ctx, chnker, job.snPath, job.target, job.file, job.fi, job.start, func() { s.saveFile(ctx, chnker, job.snPath, job.target, job.file, job.start, func() {
if job.completeReading != nil { if job.completeReading != nil {
job.completeReading() job.completeReading()
} }

View file

@ -30,7 +30,7 @@ func createTestFiles(t testing.TB, num int) (files []string) {
return files return files
} }
func startFileSaver(ctx context.Context, t testing.TB, fs fs.FS) (*fileSaver, context.Context, *errgroup.Group) { func startFileSaver(ctx context.Context, t testing.TB, fsInst fs.FS) (*fileSaver, context.Context, *errgroup.Group) {
wg, ctx := errgroup.WithContext(ctx) wg, ctx := errgroup.WithContext(ctx)
saveBlob := func(ctx context.Context, tpe restic.BlobType, buf *buffer, _ string, cb func(saveBlobResponse)) { saveBlob := func(ctx context.Context, tpe restic.BlobType, buf *buffer, _ string, cb func(saveBlobResponse)) {
@ -49,8 +49,8 @@ func startFileSaver(ctx context.Context, t testing.TB, fs fs.FS) (*fileSaver, co
} }
s := newFileSaver(ctx, wg, saveBlob, pol, workers, workers) s := newFileSaver(ctx, wg, saveBlob, pol, workers, workers)
s.NodeFromFileInfo = func(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) { s.NodeFromFileInfo = func(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*restic.Node, error) {
return fs.NodeFromFileInfo(filename, fi, ignoreXattrListError) return meta.ToNode(ignoreXattrListError)
} }
return s, ctx, wg return s, ctx, wg
@ -72,17 +72,12 @@ func TestFileSaver(t *testing.T) {
var results []futureNode var results []futureNode
for _, filename := range files { for _, filename := range files {
f, err := testFs.OpenFile(filename, os.O_RDONLY, 0) f, err := testFs.OpenFile(filename, os.O_RDONLY, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fi, err := f.Stat() ff := s.Save(ctx, filename, filename, f, startFn, completeReadingFn, completeFn)
if err != nil {
t.Fatal(err)
}
ff := s.Save(ctx, filename, filename, f, fi, startFn, completeReadingFn, completeFn)
results = append(results, ff) results = append(results, ff)
} }

View file

@ -7,3 +7,6 @@ import "syscall"
// O_NOFOLLOW instructs the kernel to not follow symlinks when opening a file. // O_NOFOLLOW instructs the kernel to not follow symlinks when opening a file.
const O_NOFOLLOW int = syscall.O_NOFOLLOW const O_NOFOLLOW int = syscall.O_NOFOLLOW
// O_DIRECTORY instructs the kernel to only open directories.
const O_DIRECTORY int = syscall.O_DIRECTORY

View file

@ -3,5 +3,12 @@
package fs package fs
// O_NOFOLLOW is a noop on Windows. // TODO honor flags when opening files
const O_NOFOLLOW int = 0
// O_NOFOLLOW is currently only interpreted by FS.OpenFile in metadataOnly mode and ignored by OpenFile.
// The value of the constant is invented and only for use within this fs package. It must not be used in other contexts.
// It must not conflict with the other O_* values from go/src/syscall/types_windows.go
const O_NOFOLLOW int = 0x40000000
// O_DIRECTORY is a noop on Windows.
const O_DIRECTORY int = 0

View file

@ -3,6 +3,7 @@ package fs
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
) )
// MkdirAll creates a directory named path, along with any necessary parents, // MkdirAll creates a directory named path, along with any necessary parents,
@ -47,6 +48,9 @@ func Lstat(name string) (os.FileInfo, error) {
// methods on the returned File can be used for I/O. // methods on the returned File can be used for I/O.
// If there is an error, it will be of type *PathError. // If there is an error, it will be of type *PathError.
func OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) { func OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
if runtime.GOOS == "windows" {
flag &^= O_NOFOLLOW
}
return os.OpenFile(fixpath(name), flag, perm) return os.OpenFile(fixpath(name), flag, perm)
} }
@ -64,9 +68,10 @@ func ResetPermissions(path string) error {
return nil return nil
} }
// Readdirnames returns a list of file in a directory. Flags are passed to fs.OpenFile. O_RDONLY is implied. // Readdirnames returns a list of file in a directory. Flags are passed to fs.OpenFile.
// O_RDONLY and O_DIRECTORY are implied.
func Readdirnames(filesystem FS, dir string, flags int) ([]string, error) { func Readdirnames(filesystem FS, dir string, flags int) ([]string, error) {
f, err := filesystem.OpenFile(dir, O_RDONLY|flags, 0) f, err := filesystem.OpenFile(dir, O_RDONLY|O_DIRECTORY|flags, false)
if err != nil { if err != nil {
return nil, fmt.Errorf("openfile for readdirnames failed: %w", err) return nil, fmt.Errorf("openfile for readdirnames failed: %w", err)
} }

View file

@ -0,0 +1,22 @@
//go:build unix
package fs
import (
"path/filepath"
"syscall"
"testing"
"github.com/restic/restic/internal/errors"
rtest "github.com/restic/restic/internal/test"
)
func TestReaddirnamesFifo(t *testing.T) {
// should not block when reading from a fifo instead of a directory
tempdir := t.TempDir()
fifoFn := filepath.Join(tempdir, "fifo")
rtest.OK(t, mkfifo(fifoFn, 0o600))
_, err := Readdirnames(&Local{}, fifoFn, 0)
rtest.Assert(t, errors.Is(err, syscall.ENOTDIR), "unexpected error %v", err)
}

View file

@ -20,24 +20,16 @@ func (fs Local) VolumeName(path string) string {
return filepath.VolumeName(path) return filepath.VolumeName(path)
} }
// OpenFile is the generalized open call; most users will use Open // OpenFile opens a file or directory for reading.
// or Create instead. It opens the named file with specified flag //
// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful, // If metadataOnly is set, an implementation MUST return a File object for
// methods on the returned File can be used for I/O. // arbitrary file types including symlinks. The implementation may internally use
// If there is an error, it will be of type *PathError. // the given file path or a file handle. In particular, an implementation may
func (fs Local) OpenFile(name string, flag int, perm os.FileMode) (File, error) { // delay actually accessing the underlying filesystem.
f, err := os.OpenFile(fixpath(name), flag, perm) //
if err != nil { // Only the O_NOFOLLOW and O_DIRECTORY flags are supported.
return nil, err func (fs Local) OpenFile(name string, flag int, metadataOnly bool) (File, error) {
} return newLocalFile(name, flag, metadataOnly)
_ = setFlags(f)
return f, nil
}
// Stat returns a FileInfo describing the named file. If there is an error, it
// will be of type *PathError.
func (fs Local) Stat(name string) (os.FileInfo, error) {
return os.Stat(fixpath(name))
} }
// Lstat returns the FileInfo structure describing the named file. // Lstat returns the FileInfo structure describing the named file.
@ -59,10 +51,6 @@ func (fs Local) ExtendedStat(fi os.FileInfo) ExtendedFileInfo {
return ExtendedStat(fi) return ExtendedStat(fi)
} }
func (fs Local) NodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) {
return nodeFromFileInfo(path, fi, ignoreXattrListError)
}
// 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
@ -103,3 +91,87 @@ func (fs Local) Base(path string) string {
func (fs Local) Dir(path string) string { func (fs Local) Dir(path string) string {
return filepath.Dir(path) return filepath.Dir(path)
} }
type localFile struct {
name string
flag int
f *os.File
fi os.FileInfo
}
// See the File interface for a description of each method
var _ File = &localFile{}
func newLocalFile(name string, flag int, metadataOnly bool) (*localFile, error) {
var f *os.File
if !metadataOnly {
var err error
f, err = os.OpenFile(fixpath(name), flag, 0)
if err != nil {
return nil, err
}
_ = setFlags(f)
}
return &localFile{
name: name,
flag: flag,
f: f,
}, nil
}
func (f *localFile) MakeReadable() error {
if f.f != nil {
panic("file is already readable")
}
newF, err := newLocalFile(f.name, f.flag, false)
if err != nil {
return err
}
// replace state and also reset cached FileInfo
*f = *newF
return nil
}
func (f *localFile) cacheFI() error {
if f.fi != nil {
return nil
}
var err error
if f.f != nil {
f.fi, err = f.f.Stat()
} else if f.flag&O_NOFOLLOW != 0 {
f.fi, err = os.Lstat(f.name)
} else {
f.fi, err = os.Stat(f.name)
}
return err
}
func (f *localFile) Stat() (os.FileInfo, error) {
err := f.cacheFI()
// the call to cacheFI MUST happen before reading from f.fi
return f.fi, err
}
func (f *localFile) ToNode(ignoreXattrListError bool) (*restic.Node, error) {
if err := f.cacheFI(); err != nil {
return nil, err
}
return nodeFromFileInfo(f.name, f.fi, ignoreXattrListError)
}
func (f *localFile) Read(p []byte) (n int, err error) {
return f.f.Read(p)
}
func (f *localFile) Readdirnames(n int) ([]string, error) {
return f.f.Readdirnames(n)
}
func (f *localFile) Close() error {
if f.f != nil {
return f.f.Close()
}
return nil
}

View file

@ -0,0 +1,222 @@
package fs
import (
"io"
"os"
"path/filepath"
"slices"
"testing"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
type fsLocalMetadataTestcase struct {
name string
follow bool
setup func(t *testing.T, path string)
nodeType restic.NodeType
}
func TestFSLocalMetadata(t *testing.T) {
for _, test := range []fsLocalMetadataTestcase{
{
name: "file",
setup: func(t *testing.T, path string) {
rtest.OK(t, os.WriteFile(path, []byte("example"), 0o600))
},
nodeType: restic.NodeTypeFile,
},
{
name: "directory",
setup: func(t *testing.T, path string) {
rtest.OK(t, os.Mkdir(path, 0o600))
},
nodeType: restic.NodeTypeDir,
},
{
name: "symlink",
setup: func(t *testing.T, path string) {
rtest.OK(t, os.Symlink(path+"old", path))
},
nodeType: restic.NodeTypeSymlink,
},
{
name: "symlink file",
follow: true,
setup: func(t *testing.T, path string) {
rtest.OK(t, os.WriteFile(path+"file", []byte("example"), 0o600))
rtest.OK(t, os.Symlink(path+"file", path))
},
nodeType: restic.NodeTypeFile,
},
} {
runFSLocalTestcase(t, test)
}
}
func runFSLocalTestcase(t *testing.T, test fsLocalMetadataTestcase) {
t.Run(test.name, func(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
test.setup(t, path)
testFs := &Local{}
flags := 0
if !test.follow {
flags |= O_NOFOLLOW
}
f, err := testFs.OpenFile(path, flags, true)
rtest.OK(t, err)
checkMetadata(t, f, path, test.follow, test.nodeType)
rtest.OK(t, f.Close())
})
}
func checkMetadata(t *testing.T, f File, path string, follow bool, nodeType restic.NodeType) {
fi, err := f.Stat()
rtest.OK(t, err)
var fi2 os.FileInfo
if follow {
fi2, err = os.Stat(path)
} else {
fi2, err = os.Lstat(path)
}
rtest.OK(t, err)
assertFIEqual(t, fi2, fi)
node, err := f.ToNode(false)
rtest.OK(t, err)
// ModTime is likely unique per file, thus it provides a good indication that it is from the correct file
rtest.Equals(t, fi.ModTime(), node.ModTime, "node ModTime")
rtest.Equals(t, nodeType, node.Type, "node Type")
}
func assertFIEqual(t *testing.T, want os.FileInfo, got os.FileInfo) {
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")
}
func TestFSLocalRead(t *testing.T) {
testFSLocalRead(t, false)
testFSLocalRead(t, true)
}
func testFSLocalRead(t *testing.T, makeReadable bool) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
testdata := "example"
rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600))
f := openReadable(t, path, makeReadable)
checkMetadata(t, f, path, false, restic.NodeTypeFile)
data, err := io.ReadAll(f)
rtest.OK(t, err)
rtest.Equals(t, testdata, string(data), "file content mismatch")
rtest.OK(t, f.Close())
}
func openReadable(t *testing.T, path string, useMakeReadable bool) File {
testFs := &Local{}
f, err := testFs.OpenFile(path, O_NOFOLLOW, useMakeReadable)
rtest.OK(t, err)
if useMakeReadable {
// file was opened as metadataOnly. open for reading
rtest.OK(t, f.MakeReadable())
}
return f
}
func TestFSLocalReaddir(t *testing.T) {
testFSLocalReaddir(t, false)
testFSLocalReaddir(t, true)
}
func testFSLocalReaddir(t *testing.T, makeReadable bool) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
rtest.OK(t, os.Mkdir(path, 0o700))
entries := []string{"testfile"}
rtest.OK(t, os.WriteFile(filepath.Join(path, entries[0]), []byte("example"), 0o600))
f := openReadable(t, path, makeReadable)
checkMetadata(t, f, path, false, restic.NodeTypeDir)
names, err := f.Readdirnames(-1)
rtest.OK(t, err)
slices.Sort(names)
rtest.Equals(t, entries, names, "directory content mismatch")
rtest.OK(t, f.Close())
}
func TestFSLocalReadableRace(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
testdata := "example"
rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600))
testFs := &Local{}
f, err := testFs.OpenFile(path, O_NOFOLLOW, true)
rtest.OK(t, err)
pathNew := path + "new"
rtest.OK(t, os.Rename(path, pathNew))
err = f.MakeReadable()
if err == nil {
// a file handle based implementation should still work
checkMetadata(t, f, pathNew, false, restic.NodeTypeFile)
data, err := io.ReadAll(f)
rtest.OK(t, err)
rtest.Equals(t, testdata, string(data), "file content mismatch")
}
rtest.OK(t, f.Close())
}
func TestFSLocalTypeChange(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "item")
testdata := "example"
rtest.OK(t, os.WriteFile(path, []byte(testdata), 0o600))
testFs := &Local{}
f, err := testFs.OpenFile(path, O_NOFOLLOW, true)
rtest.OK(t, err)
// cache metadata
_, err = f.Stat()
rtest.OK(t, err)
pathNew := path + "new"
// rename instead of unlink to let the test also work on windows
rtest.OK(t, os.Rename(path, pathNew))
rtest.OK(t, os.Mkdir(path, 0o700))
rtest.OK(t, f.MakeReadable())
fi, err := f.Stat()
rtest.OK(t, err)
if !fi.IsDir() {
// a file handle based implementation should still reference the file
checkMetadata(t, f, pathNew, false, restic.NodeTypeFile)
data, err := io.ReadAll(f)
rtest.OK(t, err)
rtest.Equals(t, testdata, string(data), "file content mismatch")
}
// else:
// path-based implementation
// nothing to test here. stat returned the new file type
rtest.OK(t, f.Close())
}

View file

@ -0,0 +1,40 @@
//go:build unix
package fs
import (
"syscall"
"testing"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
func TestFSLocalMetadataUnix(t *testing.T) {
for _, test := range []fsLocalMetadataTestcase{
{
name: "socket",
setup: func(t *testing.T, path string) {
fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
rtest.OK(t, err)
defer func() {
_ = syscall.Close(fd)
}()
addr := &syscall.SockaddrUnix{Name: path}
rtest.OK(t, syscall.Bind(fd, addr))
},
nodeType: restic.NodeTypeSocket,
},
{
name: "fifo",
setup: func(t *testing.T, path string) {
rtest.OK(t, mkfifo(path, 0o600))
},
nodeType: restic.NodeTypeFifo,
},
// device files can only be created as root
} {
runFSLocalTestcase(t, test)
}
}

View file

@ -10,7 +10,6 @@ import (
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options" "github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
) )
// VSSConfig holds extended options of windows volume shadow copy service. // VSSConfig holds extended options of windows volume shadow copy service.
@ -126,14 +125,9 @@ func (fs *LocalVss) DeleteSnapshots() {
fs.snapshots = activeSnapshots fs.snapshots = activeSnapshots
} }
// OpenFile wraps the Open method of the underlying file system. // OpenFile wraps the OpenFile method of the underlying file system.
func (fs *LocalVss) OpenFile(name string, flag int, perm os.FileMode) (File, error) { func (fs *LocalVss) OpenFile(name string, flag int, metadataOnly bool) (File, error) {
return fs.FS.OpenFile(fs.snapshotPath(name), flag, perm) return fs.FS.OpenFile(fs.snapshotPath(name), flag, metadataOnly)
}
// Stat wraps the Stat method of the underlying file system.
func (fs *LocalVss) Stat(name string) (os.FileInfo, error) {
return fs.FS.Stat(fs.snapshotPath(name))
} }
// Lstat wraps the Lstat method of the underlying file system. // Lstat wraps the Lstat method of the underlying file system.
@ -141,10 +135,6 @@ func (fs *LocalVss) Lstat(name string) (os.FileInfo, error) {
return fs.FS.Lstat(fs.snapshotPath(name)) return fs.FS.Lstat(fs.snapshotPath(name))
} }
func (fs *LocalVss) NodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) {
return fs.FS.NodeFromFileInfo(fs.snapshotPath(path), fi, ignoreXattrListError)
}
// isMountPointIncluded is true if given mountpoint included by user. // isMountPointIncluded is true if given mountpoint included by user.
func (fs *LocalVss) isMountPointIncluded(mountPoint string) bool { func (fs *LocalVss) isMountPointIncluded(mountPoint string) bool {
if fs.excludeVolumes == nil { if fs.excludeVolumes == nil {

View file

@ -317,28 +317,25 @@ func TestVSSFS(t *testing.T) {
// trigger snapshot creation and // trigger snapshot creation and
// capture FI while file still exists (should already be within the snapshot) // capture FI while file still exists (should already be within the snapshot)
origFi, err := localVss.Stat(tempfile) origFi, err := localVss.Lstat(tempfile)
rtest.OK(t, err) rtest.OK(t, err)
// remove original file // remove original file
rtest.OK(t, os.Remove(tempfile)) rtest.OK(t, os.Remove(tempfile))
statFi, err := localVss.Stat(tempfile)
rtest.OK(t, err)
rtest.Equals(t, origFi.Mode(), statFi.Mode())
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, 0) f, err := localVss.OpenFile(tempfile, os.O_RDONLY, false)
rtest.OK(t, err) rtest.OK(t, err)
data, err := io.ReadAll(f) data, err := io.ReadAll(f)
rtest.OK(t, err) rtest.OK(t, err)
rtest.Equals(t, "example", string(data), "unexpected file content") rtest.Equals(t, "example", string(data), "unexpected file content")
rtest.OK(t, f.Close())
node, err := localVss.NodeFromFileInfo(tempfile, statFi, false) node, err := f.ToNode(false)
rtest.OK(t, err) rtest.OK(t, err)
rtest.Equals(t, node.Mode, statFi.Mode()) rtest.Equals(t, node.Mode, lstatFi.Mode())
rtest.OK(t, f.Close())
} }

View file

@ -49,12 +49,7 @@ func (fs *Reader) fi() os.FileInfo {
} }
} }
// OpenFile is the generalized open call; most users will use Open func (fs *Reader) OpenFile(name string, flag int, _ bool) (f File, err error) {
// or Create instead. It opens the named file with specified flag
// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful,
// methods on the returned File can be used for I/O.
// If there is an error, it will be of type *os.PathError.
func (fs *Reader) OpenFile(name string, flag int, _ os.FileMode) (f File, err error) {
if flag & ^(O_RDONLY|O_NOFOLLOW) != 0 { if flag & ^(O_RDONLY|O_NOFOLLOW) != 0 {
return nil, pathError("open", name, return nil, pathError("open", name,
fmt.Errorf("invalid combination of flags 0x%x", flag)) fmt.Errorf("invalid combination of flags 0x%x", flag))
@ -81,12 +76,6 @@ func (fs *Reader) OpenFile(name string, flag int, _ os.FileMode) (f File, err er
return nil, pathError("open", name, syscall.ENOENT) return nil, pathError("open", name, syscall.ENOENT)
} }
// Stat returns a FileInfo describing the named file. If there is an error, it
// will be of type *os.PathError.
func (fs *Reader) Stat(name string) (os.FileInfo, error) {
return fs.Lstat(name)
}
// Lstat returns the FileInfo structure describing the named file. // Lstat returns the FileInfo structure describing the named file.
// 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.
@ -133,17 +122,6 @@ func (fs *Reader) ExtendedStat(fi os.FileInfo) ExtendedFileInfo {
} }
} }
func (fs *Reader) NodeFromFileInfo(path string, fi os.FileInfo, _ bool) (*restic.Node, error) {
node := buildBasicNode(path, fi)
// fill minimal info with current values for uid, gid
node.UID = uint32(os.Getuid())
node.GID = uint32(os.Getgid())
node.ChangeTime = node.ModTime
return node, nil
}
// 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
@ -241,6 +219,10 @@ type fakeFile struct {
// ensure that fakeFile implements File // ensure that fakeFile implements File
var _ File = fakeFile{} var _ File = fakeFile{}
func (f fakeFile) MakeReadable() error {
return nil
}
func (f fakeFile) Readdirnames(_ int) ([]string, error) { func (f fakeFile) Readdirnames(_ int) ([]string, error) {
return nil, pathError("readdirnames", f.name, os.ErrInvalid) return nil, pathError("readdirnames", f.name, os.ErrInvalid)
} }
@ -257,6 +239,17 @@ func (f fakeFile) Stat() (os.FileInfo, error) {
return f.FileInfo, nil return f.FileInfo, nil
} }
func (f fakeFile) ToNode(_ bool) (*restic.Node, error) {
node := buildBasicNode(f.name, f.FileInfo)
// fill minimal info with current values for uid, gid
node.UID = uint32(os.Getuid())
node.GID = uint32(os.Getgid())
node.ChangeTime = node.ModTime
return node, nil
}
// 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 []os.FileInfo

View file

@ -16,7 +16,7 @@ import (
) )
func verifyFileContentOpenFile(t testing.TB, fs FS, filename string, want []byte) { func verifyFileContentOpenFile(t testing.TB, fs FS, filename string, want []byte) {
f, err := fs.OpenFile(filename, O_RDONLY, 0) f, err := fs.OpenFile(filename, O_RDONLY, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -37,7 +37,7 @@ func verifyFileContentOpenFile(t testing.TB, fs FS, filename string, want []byte
} }
func verifyDirectoryContents(t testing.TB, fs FS, dir string, want []string) { func verifyDirectoryContents(t testing.TB, fs FS, dir string, want []string) {
f, err := fs.OpenFile(dir, os.O_RDONLY, 0) f, err := fs.OpenFile(dir, O_RDONLY, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -123,7 +123,7 @@ func TestFSReader(t *testing.T) {
{ {
name: "file/Stat", name: "file/Stat",
f: func(t *testing.T, fs FS) { f: func(t *testing.T, fs FS) {
f, err := fs.OpenFile(filename, os.O_RDONLY, 0) f, err := fs.OpenFile(filename, O_RDONLY, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -295,7 +295,7 @@ func TestFSReaderMinFileSize(t *testing.T) {
AllowEmptyFile: test.allowEmpty, AllowEmptyFile: test.allowEmpty,
} }
f, err := fs.OpenFile("testfile", os.O_RDONLY, 0) f, err := fs.OpenFile("testfile", O_RDONLY, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -16,8 +16,8 @@ type Track struct {
} }
// OpenFile wraps the OpenFile method of the underlying file system. // OpenFile wraps the OpenFile method of the underlying file system.
func (fs Track) OpenFile(name string, flag int, perm os.FileMode) (File, error) { func (fs Track) OpenFile(name string, flag int, metadataOnly bool) (File, error) {
f, err := fs.FS.OpenFile(fixpath(name), flag, perm) f, err := fs.FS.OpenFile(name, flag, metadataOnly)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -31,7 +31,7 @@ type trackFile struct {
func newTrackFile(stack []byte, filename string, file File) *trackFile { func newTrackFile(stack []byte, filename string, file File) *trackFile {
f := &trackFile{file} f := &trackFile{file}
runtime.SetFinalizer(f, func(_ *trackFile) { runtime.SetFinalizer(f, func(_ any) {
fmt.Fprintf(os.Stderr, "file %s not closed\n\nStacktrack:\n%s\n", filename, stack) fmt.Fprintf(os.Stderr, "file %s not closed\n\nStacktrack:\n%s\n", filename, stack)
panic("file " + filename + " not closed") panic("file " + filename + " not closed")
}) })

View file

@ -9,12 +9,18 @@ import (
// FS bundles all methods needed for a file system. // FS bundles all methods needed for a file system.
type FS interface { type FS interface {
OpenFile(name string, flag int, perm os.FileMode) (File, error) // OpenFile opens a file or directory for reading.
Stat(name string) (os.FileInfo, error) //
// If metadataOnly is set, an implementation MUST return a File object for
// arbitrary file types including symlinks. The implementation may internally use
// the given file path or a file handle. In particular, an implementation may
// delay actually accessing the underlying filesystem.
//
// 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) Lstat(name string) (os.FileInfo, error)
DeviceID(fi os.FileInfo) (deviceID uint64, err error) DeviceID(fi os.FileInfo) (deviceID uint64, err error)
ExtendedStat(fi os.FileInfo) ExtendedFileInfo ExtendedStat(fi os.FileInfo) ExtendedFileInfo
NodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error)
Join(elem ...string) string Join(elem ...string) string
Separator() string Separator() string
@ -27,11 +33,23 @@ type FS interface {
Base(path string) string Base(path string) string
} }
// File is an open file on a file system. // File is an open file on a file system. When opened as metadataOnly, an
// implementation may opt to perform filesystem operations using the filepath
// instead of actually opening the file.
type File interface { type File interface {
// MakeReadable reopens a File that was opened metadataOnly for reading.
// The method must not be called for files that are opened for reading.
// If possible, the underlying file should be reopened atomically.
// MakeReadable must work for files and directories.
MakeReadable() error
io.Reader io.Reader
io.Closer io.Closer
Readdirnames(n int) ([]string, error) Readdirnames(n int) ([]string, error)
Stat() (os.FileInfo, error) Stat() (os.FileInfo, 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.
ToNode(ignoreXattrListError bool) (*restic.Node, error)
} }

View file

@ -23,11 +23,8 @@ func nodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*
return node, err return node, err
} }
allowExtended, err := nodeFillGenericAttributes(node, path, &stat) err := nodeFillGenericAttributes(node, path, &stat)
if allowExtended { err = errors.Join(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError))
// Skip processing ExtendedAttributes if allowExtended is false.
err = errors.Join(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError))
}
return node, err return node, err
} }

View file

@ -1,26 +0,0 @@
//go:build aix
// +build aix
package fs
import "github.com/restic/restic/internal/restic"
// nodeRestoreExtendedAttributes is a no-op on AIX.
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
return nil
}
// nodeFillExtendedAttributes is a no-op on AIX.
func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error {
return nil
}
// nodeRestoreGenericAttributes is no-op on AIX.
func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
}
// nodeFillGenericAttributes is a no-op on AIX.
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) (allowExtended bool, err error) {
return true, nil
}

View file

@ -1,23 +0,0 @@
package fs
import "github.com/restic/restic/internal/restic"
// nodeRestoreExtendedAttributes is a no-op on netbsd.
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
return nil
}
// nodeFillExtendedAttributes is a no-op on netbsd.
func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error {
return nil
}
// nodeRestoreGenericAttributes is no-op on netbsd.
func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
}
// nodeFillGenericAttributes is a no-op on netbsd.
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) (allowExtended bool, err error) {
return true, nil
}

View file

@ -0,0 +1,18 @@
//go:build aix || netbsd || openbsd
// +build aix netbsd openbsd
package fs
import (
"github.com/restic/restic/internal/restic"
)
// nodeRestoreExtendedAttributes is a no-op
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
return nil
}
// nodeFillExtendedAttributes is a no-op
func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error {
return nil
}

View file

@ -1,23 +0,0 @@
package fs
import "github.com/restic/restic/internal/restic"
// nodeRestoreExtendedAttributes is a no-op on openbsd.
func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error {
return nil
}
// nodeFillExtendedAttributes is a no-op on openbsd.
func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error {
return nil
}
// nodeRestoreGenericAttributes is no-op on openbsd.
func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
}
// fillGenericAttributes is a no-op on openbsd.
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) (allowExtended bool, err error) {
return true, nil
}

View file

@ -17,56 +17,26 @@ import (
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func BenchmarkNodeFillUser(t *testing.B) { func BenchmarkNodeFromFileInfo(t *testing.B) {
tempfile, err := os.CreateTemp("", "restic-test-temp-") tempfile, err := os.CreateTemp(t.TempDir(), "restic-test-temp-")
if err != nil { rtest.OK(t, err)
t.Fatal(err)
}
fi, err := tempfile.Stat()
if err != nil {
t.Fatal(err)
}
path := tempfile.Name() path := tempfile.Name()
rtest.OK(t, tempfile.Close())
fs := Local{} fs := Local{}
f, err := fs.OpenFile(path, O_NOFOLLOW, true)
rtest.OK(t, err)
_, err = f.Stat()
rtest.OK(t, err)
t.ResetTimer() t.ResetTimer()
for i := 0; i < t.N; i++ { for i := 0; i < t.N; i++ {
_, err := fs.NodeFromFileInfo(path, fi, false) _, err := f.ToNode(false)
rtest.OK(t, err) rtest.OK(t, err)
} }
rtest.OK(t, tempfile.Close()) rtest.OK(t, f.Close())
rtest.RemoveAll(t, tempfile.Name())
}
func BenchmarkNodeFromFileInfo(t *testing.B) {
tempfile, err := os.CreateTemp("", "restic-test-temp-")
if err != nil {
t.Fatal(err)
}
fi, err := tempfile.Stat()
if err != nil {
t.Fatal(err)
}
path := tempfile.Name()
fs := Local{}
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, err := fs.NodeFromFileInfo(path, fi, false)
if err != nil {
t.Fatal(err)
}
}
rtest.OK(t, tempfile.Close())
rtest.RemoveAll(t, tempfile.Name())
} }
func parseTime(s string) time.Time { func parseTime(s string) time.Time {
@ -249,14 +219,14 @@ func TestNodeRestoreAt(t *testing.T) {
rtest.OK(t, NodeCreateAt(&test, nodePath)) rtest.OK(t, NodeCreateAt(&test, nodePath))
rtest.OK(t, NodeRestoreMetadata(&test, nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) })) rtest.OK(t, NodeRestoreMetadata(&test, nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) }))
fi, err := os.Lstat(nodePath)
rtest.OK(t, err)
fs := &Local{} fs := &Local{}
n2, err := fs.NodeFromFileInfo(nodePath, fi, false) meta, err := fs.OpenFile(nodePath, O_NOFOLLOW, true)
rtest.OK(t, err) rtest.OK(t, err)
n3, err := fs.NodeFromFileInfo(nodePath, fi, true) n2, err := meta.ToNode(false)
rtest.OK(t, err) rtest.OK(t, err)
n3, err := meta.ToNode(true)
rtest.OK(t, err)
rtest.OK(t, meta.Close())
rtest.Assert(t, n2.Equals(*n3), "unexpected node info mismatch %v", cmp.Diff(n2, n3)) rtest.Assert(t, n2.Equals(*n3), "unexpected node info mismatch %v", cmp.Diff(n2, n3))
rtest.Assert(t, test.Name == n2.Name, rtest.Assert(t, test.Name == n2.Name,

View file

@ -5,8 +5,20 @@ package fs
import ( import (
"os" "os"
"github.com/restic/restic/internal/restic"
) )
func lchown(name string, uid, gid int) error { func lchown(name string, uid, gid int) error {
return os.Lchown(name, uid, gid) return os.Lchown(name, uid, gid)
} }
// nodeRestoreGenericAttributes is no-op.
func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
}
// nodeFillGenericAttributes is a no-op.
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) error {
return nil
}

View file

@ -114,16 +114,14 @@ func TestNodeFromFileInfo(t *testing.T) {
return return
} }
if fi.Sys() == nil {
t.Skip("fi.Sys() is nil")
return
}
fs := &Local{} fs := &Local{}
node, err := fs.NodeFromFileInfo(test.filename, fi, false) meta, err := fs.OpenFile(test.filename, O_NOFOLLOW, true)
if err != nil { rtest.OK(t, err)
t.Fatal(err) node, err := meta.ToNode(false)
} rtest.OK(t, err)
rtest.OK(t, meta.Close())
rtest.OK(t, err)
switch node.Type { switch node.Type {
case restic.NodeTypeFile, restic.NodeTypeSymlink: case restic.NodeTypeFile, restic.NodeTypeSymlink:

View file

@ -83,8 +83,28 @@ func nodeRestoreExtendedAttributes(node *restic.Node, path string) (err error) {
return nil return nil
} }
// fill extended attributes in the node. This also includes the Generic attributes for windows. // fill extended attributes in the node
// It also checks if the volume supports extended attributes and stores the result in a map
// so that it does not have to be checked again for subsequent calls for paths in the same volume.
func nodeFillExtendedAttributes(node *restic.Node, path string, _ bool) (err error) { func nodeFillExtendedAttributes(node *restic.Node, path string, _ bool) (err error) {
if strings.Contains(filepath.Base(path), ":") {
// Do not process for Alternate Data Streams in Windows
return nil
}
// only capture xattrs for file/dir
if node.Type != restic.NodeTypeFile && node.Type != restic.NodeTypeDir {
return nil
}
allowExtended, err := checkAndStoreEASupport(path)
if err != nil {
return err
}
if !allowExtended {
return nil
}
var fileHandle windows.Handle var fileHandle windows.Handle
if fileHandle, err = openHandleForEA(node.Type, path, false); fileHandle == 0 { if fileHandle, err = openHandleForEA(node.Type, path, false); fileHandle == 0 {
return nil return nil
@ -316,40 +336,28 @@ func decryptFile(pathPointer *uint16) error {
// nodeFillGenericAttributes fills in the generic attributes for windows like File Attributes, // nodeFillGenericAttributes fills in the generic attributes for windows like File Attributes,
// Created time and Security Descriptors. // Created time and Security Descriptors.
// It also checks if the volume supports extended attributes and stores the result in a map func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFileInfo) error {
// so that it does not have to be checked again for subsequent calls for paths in the same volume.
func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFileInfo) (allowExtended bool, err error) {
if strings.Contains(filepath.Base(path), ":") { if strings.Contains(filepath.Base(path), ":") {
// Do not process for Alternate Data Streams in Windows // Do not process for Alternate Data Streams in Windows
// Also do not allow processing of extended attributes for ADS. return nil
return false, nil
} }
isVolume, err := isVolumePath(path) isVolume, err := isVolumePath(path)
if err != nil { if err != nil {
return false, err return err
} }
if isVolume { if isVolume {
// Do not process file attributes, created time and sd for windows root volume paths // Do not process file attributes, created time and sd for windows root volume paths
// Security descriptors are not supported for root volume paths. // Security descriptors are not supported for root volume paths.
// Though file attributes and created time are supported for root volume paths, // Though file attributes and created time are supported for root volume paths,
// we ignore them and we do not want to replace them during every restore. // we ignore them and we do not want to replace them during every restore.
allowExtended, err = checkAndStoreEASupport(path) return nil
if err != nil {
return false, err
}
return allowExtended, err
} }
var sd *[]byte var sd *[]byte
if node.Type == restic.NodeTypeFile || node.Type == restic.NodeTypeDir { if node.Type == restic.NodeTypeFile || node.Type == restic.NodeTypeDir {
// Check EA support and get security descriptor for file/dir only
allowExtended, err = checkAndStoreEASupport(path)
if err != nil {
return false, err
}
if sd, err = getSecurityDescriptor(path); err != nil { if sd, err = getSecurityDescriptor(path); err != nil {
return allowExtended, err return err
} }
} }
@ -361,7 +369,7 @@ func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFil
FileAttributes: &winFI.FileAttributes, FileAttributes: &winFI.FileAttributes,
SecurityDescriptor: sd, SecurityDescriptor: sd,
}) })
return allowExtended, err return err
} }
// checkAndStoreEASupport checks if the volume of the path supports extended attributes and stores the result in a map // checkAndStoreEASupport checks if the volume of the path supports extended attributes and stores the result in a map

View file

@ -222,11 +222,11 @@ func restoreAndGetNode(t *testing.T, tempDir string, testNode *restic.Node, warn
test.OK(t, errors.Wrapf(err, "Failed to restore metadata for: %s", testPath)) test.OK(t, errors.Wrapf(err, "Failed to restore metadata for: %s", testPath))
fs := &Local{} fs := &Local{}
fi, err := fs.Lstat(testPath) meta, err := fs.OpenFile(testPath, O_NOFOLLOW, true)
test.OK(t, errors.Wrapf(err, "Could not Lstat for path: %s", testPath)) test.OK(t, err)
nodeFromFileInfo, err := meta.ToNode(false)
nodeFromFileInfo, err := fs.NodeFromFileInfo(testPath, fi, false)
test.OK(t, errors.Wrapf(err, "Could not get NodeFromFileInfo for path: %s", testPath)) test.OK(t, errors.Wrapf(err, "Could not get NodeFromFileInfo for path: %s", testPath))
test.OK(t, meta.Close())
return testPath, nodeFromFileInfo return testPath, nodeFromFileInfo
} }

View file

@ -65,16 +65,6 @@ func handleXattrErr(err error) error {
} }
} }
// nodeRestoreGenericAttributes is no-op.
func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg string)) error {
return restic.HandleAllUnknownGenericAttributesFound(node.GenericAttributes, warn)
}
// nodeFillGenericAttributes is a no-op.
func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) (allowExtended bool, err error) {
return true, nil
}
func nodeRestoreExtendedAttributes(node *restic.Node, path string) error { func nodeRestoreExtendedAttributes(node *restic.Node, path string) error {
expectedAttrs := map[string]struct{}{} expectedAttrs := map[string]struct{}{}
for _, attr := range node.ExtendedAttributes { for _, attr := range node.ExtendedAttributes {

View file

@ -83,13 +83,17 @@ func TestNodeMarshal(t *testing.T) {
} }
} }
func TestNodeComparison(t *testing.T) { func nodeForFile(t *testing.T, name string) *restic.Node {
fs := &fs.Local{} f, err := (&fs.Local{}).OpenFile(name, fs.O_NOFOLLOW, true)
fi, err := fs.Lstat("tree_test.go")
rtest.OK(t, err) rtest.OK(t, err)
node, err := f.ToNode(false)
rtest.OK(t, err)
rtest.OK(t, f.Close())
return node
}
node, err := fs.NodeFromFileInfo("tree_test.go", fi, false) func TestNodeComparison(t *testing.T) {
rtest.OK(t, err) node := nodeForFile(t, "tree_test.go")
n2 := *node n2 := *node
rtest.Assert(t, node.Equals(n2), "nodes aren't equal") rtest.Assert(t, node.Equals(n2), "nodes aren't equal")
@ -127,11 +131,7 @@ func TestTreeEqualSerialization(t *testing.T) {
builder := restic.NewTreeJSONBuilder() builder := restic.NewTreeJSONBuilder()
for _, fn := range files[:i] { for _, fn := range files[:i] {
fs := &fs.Local{} node := nodeForFile(t, fn)
fi, err := fs.Lstat(fn)
rtest.OK(t, err)
node, err := fs.NodeFromFileInfo(fn, fi, false)
rtest.OK(t, err)
rtest.OK(t, tree.Insert(node)) rtest.OK(t, tree.Insert(node))
rtest.OK(t, builder.AddNode(node)) rtest.OK(t, builder.AddNode(node))