From a7786c67f1a9b534c74c0f13aab77e0d71160caf Mon Sep 17 00:00:00 2001 From: greatroar <61184462+greatroar@users.noreply.github.com> Date: Fri, 17 Feb 2023 16:13:46 +0100 Subject: [PATCH] cmd, restic: Refactor and fix snapshot filtering This turns snapshotFilterOptions from cmd into a restic.SnapshotFilter type and makes restic.FindFilteredSnapshot and FindFilteredSnapshots methods on that type. This fixes #4211 by ensuring that hosts and paths are named struct fields instead of unnamed function arguments in long lists of such. Timestamp limits are also included in the new type. To avoid too much pointer handling, the convention is that time zero means no limit. That's January 1st, year 1, 00:00 UTC, which is so unlikely a date that we can sacrifice it for simpler code. --- changelog/unreleased/issue-4211 | 8 ++++ cmd/restic/cmd_backup.go | 13 ++--- cmd/restic/cmd_copy.go | 9 ++-- cmd/restic/cmd_dump.go | 10 ++-- cmd/restic/cmd_find.go | 6 +-- cmd/restic/cmd_forget.go | 6 +-- cmd/restic/cmd_ls.go | 10 ++-- cmd/restic/cmd_mount.go | 8 ++-- cmd/restic/cmd_restore.go | 10 ++-- cmd/restic/cmd_rewrite.go | 6 +-- cmd/restic/cmd_snapshots.go | 6 +-- cmd/restic/cmd_stats.go | 6 +-- cmd/restic/cmd_tag.go | 6 +-- cmd/restic/find.go | 30 +++++------- cmd/restic/integration_test.go | 4 +- internal/fuse/root.go | 4 +- internal/fuse/snapshots_dirstruct.go | 2 +- internal/restic/snapshot_find.go | 68 +++++++++++++++------------ internal/restic/snapshot_find_test.go | 18 +++---- 19 files changed, 124 insertions(+), 106 deletions(-) create mode 100644 changelog/unreleased/issue-4211 diff --git a/changelog/unreleased/issue-4211 b/changelog/unreleased/issue-4211 new file mode 100644 index 000000000..45b7aee83 --- /dev/null +++ b/changelog/unreleased/issue-4211 @@ -0,0 +1,8 @@ +Bugfix: Restic dump now interprets --host and --path correctly + +Restic dump previously confused its --host= and --path= +options: it looked for snapshots with paths called from hosts +called . It now treats the options as intended. + +https://github.com/restic/restic/issues/4211 +https://github.com/restic/restic/pull/4212 diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 7c58f95c4..1244e2ed1 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -442,21 +442,18 @@ func findParentSnapshot(ctx context.Context, repo restic.Repository, opts Backup if snName == "" { snName = "latest" } - - var hosts []string - var paths []string - var tags []restic.TagList + f := restic.SnapshotFilter{TimestampLimit: timeStampLimit} if opts.GroupBy.Host { - hosts = []string{opts.Host} + f.Hosts = []string{opts.Host} } if opts.GroupBy.Path { - paths = targets + f.Paths = targets } if opts.GroupBy.Tag { - tags = []restic.TagList{opts.Tags.Flatten()} + f.Tags = []restic.TagList{opts.Tags.Flatten()} } - sn, err := restic.FindFilteredSnapshot(ctx, repo.Backend(), repo, hosts, tags, paths, &timeStampLimit, snName) + sn, err := f.FindLatest(ctx, repo.Backend(), repo, snName) // Snapshot not found is ok if no explicit parent was set if opts.Parent == "" && errors.Is(err, restic.ErrNoSnapshotFound) { err = nil diff --git a/cmd/restic/cmd_copy.go b/cmd/restic/cmd_copy.go index 14ab1917a..2f095972a 100644 --- a/cmd/restic/cmd_copy.go +++ b/cmd/restic/cmd_copy.go @@ -39,7 +39,7 @@ new destination repository using the "init" command. // CopyOptions bundles all options for the copy command. type CopyOptions struct { secondaryRepoOptions - snapshotFilterOptions + restic.SnapshotFilter } var copyOptions CopyOptions @@ -49,7 +49,7 @@ func init() { f := cmdCopy.Flags() initSecondaryRepoOptions(f, ©Options.secondaryRepoOptions, "destination", "to copy snapshots from") - initMultiSnapshotFilterOptions(f, ©Options.snapshotFilterOptions, true) + initMultiSnapshotFilter(f, ©Options.SnapshotFilter, true) } func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error { @@ -108,7 +108,7 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] } dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot) - for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, opts.Hosts, opts.Tags, opts.Paths, nil) { + for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil) { if sn.Original != nil && !sn.Original.IsNull() { dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn) } @@ -119,8 +119,7 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args [] // remember already processed trees across all snapshots visitedTrees := restic.NewIDSet() - for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, opts.Hosts, opts.Tags, opts.Paths, args) { - + for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args) { // check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields srcOriginal := *sn.ID() if sn.Original != nil { diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index a480b12f4..cda7b65b9 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -40,7 +40,7 @@ Exit status is 0 if the command was successful, and non-zero if there was any er // DumpOptions collects all options for the dump command. type DumpOptions struct { - snapshotFilterOptions + restic.SnapshotFilter Archive string } @@ -50,7 +50,7 @@ func init() { cmdRoot.AddCommand(cmdDump) flags := cmdDump.Flags() - initSingleSnapshotFilterOptions(flags, &dumpOptions.snapshotFilterOptions) + initSingleSnapshotFilter(flags, &dumpOptions.SnapshotFilter) flags.StringVarP(&dumpOptions.Archive, "archive", "a", "tar", "set archive `format` as \"tar\" or \"zip\"") } @@ -139,7 +139,11 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] } } - sn, err := restic.FindFilteredSnapshot(ctx, repo.Backend(), repo, opts.Paths, opts.Tags, opts.Hosts, nil, snapshotIDString) + sn, err := (&restic.SnapshotFilter{ + Hosts: opts.Hosts, + Paths: opts.Paths, + Tags: opts.Tags, + }).FindLatest(ctx, repo.Backend(), repo, snapshotIDString) if err != nil { return errors.Fatalf("failed to find snapshot: %v", err) } diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index 8e5f9b604..e5457c3be 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -51,7 +51,7 @@ type FindOptions struct { PackID, ShowPackID bool CaseInsensitive bool ListLong bool - snapshotFilterOptions + restic.SnapshotFilter } var findOptions FindOptions @@ -70,7 +70,7 @@ func init() { f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern") f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") - initMultiSnapshotFilterOptions(f, &findOptions.snapshotFilterOptions, true) + initMultiSnapshotFilter(f, &findOptions.SnapshotFilter, true) } type findPattern struct { @@ -618,7 +618,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args [] } } - for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, opts.Hosts, opts.Tags, opts.Paths, opts.Snapshots) { + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots) { if f.blobIDs != nil || f.treeIDs != nil { if err = f.findIDs(ctx, sn); err != nil && err.Error() != "OK" { return err diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index cc4923936..fbe4c1c8a 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -52,7 +52,7 @@ type ForgetOptions struct { WithinYearly restic.Duration KeepTags restic.TagLists - snapshotFilterOptions + restic.SnapshotFilter Compact bool // Grouping @@ -81,7 +81,7 @@ func init() { f.VarP(&forgetOptions.WithinYearly, "keep-within-yearly", "", "keep yearly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot") f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)") - initMultiSnapshotFilterOptions(f, &forgetOptions.snapshotFilterOptions, false) + initMultiSnapshotFilter(f, &forgetOptions.SnapshotFilter, false) f.StringArrayVar(&forgetOptions.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)") err := f.MarkDeprecated("hostname", "use --host") if err != nil { @@ -126,7 +126,7 @@ func runForget(ctx context.Context, opts ForgetOptions, gopts GlobalOptions, arg var snapshots restic.Snapshots removeSnIDs := restic.NewIDSet() - for sn := range FindFilteredSnapshots(ctx, repo.Backend(), repo, opts.Hosts, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo.Backend(), repo, &opts.SnapshotFilter, args) { snapshots = append(snapshots, sn) } diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 7dd41ab21..aeaa750eb 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -49,7 +49,7 @@ Exit status is 0 if the command was successful, and non-zero if there was any er // LsOptions collects all options for the ls command. type LsOptions struct { ListLong bool - snapshotFilterOptions + restic.SnapshotFilter Recursive bool } @@ -59,7 +59,7 @@ func init() { cmdRoot.AddCommand(cmdLs) flags := cmdLs.Flags() - initSingleSnapshotFilterOptions(flags, &lsOptions.snapshotFilterOptions) + initSingleSnapshotFilter(flags, &lsOptions.SnapshotFilter) flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories") } @@ -210,7 +210,11 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri } } - sn, err := restic.FindFilteredSnapshot(ctx, snapshotLister, repo, opts.Hosts, opts.Tags, opts.Paths, nil, args[0]) + sn, err := (&restic.SnapshotFilter{ + Hosts: opts.Hosts, + Paths: opts.Paths, + Tags: opts.Tags, + }).FindLatest(ctx, snapshotLister, repo, args[0]) if err != nil { return err } diff --git a/cmd/restic/cmd_mount.go b/cmd/restic/cmd_mount.go index 7afb30f7c..0501bfe89 100644 --- a/cmd/restic/cmd_mount.go +++ b/cmd/restic/cmd_mount.go @@ -77,7 +77,7 @@ type MountOptions struct { OwnerRoot bool AllowOther bool NoDefaultPermissions bool - snapshotFilterOptions + restic.SnapshotFilter TimeTemplate string PathTemplates []string } @@ -92,7 +92,7 @@ func init() { mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory") mountFlags.BoolVar(&mountOptions.NoDefaultPermissions, "no-default-permissions", false, "for 'allow-other', ignore Unix permissions and allow users to read all snapshot files") - initMultiSnapshotFilterOptions(mountFlags, &mountOptions.snapshotFilterOptions, true) + initMultiSnapshotFilter(mountFlags, &mountOptions.SnapshotFilter, true) mountFlags.StringArrayVar(&mountOptions.PathTemplates, "path-template", nil, "set `template` for path names (can be specified multiple times)") mountFlags.StringVar(&mountOptions.TimeTemplate, "snapshot-template", time.RFC3339, "set `template` to use for snapshot dirs") @@ -180,9 +180,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args cfg := fuse.Config{ OwnerIsRoot: opts.OwnerRoot, - Hosts: opts.Hosts, - Tags: opts.Tags, - Paths: opts.Paths, + Filter: opts.SnapshotFilter, TimeTemplate: opts.TimeTemplate, PathTemplates: opts.PathTemplates, } diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index b70cb52ff..579711662 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -42,7 +42,7 @@ type RestoreOptions struct { Include []string InsensitiveInclude []string Target string - snapshotFilterOptions + restic.SnapshotFilter Sparse bool Verify bool } @@ -59,7 +59,7 @@ func init() { flags.StringArrayVar(&restoreOptions.InsensitiveInclude, "iinclude", nil, "same as `--include` but ignores the casing of filenames") flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to") - initSingleSnapshotFilterOptions(flags, &restoreOptions.snapshotFilterOptions) + initSingleSnapshotFilter(flags, &restoreOptions.SnapshotFilter) flags.BoolVar(&restoreOptions.Sparse, "sparse", false, "restore files as sparse") flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content") } @@ -131,7 +131,11 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, a } } - sn, err := restic.FindFilteredSnapshot(ctx, repo.Backend(), repo, opts.Hosts, opts.Tags, opts.Paths, nil, snapshotIDString) + sn, err := (&restic.SnapshotFilter{ + Hosts: opts.Hosts, + Paths: opts.Paths, + Tags: opts.Tags, + }).FindLatest(ctx, repo.Backend(), repo, snapshotIDString) if err != nil { return errors.Fatalf("failed to find snapshot: %v", err) } diff --git a/cmd/restic/cmd_rewrite.go b/cmd/restic/cmd_rewrite.go index cfe56db87..0d9aa1c8c 100644 --- a/cmd/restic/cmd_rewrite.go +++ b/cmd/restic/cmd_rewrite.go @@ -51,7 +51,7 @@ type RewriteOptions struct { Forget bool DryRun bool - snapshotFilterOptions + restic.SnapshotFilter excludePatternOptions } @@ -64,7 +64,7 @@ func init() { f.BoolVarP(&rewriteOptions.Forget, "forget", "", false, "remove original snapshots after creating new ones") f.BoolVarP(&rewriteOptions.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done") - initMultiSnapshotFilterOptions(f, &rewriteOptions.snapshotFilterOptions, true) + initMultiSnapshotFilter(f, &rewriteOptions.SnapshotFilter, true) initExcludePatternOptions(f, &rewriteOptions.excludePatternOptions) } @@ -186,7 +186,7 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a } changedCount := 0 - for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, opts.Hosts, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) { Verbosef("\nsnapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time) changed, err := rewriteSnapshot(ctx, repo, sn, opts) if err != nil { diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 408018736..2de8801cb 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -32,7 +32,7 @@ Exit status is 0 if the command was successful, and non-zero if there was any er // SnapshotOptions bundles all options for the snapshots command. type SnapshotOptions struct { - snapshotFilterOptions + restic.SnapshotFilter Compact bool Last bool // This option should be removed in favour of Latest. Latest int @@ -45,7 +45,7 @@ func init() { cmdRoot.AddCommand(cmdSnapshots) f := cmdSnapshots.Flags() - initMultiSnapshotFilterOptions(f, &snapshotOptions.snapshotFilterOptions, true) + initMultiSnapshotFilter(f, &snapshotOptions.SnapshotFilter, true) f.BoolVarP(&snapshotOptions.Compact, "compact", "c", false, "use compact output format") f.BoolVar(&snapshotOptions.Last, "last", false, "only show the last snapshot for each host and path") err := f.MarkDeprecated("last", "use --latest 1") @@ -73,7 +73,7 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions } var snapshots restic.Snapshots - for sn := range FindFilteredSnapshots(ctx, repo.Backend(), repo, opts.Hosts, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo.Backend(), repo, &opts.SnapshotFilter, args) { snapshots = append(snapshots, sn) } snapshotGroups, grouped, err := restic.GroupSnapshots(snapshots, opts.GroupBy) diff --git a/cmd/restic/cmd_stats.go b/cmd/restic/cmd_stats.go index 99d16b932..55ba6f254 100644 --- a/cmd/restic/cmd_stats.go +++ b/cmd/restic/cmd_stats.go @@ -58,7 +58,7 @@ type StatsOptions struct { // the mode of counting to perform (see consts for available modes) countMode string - snapshotFilterOptions + restic.SnapshotFilter } var statsOptions StatsOptions @@ -67,7 +67,7 @@ func init() { cmdRoot.AddCommand(cmdStats) f := cmdStats.Flags() f.StringVar(&statsOptions.countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file or raw-data") - initMultiSnapshotFilterOptions(f, &statsOptions.snapshotFilterOptions, true) + initMultiSnapshotFilter(f, &statsOptions.SnapshotFilter, true) } func runStats(ctx context.Context, gopts GlobalOptions, args []string) error { @@ -111,7 +111,7 @@ func runStats(ctx context.Context, gopts GlobalOptions, args []string) error { SnapshotsCount: 0, } - for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, statsOptions.Hosts, statsOptions.Tags, statsOptions.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &statsOptions.SnapshotFilter, args) { err = statsWalkSnapshot(ctx, sn, repo, stats) if err != nil { return fmt.Errorf("error walking snapshot: %v", err) diff --git a/cmd/restic/cmd_tag.go b/cmd/restic/cmd_tag.go index 222ddd04a..e5948ea02 100644 --- a/cmd/restic/cmd_tag.go +++ b/cmd/restic/cmd_tag.go @@ -35,7 +35,7 @@ Exit status is 0 if the command was successful, and non-zero if there was any er // TagOptions bundles all options for the 'tag' command. type TagOptions struct { - snapshotFilterOptions + restic.SnapshotFilter SetTags restic.TagLists AddTags restic.TagLists RemoveTags restic.TagLists @@ -50,7 +50,7 @@ func init() { tagFlags.Var(&tagOptions.SetTags, "set", "`tags` which will replace the existing tags in the format `tag[,tag,...]` (can be given multiple times)") tagFlags.Var(&tagOptions.AddTags, "add", "`tags` which will be added to the existing tags in the format `tag[,tag,...]` (can be given multiple times)") tagFlags.Var(&tagOptions.RemoveTags, "remove", "`tags` which will be removed from the existing tags in the format `tag[,tag,...]` (can be given multiple times)") - initMultiSnapshotFilterOptions(tagFlags, &tagOptions.snapshotFilterOptions, true) + initMultiSnapshotFilter(tagFlags, &tagOptions.SnapshotFilter, true) } func changeTags(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, setTags, addTags, removeTags []string) (bool, error) { @@ -119,7 +119,7 @@ func runTag(ctx context.Context, opts TagOptions, gopts GlobalOptions, args []st } changeCnt := 0 - for sn := range FindFilteredSnapshots(ctx, repo.Backend(), repo, opts.Hosts, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo.Backend(), repo, &opts.SnapshotFilter, args) { changed, err := changeTags(ctx, repo, sn, opts.SetTags.Flatten(), opts.AddTags.Flatten(), opts.RemoveTags.Flatten()) if err != nil { Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err) diff --git a/cmd/restic/find.go b/cmd/restic/find.go index 7b488c7aa..54d3563b1 100644 --- a/cmd/restic/find.go +++ b/cmd/restic/find.go @@ -8,34 +8,28 @@ import ( "github.com/spf13/pflag" ) -type snapshotFilterOptions struct { - Hosts []string - Tags restic.TagLists - Paths []string -} - -// initMultiSnapshotFilterOptions is used for commands that work on multiple snapshots +// initMultiSnapshotFilter is used for commands that work on multiple snapshots // MUST be combined with restic.FindFilteredSnapshots or FindFilteredSnapshots -func initMultiSnapshotFilterOptions(flags *pflag.FlagSet, options *snapshotFilterOptions, addHostShorthand bool) { +func initMultiSnapshotFilter(flags *pflag.FlagSet, filt *restic.SnapshotFilter, addHostShorthand bool) { hostShorthand := "H" if !addHostShorthand { hostShorthand = "" } - flags.StringArrayVarP(&options.Hosts, "host", hostShorthand, nil, "only consider snapshots for this `host` (can be specified multiple times)") - flags.Var(&options.Tags, "tag", "only consider snapshots including `tag[,tag,...]` (can be specified multiple times)") - flags.StringArrayVar(&options.Paths, "path", nil, "only consider snapshots including this (absolute) `path` (can be specified multiple times)") + flags.StringArrayVarP(&filt.Hosts, "host", hostShorthand, nil, "only consider snapshots for this `host` (can be specified multiple times)") + flags.Var(&filt.Tags, "tag", "only consider snapshots including `tag[,tag,...]` (can be specified multiple times)") + flags.StringArrayVar(&filt.Paths, "path", nil, "only consider snapshots including this (absolute) `path` (can be specified multiple times)") } -// initSingleSnapshotFilterOptions is used for commands that work on a single snapshot +// initSingleSnapshotFilter is used for commands that work on a single snapshot // MUST be combined with restic.FindFilteredSnapshot -func initSingleSnapshotFilterOptions(flags *pflag.FlagSet, options *snapshotFilterOptions) { - flags.StringArrayVarP(&options.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when snapshot ID \"latest\" is given (can be specified multiple times)") - flags.Var(&options.Tags, "tag", "only consider snapshots including `tag[,tag,...]`, when snapshot ID \"latest\" is given (can be specified multiple times)") - flags.StringArrayVar(&options.Paths, "path", nil, "only consider snapshots including this (absolute) `path`, when snapshot ID \"latest\" is given (can be specified multiple times)") +func initSingleSnapshotFilter(flags *pflag.FlagSet, filt *restic.SnapshotFilter) { + flags.StringArrayVarP(&filt.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when snapshot ID \"latest\" is given (can be specified multiple times)") + flags.Var(&filt.Tags, "tag", "only consider snapshots including `tag[,tag,...]`, when snapshot ID \"latest\" is given (can be specified multiple times)") + flags.StringArrayVar(&filt.Paths, "path", nil, "only consider snapshots including this (absolute) `path`, when snapshot ID \"latest\" is given (can be specified multiple times)") } // FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots. -func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, hosts []string, tags []restic.TagList, paths []string, snapshotIDs []string) <-chan *restic.Snapshot { +func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, f *restic.SnapshotFilter, snapshotIDs []string) <-chan *restic.Snapshot { out := make(chan *restic.Snapshot) go func() { defer close(out) @@ -45,7 +39,7 @@ func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic. return } - err = restic.FindFilteredSnapshots(ctx, be, loader, hosts, tags, paths, snapshotIDs, func(id string, sn *restic.Snapshot, err error) error { + err = f.FindAll(ctx, be, loader, snapshotIDs, func(id string, sn *restic.Snapshot, err error) error { if err != nil { Warnf("Ignoring %q: %v\n", id, err) } else { diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 062a5954c..c87722f02 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -106,7 +106,7 @@ func testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID res func testRunRestoreLatest(t testing.TB, gopts GlobalOptions, dir string, paths []string, hosts []string) { opts := RestoreOptions{ Target: dir, - snapshotFilterOptions: snapshotFilterOptions{ + SnapshotFilter: restic.SnapshotFilter{ Hosts: hosts, Paths: paths, }, @@ -2196,7 +2196,7 @@ func TestFindListOnce(t *testing.T) { snapshotIDs := restic.NewIDSet() // specify the two oldest snapshots explicitly and use "latest" to reference the newest one - for sn := range FindFilteredSnapshots(context.TODO(), repo.Backend(), repo, nil, nil, nil, []string{ + for sn := range FindFilteredSnapshots(context.TODO(), repo.Backend(), repo, &restic.SnapshotFilter{}, []string{ secondSnapshot[0].String(), secondSnapshot[1].String()[:8], "latest", diff --git a/internal/fuse/root.go b/internal/fuse/root.go index fc8841964..ab6116f0d 100644 --- a/internal/fuse/root.go +++ b/internal/fuse/root.go @@ -16,9 +16,7 @@ import ( // Config holds settings for the fuse mount. type Config struct { OwnerIsRoot bool - Hosts []string - Tags []restic.TagList - Paths []string + Filter restic.SnapshotFilter TimeTemplate string PathTemplates []string } diff --git a/internal/fuse/snapshots_dirstruct.go b/internal/fuse/snapshots_dirstruct.go index f8e66d076..3080d4de8 100644 --- a/internal/fuse/snapshots_dirstruct.go +++ b/internal/fuse/snapshots_dirstruct.go @@ -295,7 +295,7 @@ func (d *SnapshotsDirStructure) updateSnapshots(ctx context.Context) error { } var snapshots restic.Snapshots - err := restic.FindFilteredSnapshots(ctx, d.root.repo.Backend(), d.root.repo, d.root.cfg.Hosts, d.root.cfg.Tags, d.root.cfg.Paths, nil, func(id string, sn *restic.Snapshot, err error) error { + err := d.root.cfg.Filter.FindAll(ctx, d.root.repo.Backend(), d.root.repo, nil, func(id string, sn *restic.Snapshot, err error) error { if sn != nil { snapshots = append(snapshots, sn) } diff --git a/internal/restic/snapshot_find.go b/internal/restic/snapshot_find.go index 4f8231a7f..4d4bb4957 100644 --- a/internal/restic/snapshot_find.go +++ b/internal/restic/snapshot_find.go @@ -12,13 +12,32 @@ import ( // ErrNoSnapshotFound is returned when no snapshot for the given criteria could be found. var ErrNoSnapshotFound = errors.New("no snapshot found") -// findLatestSnapshot finds latest snapshot with optional target/directory, tags, hostname, and timestamp filters. -func findLatestSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, hosts []string, - tags []TagList, paths []string, timeStampLimit *time.Time) (*Snapshot, error) { +// A SnapshotFilter denotes a set of snapshots based on hosts, tags and paths. +type SnapshotFilter struct { + _ struct{} // Force naming fields in literals. + + Hosts []string + Tags TagLists + Paths []string + // Match snapshots from before this timestamp. Zero for no limit. + TimestampLimit time.Time +} + +func (f *SnapshotFilter) empty() bool { + return len(f.Hosts)+len(f.Tags)+len(f.Paths) == 0 +} + +func (f *SnapshotFilter) matches(sn *Snapshot) bool { + return sn.HasHostname(f.Hosts) && sn.HasTagList(f.Tags) && sn.HasPaths(f.Paths) +} + +// findLatest finds the latest snapshot with optional target/directory, +// tags, hostname, and timestamp filters. +func (f *SnapshotFilter) findLatest(ctx context.Context, be Lister, loader LoaderUnpacked) (*Snapshot, error) { var err error - absTargets := make([]string, 0, len(paths)) - for _, target := range paths { + absTargets := make([]string, 0, len(f.Paths)) + for _, target := range f.Paths { if !filepath.IsAbs(target) { target, err = filepath.Abs(target) if err != nil { @@ -35,7 +54,7 @@ func findLatestSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, h return errors.Errorf("Error loading snapshot %v: %v", id.Str(), err) } - if timeStampLimit != nil && snapshot.Time.After(*timeStampLimit) { + if !f.TimestampLimit.IsZero() && snapshot.Time.After(f.TimestampLimit) { return nil } @@ -43,15 +62,7 @@ func findLatestSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, h return nil } - if !snapshot.HasHostname(hosts) { - return nil - } - - if !snapshot.HasTagList(tags) { - return nil - } - - if !snapshot.HasPaths(absTargets) { + if !f.matches(snapshot) { return nil } @@ -85,12 +96,14 @@ func FindSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, s strin return LoadSnapshot(ctx, loader, id) } -// FindFilteredSnapshot returns either the latests from a filtered list of all snapshots or a snapshot specified by `snapshotID`. -func FindFilteredSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, hosts []string, tags []TagList, paths []string, timeStampLimit *time.Time, snapshotID string) (*Snapshot, error) { +// FindLatest returns either the latest of a filtered list of all snapshots +// or a snapshot specified by `snapshotID`. +func (f *SnapshotFilter) FindLatest(ctx context.Context, be Lister, loader LoaderUnpacked, snapshotID string) (*Snapshot, error) { if snapshotID == "latest" { - sn, err := findLatestSnapshot(ctx, be, loader, hosts, tags, paths, timeStampLimit) + sn, err := f.findLatest(ctx, be, loader) if err == ErrNoSnapshotFound { - err = fmt.Errorf("snapshot filter (Paths:%v Tags:%v Hosts:%v): %w", paths, tags, hosts, err) + err = fmt.Errorf("snapshot filter (Paths:%v Tags:%v Hosts:%v): %w", + f.Paths, f.Tags, f.Hosts, err) } return sn, err } @@ -99,8 +112,8 @@ func FindFilteredSnapshot(ctx context.Context, be Lister, loader LoaderUnpacked, type SnapshotFindCb func(string, *Snapshot, error) error -// FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots. -func FindFilteredSnapshots(ctx context.Context, be Lister, loader LoaderUnpacked, hosts []string, tags []TagList, paths []string, snapshotIDs []string, fn SnapshotFindCb) error { +// FindAll yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots. +func (f *SnapshotFilter) FindAll(ctx context.Context, be Lister, loader LoaderUnpacked, snapshotIDs []string, fn SnapshotFindCb) error { if len(snapshotIDs) != 0 { var err error usedFilter := false @@ -116,9 +129,10 @@ func FindFilteredSnapshots(ctx context.Context, be Lister, loader LoaderUnpacked usedFilter = true - sn, err = findLatestSnapshot(ctx, be, loader, hosts, tags, paths, nil) + sn, err = f.findLatest(ctx, be, loader) if err == ErrNoSnapshotFound { - err = errors.Errorf("no snapshot matched given filter (Paths:%v Tags:%v Hosts:%v)", paths, tags, hosts) + err = errors.Errorf("no snapshot matched given filter (Paths:%v Tags:%v Hosts:%v)", + f.Paths, f.Tags, f.Hosts) } if sn != nil { ids.Insert(*sn.ID()) @@ -141,18 +155,14 @@ func FindFilteredSnapshots(ctx context.Context, be Lister, loader LoaderUnpacked } // Give the user some indication their filters are not used. - if !usedFilter && (len(hosts) != 0 || len(tags) != 0 || len(paths) != 0) { + if !usedFilter && !f.empty() { return fn("filters", nil, errors.Errorf("explicit snapshot ids are given")) } return nil } return ForAllSnapshots(ctx, be, loader, nil, func(id ID, sn *Snapshot, err error) error { - if err != nil { - return fn(id.String(), sn, err) - } - - if !sn.HasHostname(hosts) || !sn.HasTagList(tags) || !sn.HasPaths(paths) { + if err == nil && !f.matches(sn) { return nil } diff --git a/internal/restic/snapshot_find_test.go b/internal/restic/snapshot_find_test.go index 3c587dde1..d098b5224 100644 --- a/internal/restic/snapshot_find_test.go +++ b/internal/restic/snapshot_find_test.go @@ -14,13 +14,14 @@ func TestFindLatestSnapshot(t *testing.T) { restic.TestCreateSnapshot(t, repo, parseTimeUTC("2017-07-07 07:07:07"), 1, 0) latestSnapshot := restic.TestCreateSnapshot(t, repo, parseTimeUTC("2019-09-09 09:09:09"), 1, 0) - sn, err := restic.FindFilteredSnapshot(context.TODO(), repo.Backend(), repo, []string{"foo"}, []restic.TagList{}, []string{}, nil, "latest") + f := restic.SnapshotFilter{Hosts: []string{"foo"}} + sn, err := f.FindLatest(context.TODO(), repo.Backend(), repo, "latest") if err != nil { - t.Fatalf("FindLatestSnapshot returned error: %v", err) + t.Fatalf("FindLatest returned error: %v", err) } if *sn.ID() != *latestSnapshot.ID() { - t.Errorf("FindLatestSnapshot returned wrong snapshot ID: %v", *sn.ID()) + t.Errorf("FindLatest returned wrong snapshot ID: %v", *sn.ID()) } } @@ -30,14 +31,15 @@ func TestFindLatestSnapshotWithMaxTimestamp(t *testing.T) { desiredSnapshot := restic.TestCreateSnapshot(t, repo, parseTimeUTC("2017-07-07 07:07:07"), 1, 0) restic.TestCreateSnapshot(t, repo, parseTimeUTC("2019-09-09 09:09:09"), 1, 0) - maxTimestamp := parseTimeUTC("2018-08-08 08:08:08") - - sn, err := restic.FindFilteredSnapshot(context.TODO(), repo.Backend(), repo, []string{"foo"}, []restic.TagList{}, []string{}, &maxTimestamp, "latest") + sn, err := (&restic.SnapshotFilter{ + Hosts: []string{"foo"}, + TimestampLimit: parseTimeUTC("2018-08-08 08:08:08"), + }).FindLatest(context.TODO(), repo.Backend(), repo, "latest") if err != nil { - t.Fatalf("FindLatestSnapshot returned error: %v", err) + t.Fatalf("FindLatest returned error: %v", err) } if *sn.ID() != *desiredSnapshot.ID() { - t.Errorf("FindLatestSnapshot returned wrong snapshot ID: %v", *sn.ID()) + t.Errorf("FindLatest returned wrong snapshot ID: %v", *sn.ID()) } }