From 054da8510979c0a2ddb757eed42050b354611a79 Mon Sep 17 00:00:00 2001 From: Jim Minter Date: Wed, 11 Mar 2026 20:48:24 -0600 Subject: [PATCH] implement restic backup --exclude-no-dump --- cmd/restic/cmd_backup.go | 12 +++++ internal/archiver/attributes_linux.go | 19 ++++++++ internal/archiver/attributes_other.go | 9 ++++ internal/archiver/exclude.go | 13 ++++++ internal/archiver/exclude_linux_test.go | 60 +++++++++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 internal/archiver/attributes_linux.go create mode 100644 internal/archiver/attributes_other.go create mode 100644 internal/archiver/exclude_linux_test.go diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index f9b45fe51..1ee5aa710 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -85,6 +85,7 @@ type BackupOptions struct { ExcludeCaches bool ExcludeLargerThan string ExcludeCloudFiles bool + ExcludeNoDump bool Stdin bool StdinFilename string StdinCommand bool @@ -143,6 +144,9 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) { if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { f.BoolVar(&opts.ExcludeCloudFiles, "exclude-cloud-files", false, "excludes online-only cloud files (such as OneDrive, iCloud drive, …)") } + if runtime.GOOS == "linux" { + f.BoolVar(&opts.ExcludeNoDump, "exclude-no-dump", false, "excludes directories and files marked with the `no dump` attribute)") + } f.BoolVar(&opts.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot") // parse read concurrency from env, on error the default value will be used @@ -380,6 +384,14 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf fu funcs = append(funcs, f) } + if opts.ExcludeNoDump { + f, err := archiver.RejectByNoDump(warnf) + if err != nil { + return nil, err + } + funcs = append(funcs, f) + } + return funcs, nil } diff --git a/internal/archiver/attributes_linux.go b/internal/archiver/attributes_linux.go new file mode 100644 index 000000000..8e98c4d93 --- /dev/null +++ b/internal/archiver/attributes_linux.go @@ -0,0 +1,19 @@ +//go:build linux + +package archiver + +import ( + "golang.org/x/sys/unix" +) + +// isNoDump returns whether the "no dump" Linux file attribute is set on path. +// See CHATTR(1) for more information about Linux file attributes. +func isNoDump(path string) (bool, error) { + statx := &unix.Statx_t{} + + if err := unix.Statx(0, path, unix.AT_NO_AUTOMOUNT|unix.AT_SYMLINK_NOFOLLOW, 0, statx); err != nil { + return false, err + } + + return statx.Attributes&unix.STATX_ATTR_NODUMP != 0, nil +} diff --git a/internal/archiver/attributes_other.go b/internal/archiver/attributes_other.go new file mode 100644 index 000000000..b4bc49dc0 --- /dev/null +++ b/internal/archiver/attributes_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package archiver + +// isNoDump returns whether the "no dump" Linux file attribute is set on path. +// See CHATTR(1) for more information about Linux file attributes. +func isNoDump(path string) (bool, error) { + return false, nil +} diff --git a/internal/archiver/exclude.go b/internal/archiver/exclude.go index c7dff0acb..b9b37776b 100644 --- a/internal/archiver/exclude.go +++ b/internal/archiver/exclude.go @@ -334,3 +334,16 @@ func RejectCloudFiles(warnf func(msg string, args ...interface{})) (RejectFunc, return false }, nil } + +// RejectByNoDump returns a func which on Linux rejects files with the "no dump" +// Linux file attribute is set. On other OSes it rejects no files. +func RejectByNoDump(warnf func(string, ...any)) (RejectFunc, error) { + return func(item string, _ *fs.ExtendedFileInfo, _ fs.FS) bool { + rv, err := isNoDump(item) + if err != nil { + warnf("item %v: error getting attributes: %v", item, err) + } + + return rv + }, nil +} diff --git a/internal/archiver/exclude_linux_test.go b/internal/archiver/exclude_linux_test.go new file mode 100644 index 000000000..a9af014cc --- /dev/null +++ b/internal/archiver/exclude_linux_test.go @@ -0,0 +1,60 @@ +//go:build linux + +package archiver + +import ( + "os" + "testing" + + "golang.org/x/sys/unix" + + "github.com/restic/restic/internal/test" +) + +func TestRejectByNoDump(t *testing.T) { + tempDir := test.TempDir(t) + + items := []struct { + path string + dir bool + noDump bool + }{ + {"/no-dump", true, true}, + {"/normal", true, false}, + {"/normal/no-dump", false, true}, + {"/normal/normal", false, false}, + } + + for _, item := range items { + if item.dir { + test.OK(t, os.Mkdir(tempDir+item.path, 0700)) + } else { + test.OK(t, os.WriteFile(tempDir+item.path, nil, 0600)) + } + + if item.noDump { + test.OK(t, setNoDump(tempDir+item.path)) + } + } + + reject, err := RejectByNoDump(nil) + test.OK(t, err) + + for _, item := range items { + rejected := reject(tempDir+item.path, nil, nil) + if rejected != item.noDump { + t.Errorf("inclusion status of %s is wrong: want %v, got %v", item.path, item.noDump, rejected) + } + } +} + +// setNoDump sets the "no dump" Linux file attribute on path (file or directory). +func setNoDump(path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + return unix.IoctlSetPointerInt(int(f.Fd()), unix.FS_IOC_SETFLAGS, 0x40 /* FS_NODUMP_FL */) +}