From 0d6e00811448c9fc49e40af483b7cf49e8a6aefc Mon Sep 17 00:00:00 2001 From: Tesshu Flower Date: Mon, 4 Nov 2024 02:14:45 -0500 Subject: [PATCH] restore: exclude/include xattrs For: https://github.com/restic/restic/issues/5089 Signed-off-by: Tesshu Flower --- cmd/restic/cmd_restore.go | 48 ++++++++++++++++++++++++++---- internal/fs/node.go | 8 ++--- internal/fs/node_test.go | 6 ++-- internal/fs/node_xattr.go | 20 ++++++++----- internal/fs/node_xattr_all_test.go | 2 +- internal/restorer/restorer.go | 4 ++- 6 files changed, 68 insertions(+), 20 deletions(-) diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 82dd408a8..fc3148ce1 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -54,11 +54,13 @@ type RestoreOptions struct { filter.IncludePatternOptions Target string restic.SnapshotFilter - DryRun bool - Sparse bool - Verify bool - Overwrite restorer.OverwriteBehavior - Delete bool + DryRun bool + Sparse bool + Verify bool + Overwrite restorer.OverwriteBehavior + Delete bool + ExcludeXattrPattern []string + IncludeXattrPattern []string } var restoreOptions RestoreOptions @@ -72,6 +74,9 @@ func init() { restoreOptions.ExcludePatternOptions.Add(flags) restoreOptions.IncludePatternOptions.Add(flags) + flags.StringArrayVar(&restoreOptions.ExcludeXattrPattern, "exclude-xattr", nil, "exclude xattr by `pattern` (can be specified multiple times)") + flags.StringArrayVar(&restoreOptions.IncludeXattrPattern, "include-xattr", nil, "include xattr by `pattern` (can be specified multiple times)") + initSingleSnapshotFilter(flags, &restoreOptions.SnapshotFilter) flags.BoolVar(&restoreOptions.DryRun, "dry-run", false, "do not write any data, just show what would be done") flags.BoolVar(&restoreOptions.Sparse, "sparse", false, "restore files as sparse") @@ -96,6 +101,9 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, hasExcludes := len(excludePatternFns) > 0 hasIncludes := len(includePatternFns) > 0 + hasXattrExcludes := len(opts.ExcludeXattrPattern) > 0 + hasXattrIncludes := len(opts.IncludeXattrPattern) > 0 + switch { case len(args) == 0: return errors.Fatal("no snapshot ID specified") @@ -110,6 +118,11 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, if hasExcludes && hasIncludes { return errors.Fatal("exclude and include patterns are mutually exclusive") } + + if hasXattrExcludes && hasXattrIncludes { + return errors.Fatal("exclude and include xattr patterns are mutually exclusive") + } + if opts.DryRun && opts.Verify { return errors.Fatal("--dry-run and --verify are mutually exclusive") } @@ -219,6 +232,31 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, res.SelectFilter = selectIncludeFilter } + if !hasXattrExcludes && !hasXattrIncludes { + // set default of including xattrs from the 'user' namespace + opts.IncludeXattrPattern = []string{"user.*"} + } + if hasXattrExcludes { + if err := filter.ValidatePatterns(opts.ExcludeXattrPattern); err != nil { + return errors.Fatalf("--exclude-xattr: %s", err) + } + + res.XattrSelectFilter = func(xattrName string) bool { + shouldReject := filter.RejectByPattern(opts.ExcludeXattrPattern, Warnf)(xattrName) + return !shouldReject + } + } else { + // User has either input include xattr pattern(s) or we're using our default include pattern + if err := filter.ValidatePatterns(opts.IncludeXattrPattern); err != nil { + return errors.Fatalf("--include-xattr: %s", err) + } + + res.XattrSelectFilter = func(xattrName string) bool { + shouldInclude, _ := filter.IncludeByPattern(opts.IncludeXattrPattern, Warnf)(xattrName) + return shouldInclude + } + } + if !gopts.JSON { msg.P("restoring %s to %s\n", res.Snapshot(), opts.Target) } diff --git a/internal/fs/node.go b/internal/fs/node.go index 058d9cc7b..ab2aca957 100644 --- a/internal/fs/node.go +++ b/internal/fs/node.go @@ -230,8 +230,8 @@ func mkfifo(path string, mode uint32) (err error) { } // NodeRestoreMetadata restores node metadata -func NodeRestoreMetadata(node *restic.Node, path string, warn func(msg string)) error { - err := nodeRestoreMetadata(node, path, warn) +func NodeRestoreMetadata(node *restic.Node, path string, warn func(msg string), xattrSelectFilter func(xattrName string) bool) error { + err := nodeRestoreMetadata(node, path, warn, xattrSelectFilter) if err != nil { // It is common to have permission errors for folders like /home // unless you're running as root, so ignore those. @@ -246,14 +246,14 @@ func NodeRestoreMetadata(node *restic.Node, path string, warn func(msg string)) return err } -func nodeRestoreMetadata(node *restic.Node, path string, warn func(msg string)) error { +func nodeRestoreMetadata(node *restic.Node, path string, warn func(msg string), xattrSelectFilter func(xattrName string) bool) error { var firsterr error if err := lchown(path, int(node.UID), int(node.GID)); err != nil { firsterr = errors.WithStack(err) } - if err := nodeRestoreExtendedAttributes(node, path); err != nil { + if err := nodeRestoreExtendedAttributes(node, path, xattrSelectFilter); err != nil { debug.Log("error restoring extended attributes for %v: %v", path, err) if firsterr == nil { firsterr = err diff --git a/internal/fs/node_test.go b/internal/fs/node_test.go index 65098e304..b67295f68 100644 --- a/internal/fs/node_test.go +++ b/internal/fs/node_test.go @@ -217,7 +217,8 @@ func TestNodeRestoreAt(t *testing.T) { nodePath = filepath.Join(tempdir, test.Name) } 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)) }, + func(_ string) bool { return true } /* restore all xattrs */)) fs := &Local{} meta, err := fs.OpenFile(nodePath, O_NOFOLLOW, true) @@ -292,6 +293,7 @@ func TestNodeRestoreMetadataError(t *testing.T) { nodePath := filepath.Join(tempdir, node.Name) // This will fail because the target file does not exist - err := NodeRestoreMetadata(node, nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) }) + err := NodeRestoreMetadata(node, nodePath, func(msg string) { rtest.OK(t, fmt.Errorf("Warning triggered for path: %s: %s", nodePath, msg)) }, + func(_ string) bool { return true }) test.Assert(t, errors.Is(err, os.ErrNotExist), "failed for an unexpected reason") } diff --git a/internal/fs/node_xattr.go b/internal/fs/node_xattr.go index e1ddf9826..11f75893b 100644 --- a/internal/fs/node_xattr.go +++ b/internal/fs/node_xattr.go @@ -65,14 +65,17 @@ func handleXattrErr(err error) error { } } -func nodeRestoreExtendedAttributes(node *restic.Node, path string) error { +func nodeRestoreExtendedAttributes(node *restic.Node, path string, xattrSelectFilter func(xattrName string) bool) error { expectedAttrs := map[string]struct{}{} for _, attr := range node.ExtendedAttributes { - err := setxattr(path, attr.Name, attr.Value) - if err != nil { - return err + // Only restore xattrs that match the filter + if xattrSelectFilter(attr.Name) { + err := setxattr(path, attr.Name, attr.Value) + if err != nil { + return err + } + expectedAttrs[attr.Name] = struct{}{} } - expectedAttrs[attr.Name] = struct{}{} } // remove unexpected xattrs @@ -84,8 +87,11 @@ func nodeRestoreExtendedAttributes(node *restic.Node, path string) error { if _, ok := expectedAttrs[name]; ok { continue } - if err := removexattr(path, name); err != nil { - return err + // Only attempt to remove xattrs that match the filter + if xattrSelectFilter(name) { + if err := removexattr(path, name); err != nil { + return err + } } } diff --git a/internal/fs/node_xattr_all_test.go b/internal/fs/node_xattr_all_test.go index 690302f70..d71773e0a 100644 --- a/internal/fs/node_xattr_all_test.go +++ b/internal/fs/node_xattr_all_test.go @@ -26,7 +26,7 @@ func setAndVerifyXattr(t *testing.T, file string, attrs []restic.ExtendedAttribu Type: restic.NodeTypeFile, ExtendedAttributes: attrs, } - rtest.OK(t, nodeRestoreExtendedAttributes(node, file)) + rtest.OK(t, nodeRestoreExtendedAttributes(node, file, func(_ string) bool { return true } /*restore all xattrs*/)) nodeActual := &restic.Node{ Type: restic.NodeTypeFile, diff --git a/internal/restorer/restorer.go b/internal/restorer/restorer.go index 14a8edeac..536958d4f 100644 --- a/internal/restorer/restorer.go +++ b/internal/restorer/restorer.go @@ -31,6 +31,8 @@ type Restorer struct { // SelectFilter determines whether the item is selectedForRestore or whether a childMayBeSelected. // selectedForRestore must not depend on isDir as `removeUnexpectedFiles` always passes false to isDir. SelectFilter func(item string, isDir bool) (selectedForRestore bool, childMayBeSelected bool) + + XattrSelectFilter func(xattrName string) (xattrSelectedForRestore bool) } var restorerAbortOnAllErrors = func(_ string, err error) error { return err } @@ -288,7 +290,7 @@ func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location s return nil } debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location) - err := fs.NodeRestoreMetadata(node, target, res.Warn) + err := fs.NodeRestoreMetadata(node, target, res.Warn, res.XattrSelectFilter) if err != nil { debug.Log("node.RestoreMetadata(%s) error %v", target, err) }