forked from TrueCloudLab/restic
Merge pull request #4668 from MichaelEischer/backup-xattr-parent-enoperm
backup: Ignore xattr.list permission error for parent directories
This commit is contained in:
commit
8efc3a8b7d
17 changed files with 97 additions and 25 deletions
11
changelog/unreleased/issue-3600
Normal file
11
changelog/unreleased/issue-3600
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Bugfix: `backup` works if xattrs above the backup target cannot be read
|
||||||
|
|
||||||
|
When backup targets are specified using absolute paths, then `backup` also
|
||||||
|
includes information about the parent folders of the backup targets in the
|
||||||
|
snapshot. If the extended attributes for some of these folders could not be
|
||||||
|
read due to missing permissions, this caused the backup to fail. This has been
|
||||||
|
fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3600
|
||||||
|
https://github.com/restic/restic/pull/4668
|
||||||
|
https://forum.restic.net/t/parent-directories-above-the-snapshot-source-path-fatal-error-permission-denied/7216
|
|
@ -237,8 +237,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) (*restic.Node, error) {
|
func (arch *Archiver) nodeFromFileInfo(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) {
|
||||||
node, err := restic.NodeFromFileInfo(filename, fi)
|
node, err := restic.NodeFromFileInfo(filename, fi, ignoreXattrListError)
|
||||||
if !arch.WithAtime {
|
if !arch.WithAtime {
|
||||||
node.AccessTime = node.ModTime
|
node.AccessTime = node.ModTime
|
||||||
}
|
}
|
||||||
|
@ -289,7 +289,7 @@ func (arch *Archiver) wrapLoadTreeError(id restic.ID, err error) error {
|
||||||
func (arch *Archiver) saveDir(ctx context.Context, snPath string, dir string, fi os.FileInfo, previous *restic.Tree, complete CompleteFunc) (d FutureNode, err error) {
|
func (arch *Archiver) saveDir(ctx context.Context, snPath string, dir string, fi os.FileInfo, previous *restic.Tree, complete CompleteFunc) (d FutureNode, err error) {
|
||||||
debug.Log("%v %v", snPath, dir)
|
debug.Log("%v %v", snPath, dir)
|
||||||
|
|
||||||
treeNode, err := arch.nodeFromFileInfo(snPath, dir, fi)
|
treeNode, err := arch.nodeFromFileInfo(snPath, dir, fi, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FutureNode{}, err
|
return FutureNode{}, err
|
||||||
}
|
}
|
||||||
|
@ -444,7 +444,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)
|
node, err := arch.nodeFromFileInfo(snPath, target, fi, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FutureNode{}, false, err
|
return FutureNode{}, false, err
|
||||||
}
|
}
|
||||||
|
@ -540,7 +540,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)
|
node, err := arch.nodeFromFileInfo(snPath, target, fi, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FutureNode{}, false, err
|
return FutureNode{}, false, err
|
||||||
}
|
}
|
||||||
|
@ -623,7 +623,9 @@ func (arch *Archiver) saveTree(ctx context.Context, snPath string, atree *Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.Log("%v, dir node data loaded from %v", snPath, atree.FileInfoPath)
|
debug.Log("%v, dir node data loaded from %v", snPath, atree.FileInfoPath)
|
||||||
node, err = arch.nodeFromFileInfo(snPath, atree.FileInfoPath, fi)
|
// in some cases reading xattrs for directories above the backup target 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -556,7 +556,7 @@ func rename(t testing.TB, oldname, newname string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeFromFI(t testing.TB, filename string, fi os.FileInfo) *restic.Node {
|
func nodeFromFI(t testing.TB, filename string, fi os.FileInfo) *restic.Node {
|
||||||
node, err := restic.NodeFromFileInfo(filename, fi)
|
node, err := restic.NodeFromFileInfo(filename, fi, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -2230,7 +2230,7 @@ func TestMetadataChanged(t *testing.T) {
|
||||||
|
|
||||||
// get metadata
|
// get metadata
|
||||||
fi := lstat(t, "testfile")
|
fi := lstat(t, "testfile")
|
||||||
want, err := restic.NodeFromFileInfo("testfile", fi)
|
want, err := restic.NodeFromFileInfo("testfile", fi, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ func wrapFileInfo(fi os.FileInfo) os.FileInfo {
|
||||||
|
|
||||||
func statAndSnapshot(t *testing.T, repo restic.Repository, name string) (*restic.Node, *restic.Node) {
|
func statAndSnapshot(t *testing.T, repo restic.Repository, name string) (*restic.Node, *restic.Node) {
|
||||||
fi := lstat(t, name)
|
fi := lstat(t, name)
|
||||||
want, err := restic.NodeFromFileInfo(name, fi)
|
want, err := restic.NodeFromFileInfo(name, fi, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
_, node := snapshot(t, repo, fs.Local{}, nil, name)
|
_, node := snapshot(t, repo, fs.Local{}, nil, name)
|
||||||
|
|
|
@ -29,7 +29,7 @@ type FileSaver struct {
|
||||||
|
|
||||||
CompleteBlob func(bytes uint64)
|
CompleteBlob func(bytes uint64)
|
||||||
|
|
||||||
NodeFromFileInfo func(snPath, filename string, fi os.FileInfo) (*restic.Node, error)
|
NodeFromFileInfo func(snPath, filename string, fi os.FileInfo, 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
|
||||||
|
@ -156,7 +156,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, f.Name(), fi)
|
node, err := s.NodeFromFileInfo(snPath, f.Name(), fi, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
completeError(err)
|
completeError(err)
|
||||||
|
|
|
@ -49,8 +49,8 @@ func startFileSaver(ctx context.Context, t testing.TB) (*FileSaver, context.Cont
|
||||||
}
|
}
|
||||||
|
|
||||||
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) (*restic.Node, error) {
|
s.NodeFromFileInfo = func(snPath, filename string, fi os.FileInfo, ignoreXattrListError bool) (*restic.Node, error) {
|
||||||
return restic.NodeFromFileInfo(filename, fi)
|
return restic.NodeFromFileInfo(filename, fi, ignoreXattrListError)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, ctx, wg
|
return s, ctx, wg
|
||||||
|
|
|
@ -134,7 +134,7 @@ func (node Node) String() string {
|
||||||
|
|
||||||
// NodeFromFileInfo returns a new node from the given path and FileInfo. It
|
// NodeFromFileInfo returns a new node from the given path and FileInfo. It
|
||||||
// returns the first error that is encountered, together with a node.
|
// returns the first error that is encountered, together with a node.
|
||||||
func NodeFromFileInfo(path string, fi os.FileInfo) (*Node, error) {
|
func NodeFromFileInfo(path string, fi os.FileInfo, ignoreXattrListError bool) (*Node, error) {
|
||||||
mask := os.ModePerm | os.ModeType | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
|
mask := os.ModePerm | os.ModeType | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
|
||||||
node := &Node{
|
node := &Node{
|
||||||
Path: path,
|
Path: path,
|
||||||
|
@ -148,7 +148,7 @@ func NodeFromFileInfo(path string, fi os.FileInfo) (*Node, error) {
|
||||||
node.Size = uint64(fi.Size())
|
node.Size = uint64(fi.Size())
|
||||||
}
|
}
|
||||||
|
|
||||||
err := node.fillExtra(path, fi)
|
err := node.fillExtra(path, fi, ignoreXattrListError)
|
||||||
return node, err
|
return node, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -675,7 +675,7 @@ func lookupGroup(gid uint32) string {
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *Node) fillExtra(path string, fi os.FileInfo) error {
|
func (node *Node) fillExtra(path string, fi os.FileInfo, ignoreXattrListError bool) error {
|
||||||
stat, ok := toStatT(fi.Sys())
|
stat, ok := toStatT(fi.Sys())
|
||||||
if !ok {
|
if !ok {
|
||||||
// fill minimal info with current values for uid, gid
|
// fill minimal info with current values for uid, gid
|
||||||
|
@ -719,7 +719,7 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error {
|
||||||
allowExtended, err := node.fillGenericAttributes(path, fi, stat)
|
allowExtended, err := node.fillGenericAttributes(path, fi, stat)
|
||||||
if allowExtended {
|
if allowExtended {
|
||||||
// Skip processing ExtendedAttributes if allowExtended is false.
|
// Skip processing ExtendedAttributes if allowExtended is false.
|
||||||
errEx := node.fillExtendedAttributes(path)
|
errEx := node.fillExtendedAttributes(path, ignoreXattrListError)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = errEx
|
err = errEx
|
||||||
} else {
|
} else {
|
||||||
|
@ -729,10 +729,13 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *Node) fillExtendedAttributes(path string) error {
|
func (node *Node) fillExtendedAttributes(path string, ignoreListError bool) error {
|
||||||
xattrs, err := Listxattr(path)
|
xattrs, err := Listxattr(path)
|
||||||
debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err)
|
debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if ignoreListError && IsListxattrPermissionError(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,10 @@ func Listxattr(path string) ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsListxattrPermissionError(_ error) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Setxattr is a no-op on AIX.
|
// Setxattr is a no-op on AIX.
|
||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -23,6 +23,10 @@ func Listxattr(path string) ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsListxattrPermissionError(_ error) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Setxattr is a no-op on netbsd.
|
// Setxattr is a no-op on netbsd.
|
||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -23,6 +23,10 @@ func Listxattr(path string) ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsListxattrPermissionError(_ error) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Setxattr is a no-op on openbsd.
|
// Setxattr is a no-op on openbsd.
|
||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/restic/restic/internal/test"
|
"github.com/restic/restic/internal/test"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
@ -31,7 +32,7 @@ func BenchmarkNodeFillUser(t *testing.B) {
|
||||||
t.ResetTimer()
|
t.ResetTimer()
|
||||||
|
|
||||||
for i := 0; i < t.N; i++ {
|
for i := 0; i < t.N; i++ {
|
||||||
_, err := NodeFromFileInfo(path, fi)
|
_, err := NodeFromFileInfo(path, fi, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ func BenchmarkNodeFromFileInfo(t *testing.B) {
|
||||||
t.ResetTimer()
|
t.ResetTimer()
|
||||||
|
|
||||||
for i := 0; i < t.N; i++ {
|
for i := 0; i < t.N; i++ {
|
||||||
_, err := NodeFromFileInfo(path, fi)
|
_, err := NodeFromFileInfo(path, fi, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -227,8 +228,11 @@ func TestNodeRestoreAt(t *testing.T) {
|
||||||
fi, err := os.Lstat(nodePath)
|
fi, err := os.Lstat(nodePath)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
n2, err := NodeFromFileInfo(nodePath, fi)
|
n2, err := NodeFromFileInfo(nodePath, fi, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
n3, err := NodeFromFileInfo(nodePath, fi, true)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
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,
|
||||||
"%v: name doesn't match (%v != %v)", test.Type, test.Name, n2.Name)
|
"%v: name doesn't match (%v != %v)", test.Type, test.Name, n2.Name)
|
||||||
|
|
|
@ -128,7 +128,7 @@ func TestNodeFromFileInfo(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
node, err := NodeFromFileInfo(test.filename, fi)
|
node, err := NodeFromFileInfo(test.filename, fi, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,10 @@ func Listxattr(path string) ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsListxattrPermissionError(_ error) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Setxattr associates name and data together as an attribute of path.
|
// Setxattr associates name and data together as an attribute of path.
|
||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -165,7 +165,7 @@ func restoreAndGetNode(t *testing.T, tempDir string, testNode Node, warningExpec
|
||||||
fi, err := os.Lstat(testPath)
|
fi, err := os.Lstat(testPath)
|
||||||
test.OK(t, errors.Wrapf(err, "Could not Lstat for path: %s", testPath))
|
test.OK(t, errors.Wrapf(err, "Could not Lstat for path: %s", testPath))
|
||||||
|
|
||||||
nodeFromFileInfo, err := NodeFromFileInfo(testPath, fi)
|
nodeFromFileInfo, err := 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))
|
||||||
|
|
||||||
return testPath, nodeFromFileInfo
|
return testPath, nodeFromFileInfo
|
||||||
|
|
|
@ -25,6 +25,14 @@ func Listxattr(path string) ([]string, error) {
|
||||||
return l, handleXattrErr(err)
|
return l, handleXattrErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsListxattrPermissionError(err error) bool {
|
||||||
|
var xerr *xattr.Error
|
||||||
|
if errors.As(err, &xerr) {
|
||||||
|
return xerr.Op == "xattr.list" && errors.Is(xerr.Err, os.ErrPermission)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Setxattr associates name and data together as an attribute of path.
|
// Setxattr associates name and data together as an attribute of path.
|
||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return handleXattrErr(xattr.LSet(path, name, data))
|
return handleXattrErr(xattr.LSet(path, name, data))
|
||||||
|
|
28
internal/restic/node_xattr_test.go
Normal file
28
internal/restic/node_xattr_test.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
//go:build darwin || freebsd || linux || solaris
|
||||||
|
// +build darwin freebsd linux solaris
|
||||||
|
|
||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pkg/xattr"
|
||||||
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsListxattrPermissionError(t *testing.T) {
|
||||||
|
xerr := &xattr.Error{
|
||||||
|
Op: "xattr.list",
|
||||||
|
Name: "test",
|
||||||
|
Err: os.ErrPermission,
|
||||||
|
}
|
||||||
|
err := handleXattrErr(xerr)
|
||||||
|
rtest.Assert(t, err != nil, "missing error")
|
||||||
|
rtest.Assert(t, IsListxattrPermissionError(err), "expected IsListxattrPermissionError to return true for %v", err)
|
||||||
|
|
||||||
|
xerr.Err = os.ErrNotExist
|
||||||
|
err = handleXattrErr(xerr)
|
||||||
|
rtest.Assert(t, err != nil, "missing error")
|
||||||
|
rtest.Assert(t, !IsListxattrPermissionError(err), "expected IsListxattrPermissionError to return false for %v", err)
|
||||||
|
}
|
|
@ -86,7 +86,7 @@ func TestNodeComparison(t *testing.T) {
|
||||||
fi, err := os.Lstat("tree_test.go")
|
fi, err := os.Lstat("tree_test.go")
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
node, err := restic.NodeFromFileInfo("tree_test.go", fi)
|
node, err := restic.NodeFromFileInfo("tree_test.go", fi, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
n2 := *node
|
n2 := *node
|
||||||
|
@ -127,7 +127,7 @@ func TestTreeEqualSerialization(t *testing.T) {
|
||||||
for _, fn := range files[:i] {
|
for _, fn := range files[:i] {
|
||||||
fi, err := os.Lstat(fn)
|
fi, err := os.Lstat(fn)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
node, err := restic.NodeFromFileInfo(fn, fi)
|
node, err := restic.NodeFromFileInfo(fn, fi, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.OK(t, tree.Insert(node))
|
rtest.OK(t, tree.Insert(node))
|
||||||
|
|
Loading…
Reference in a new issue