diff --git a/src/cmds/restic/cmd_backup.go b/src/cmds/restic/cmd_backup.go index 41f6dcaf2..773645693 100644 --- a/src/cmds/restic/cmd_backup.go +++ b/src/cmds/restic/cmd_backup.go @@ -392,7 +392,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { // Find last snapshot to set it as parent, if not already set if !opts.Force && parentSnapshotID == nil { - id, err := restic.FindLatestSnapshot(context.TODO(), repo, target, opts.Tags, opts.Hostname) + id, err := restic.FindLatestSnapshot(context.TODO(), repo, target, []restic.TagList{opts.Tags}, opts.Hostname) if err == nil { parentSnapshotID = &id } else if err != restic.ErrNoSnapshotFound { diff --git a/src/cmds/restic/cmd_find.go b/src/cmds/restic/cmd_find.go index e571cc265..e3b8e2784 100644 --- a/src/cmds/restic/cmd_find.go +++ b/src/cmds/restic/cmd_find.go @@ -50,7 +50,7 @@ func init() { f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") f.StringVarP(&findOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") - f.StringArrayVar(&findOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given") + f.StringArrayVar(&findOptions.Tags, "tag", nil, "only consider snapshots which include the list of `tag,tag,...`, when no snapshot-ID is given") f.StringArrayVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") } @@ -290,13 +290,15 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error { ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() + tagLists := restic.SplitTagLists(opts.Tags) + f := &Finder{ repo: repo, pat: pat, out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON}, notfound: restic.NewIDSet(), } - for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, tagLists, opts.Paths, opts.Snapshots) { if err = f.findInSnapshot(sn); err != nil { return err } diff --git a/src/cmds/restic/cmd_forget.go b/src/cmds/restic/cmd_forget.go index 8a2a98691..e9d96d7fc 100644 --- a/src/cmds/restic/cmd_forget.go +++ b/src/cmds/restic/cmd_forget.go @@ -92,7 +92,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() - for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, restic.SplitTagLists(opts.Tags), opts.Paths, args) { if len(args) > 0 { // When explicit snapshots args are given, remove them immediately. if !opts.DryRun { diff --git a/src/cmds/restic/cmd_ls.go b/src/cmds/restic/cmd_ls.go index 329ff5f70..c6461e950 100644 --- a/src/cmds/restic/cmd_ls.go +++ b/src/cmds/restic/cmd_ls.go @@ -80,7 +80,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() - for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, restic.SplitTagLists(opts.Tags), opts.Paths, args) { Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time) if err = printTree(repo, sn.Tree, string(filepath.Separator)); err != nil { diff --git a/src/cmds/restic/cmd_mount.go b/src/cmds/restic/cmd_mount.go index 323f34c8c..43538ca20 100644 --- a/src/cmds/restic/cmd_mount.go +++ b/src/cmds/restic/cmd_mount.go @@ -6,6 +6,7 @@ package main import ( "context" "os" + "restic" "github.com/spf13/cobra" @@ -103,7 +104,7 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error { cfg := fuse.Config{ OwnerIsRoot: opts.OwnerRoot, Host: opts.Host, - Tags: opts.Tags, + Tags: restic.SplitTagLists(opts.Tags), Paths: opts.Paths, } root, err := fuse.NewRoot(context.TODO(), repo, cfg) diff --git a/src/cmds/restic/cmd_restore.go b/src/cmds/restic/cmd_restore.go index 0d1575f29..4d304ce14 100644 --- a/src/cmds/restic/cmd_restore.go +++ b/src/cmds/restic/cmd_restore.go @@ -89,7 +89,7 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { var id restic.ID if snapshotIDString == "latest" { - id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host) + id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, restic.SplitTagLists(opts.Tags), opts.Host) if err != nil { Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host) } diff --git a/src/cmds/restic/cmd_snapshots.go b/src/cmds/restic/cmd_snapshots.go index 4c0eb8f6e..c69df0810 100644 --- a/src/cmds/restic/cmd_snapshots.go +++ b/src/cmds/restic/cmd_snapshots.go @@ -59,7 +59,7 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro defer cancel() var list restic.Snapshots - for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, restic.SplitTagLists(opts.Tags), opts.Paths, args) { list = append(list, sn) } sort.Sort(sort.Reverse(list)) diff --git a/src/cmds/restic/cmd_tag.go b/src/cmds/restic/cmd_tag.go index 495aed57e..b2f2dec38 100644 --- a/src/cmds/restic/cmd_tag.go +++ b/src/cmds/restic/cmd_tag.go @@ -123,7 +123,7 @@ func runTag(opts TagOptions, gopts GlobalOptions, args []string) error { changeCnt := 0 ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() - for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, restic.SplitTagLists(opts.Tags), opts.Paths, args) { changed, err := changeTags(repo, sn, opts.SetTags, opts.AddTags, opts.RemoveTags) if err != nil { Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err) diff --git a/src/cmds/restic/find.go b/src/cmds/restic/find.go index 7efdc7d8c..4e4b63c59 100644 --- a/src/cmds/restic/find.go +++ b/src/cmds/restic/find.go @@ -8,7 +8,7 @@ import ( ) // FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots. -func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, host string, tags []string, paths []string, snapshotIDs []string) <-chan *restic.Snapshot { +func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, host string, tags []restic.TagList, paths []string, snapshotIDs []string) <-chan *restic.Snapshot { out := make(chan *restic.Snapshot) go func() { defer close(out) diff --git a/src/restic/fuse/root.go b/src/restic/fuse/root.go index b82502c7a..613be0ff9 100644 --- a/src/restic/fuse/root.go +++ b/src/restic/fuse/root.go @@ -16,7 +16,7 @@ import ( type Config struct { OwnerIsRoot bool Host string - Tags []string + Tags []restic.TagList Paths []string } diff --git a/src/restic/snapshot.go b/src/restic/snapshot.go index 150f3b132..9053fa6ec 100644 --- a/src/restic/snapshot.go +++ b/src/restic/snapshot.go @@ -5,6 +5,7 @@ import ( "fmt" "os/user" "path/filepath" + "restic/debug" "time" ) @@ -150,6 +151,25 @@ func (sn *Snapshot) HasTags(l []string) bool { return true } +// HasTagList returns true if the snapshot satisfies at least one TagList, +// so there is a TagList in l for which all tags are included in sn. +func (sn *Snapshot) HasTagList(l []TagList) bool { + debug.Log("testing snapshot with tags %v against list: %v", sn.Tags, l) + + if len(l) == 0 { + return true + } + + for _, tags := range l { + if sn.HasTags(tags) { + debug.Log(" snapshot satisfies %v", tags, l) + return true + } + } + + return false +} + func (sn *Snapshot) hasPath(path string) bool { for _, snPath := range sn.Paths { if path == snPath { diff --git a/src/restic/snapshot_find.go b/src/restic/snapshot_find.go index 406bd2c1b..b55d892c5 100644 --- a/src/restic/snapshot_find.go +++ b/src/restic/snapshot_find.go @@ -12,7 +12,7 @@ import ( var ErrNoSnapshotFound = errors.New("no snapshot found") // FindLatestSnapshot finds latest snapshot with optional target/directory, tags and hostname filters. -func FindLatestSnapshot(ctx context.Context, repo Repository, targets []string, tags []string, hostname string) (ID, error) { +func FindLatestSnapshot(ctx context.Context, repo Repository, targets []string, tagLists []TagList, hostname string) (ID, error) { var ( latest time.Time latestID ID @@ -24,11 +24,21 @@ func FindLatestSnapshot(ctx context.Context, repo Repository, targets []string, if err != nil { return ID{}, errors.Errorf("Error listing snapshot: %v", err) } - if snapshot.Time.After(latest) && (hostname == "" || hostname == snapshot.Hostname) && snapshot.HasTags(tags) && snapshot.HasPaths(targets) { - latest = snapshot.Time - latestID = snapshotID - found = true + if snapshot.Time.Before(latest) || (hostname != "" && hostname != snapshot.Hostname) { + continue } + + if !snapshot.HasTagList(tagLists) { + continue + } + + if !snapshot.HasPaths(targets) { + continue + } + + latest = snapshot.Time + latestID = snapshotID + found = true } if !found { @@ -53,7 +63,7 @@ func FindSnapshot(repo Repository, s string) (ID, error) { // FindFilteredSnapshots yields Snapshots filtered from the list of all // snapshots. -func FindFilteredSnapshots(ctx context.Context, repo Repository, host string, tags []string, paths []string) Snapshots { +func FindFilteredSnapshots(ctx context.Context, repo Repository, host string, tags []TagList, paths []string) Snapshots { results := make(Snapshots, 0, 20) for id := range repo.List(ctx, SnapshotFile) { @@ -62,7 +72,7 @@ func FindFilteredSnapshots(ctx context.Context, repo Repository, host string, ta fmt.Fprintf(os.Stderr, "could not load snapshot %v: %v\n", id.Str(), err) continue } - if (host != "" && host != sn.Hostname) || !sn.HasTags(tags) || !sn.HasPaths(paths) { + if (host != "" && host != sn.Hostname) || !sn.HasTagList(tags) || !sn.HasPaths(paths) { continue } diff --git a/src/restic/snapshot_policy.go b/src/restic/snapshot_policy.go index 5e674fcc0..88ec9ed99 100644 --- a/src/restic/snapshot_policy.go +++ b/src/restic/snapshot_policy.go @@ -20,6 +20,16 @@ func SplitTagList(s string) (l TagList) { return l } +// SplitTagLists splits a slice of strings into a slice of TagLists using +// SplitTagList. +func SplitTagLists(s []string) (l []TagList) { + l = make([]TagList, 0, len(s)) + for _, t := range s { + l = append(l, SplitTagList(t)) + } + return l +} + // ExpirePolicy configures which snapshots should be automatically removed. type ExpirePolicy struct { Last int // keep the last n snapshots