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()) } }