forked from TrueCloudLab/restic
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.
This commit is contained in:
parent
74f7dd0b38
commit
97274ecabd
19 changed files with 126 additions and 99 deletions
8
changelog/unreleased/issue-4211
Normal file
8
changelog/unreleased/issue-4211
Normal file
|
@ -0,0 +1,8 @@
|
|||
Bugfix: Restic dump now interprets --host and --path correctly
|
||||
|
||||
Restic dump previously confused its --host=<host> and --path=<path>
|
||||
options: it looked for snapshots with paths called <host> from hosts
|
||||
called <path>. It now treats the options as intended.
|
||||
|
||||
https://github.com/restic/restic/issues/4211
|
||||
https://github.com/restic/restic/pull/4212
|
|
@ -439,7 +439,13 @@ func findParentSnapshot(ctx context.Context, repo restic.Repository, opts Backup
|
|||
if snName == "" {
|
||||
snName = "latest"
|
||||
}
|
||||
sn, err := restic.FindFilteredSnapshot(ctx, repo.Backend(), repo, []string{opts.Host}, []restic.TagList{}, targets, &timeStampLimit, snName)
|
||||
f := restic.SnapshotFilter{
|
||||
Hosts: []string{opts.Host},
|
||||
Paths: targets,
|
||||
TimestampLimit: timeStampLimit,
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue