forked from TrueCloudLab/restic
Merge pull request #5143 from MichaelEischer/fs-handle-interface
fs: rework FS interface to be handle based
This commit is contained in:
commit
8642049532
31 changed files with 817 additions and 485 deletions
|
@ -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().
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
22
internal/fs/file_unix_test.go
Normal file
22
internal/fs/file_unix_test.go
Normal 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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
222
internal/fs/fs_local_test.go
Normal file
222
internal/fs/fs_local_test.go
Normal 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())
|
||||||
|
}
|
40
internal/fs/fs_local_unix_test.go
Normal file
40
internal/fs/fs_local_unix_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
18
internal/fs/node_noxattr.go
Normal file
18
internal/fs/node_noxattr.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in a new issue