From c554cdac4c93e5315cee5171843e8cfc5251385d Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Fri, 7 Jul 2017 03:19:06 +0200 Subject: [PATCH 1/8] Update HasTags() and HasPaths() to follow #1081 idea Replace all but 3 occurences of StringSliceVar to StringArrayVar. This will prevent the flag parser to interpretate the given values as CSV string. Both --tag, --keep-tag and --path can be given multiple times, the command will match snapshots matching ANY of the tags/paths. Only when a value is given which contains a comma separated list of tags/paths, ALL elements need to match. --- src/cmds/restic/cmd_backup.go | 6 +- src/cmds/restic/cmd_find.go | 6 +- src/cmds/restic/cmd_forget.go | 6 +- src/cmds/restic/cmd_ls.go | 4 +- src/cmds/restic/cmd_mount.go | 4 +- src/cmds/restic/cmd_restore.go | 8 +- src/cmds/restic/cmd_snapshots.go | 4 +- src/cmds/restic/cmd_tag.go | 4 +- src/restic/snapshot.go | 53 +++++-- ...filter_test.go => snapshot_policy_test.go} | 1 + src/restic/testdata/policy_keep_snapshots_20 | 131 ++++++++++++++++++ 11 files changed, 194 insertions(+), 33 deletions(-) rename src/restic/{snapshot_filter_test.go => snapshot_policy_test.go} (99%) create mode 100644 src/restic/testdata/policy_keep_snapshots_20 diff --git a/src/cmds/restic/cmd_backup.go b/src/cmds/restic/cmd_backup.go index 53204f2d2..41f6dcaf2 100644 --- a/src/cmds/restic/cmd_backup.go +++ b/src/cmds/restic/cmd_backup.go @@ -68,12 +68,12 @@ func init() { f := cmdBackup.Flags() f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)") f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`) - f.StringSliceVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)") - f.StringSliceVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)") + f.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)") + f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)") f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems") f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin") f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin") - f.StringSliceVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)") + f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)") f.StringVar(&backupOptions.Hostname, "hostname", hostname, "set the `hostname` for the snapshot manually") f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)") } diff --git a/src/cmds/restic/cmd_find.go b/src/cmds/restic/cmd_find.go index 36932491c..e571cc265 100644 --- a/src/cmds/restic/cmd_find.go +++ b/src/cmds/restic/cmd_find.go @@ -45,13 +45,13 @@ func init() { f := cmdFind.Flags() f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time") f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time") - f.StringSliceVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)") + f.StringArrayVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)") 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") f.StringVarP(&findOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") - f.StringSliceVar(&findOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given") - f.StringSliceVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, 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.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") } type findPattern struct { diff --git a/src/cmds/restic/cmd_forget.go b/src/cmds/restic/cmd_forget.go index f4c03b4f1..ba268162c 100644 --- a/src/cmds/restic/cmd_forget.go +++ b/src/cmds/restic/cmd_forget.go @@ -55,14 +55,14 @@ func init() { f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots") f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots") - f.StringSliceVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "keep snapshots with this `tag` (can be specified multiple times)") + f.StringArrayVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "keep snapshots with this `tag` (can be specified multiple times)") f.BoolVarP(&forgetOptions.GroupByTags, "group-by-tags", "G", false, "Group by host,paths,tags instead of just host,paths") // Sadly the commonly used shortcut `H` is already used. f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`") // Deprecated since 2017-03-07. f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname` (deprecated)") - f.StringSliceVar(&forgetOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)") - f.StringSliceVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)") + f.StringArrayVar(&forgetOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)") + f.StringArrayVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)") f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done") f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed") diff --git a/src/cmds/restic/cmd_ls.go b/src/cmds/restic/cmd_ls.go index d71d4e2b3..329ff5f70 100644 --- a/src/cmds/restic/cmd_ls.go +++ b/src/cmds/restic/cmd_ls.go @@ -41,8 +41,8 @@ func init() { flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") - flags.StringSliceVar(&lsOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot ID is given") - flags.StringSliceVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given") + flags.StringArrayVar(&lsOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot ID is given") + flags.StringArrayVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given") } func printTree(repo *repository.Repository, id *restic.ID, prefix string) error { diff --git a/src/cmds/restic/cmd_mount.go b/src/cmds/restic/cmd_mount.go index 243fa4e37..323f34c8c 100644 --- a/src/cmds/restic/cmd_mount.go +++ b/src/cmds/restic/cmd_mount.go @@ -52,8 +52,8 @@ func init() { mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory") mountFlags.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`) - mountFlags.StringSliceVar(&mountOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`") - mountFlags.StringSliceVar(&mountOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`") + mountFlags.StringArrayVar(&mountOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`") + mountFlags.StringArrayVar(&mountOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`") } func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error { diff --git a/src/cmds/restic/cmd_restore.go b/src/cmds/restic/cmd_restore.go index 9dec03851..0d1575f29 100644 --- a/src/cmds/restic/cmd_restore.go +++ b/src/cmds/restic/cmd_restore.go @@ -40,13 +40,13 @@ func init() { cmdRoot.AddCommand(cmdRestore) flags := cmdRestore.Flags() - flags.StringSliceVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)") - flags.StringSliceVarP(&restoreOptions.Include, "include", "i", nil, "include a `pattern`, exclude everything else (can be specified multiple times)") + flags.StringArrayVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)") + flags.StringArrayVarP(&restoreOptions.Include, "include", "i", nil, "include a `pattern`, exclude everything else (can be specified multiple times)") flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to") flags.StringVarP(&restoreOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`) - flags.StringSliceVar(&restoreOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` for snapshot ID \"latest\"") - flags.StringSliceVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"") + flags.StringArrayVar(&restoreOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` for snapshot ID \"latest\"") + flags.StringArrayVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"") } func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { diff --git a/src/cmds/restic/cmd_snapshots.go b/src/cmds/restic/cmd_snapshots.go index 7a3fa9879..4c0eb8f6e 100644 --- a/src/cmds/restic/cmd_snapshots.go +++ b/src/cmds/restic/cmd_snapshots.go @@ -37,8 +37,8 @@ func init() { f := cmdSnapshots.Flags() f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`") - f.StringSliceVar(&snapshotOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)") - f.StringSliceVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)") + f.StringArrayVar(&snapshotOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)") + f.StringArrayVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)") } func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error { diff --git a/src/cmds/restic/cmd_tag.go b/src/cmds/restic/cmd_tag.go index 32c2ba583..495aed57e 100644 --- a/src/cmds/restic/cmd_tag.go +++ b/src/cmds/restic/cmd_tag.go @@ -48,8 +48,8 @@ func init() { tagFlags.StringSliceVar(&tagOptions.RemoveTags, "remove", nil, "`tag` which will be removed from the existing tags (can be given multiple times)") tagFlags.StringVarP(&tagOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") - tagFlags.StringSliceVar(&tagOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given") - tagFlags.StringSliceVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") + tagFlags.StringArrayVar(&tagOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given") + tagFlags.StringArrayVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") } func changeTags(repo *repository.Repository, sn *restic.Snapshot, setTags, addTags, removeTags []string) (bool, error) { diff --git a/src/restic/snapshot.go b/src/restic/snapshot.go index 11693bd52..a641e5b8f 100644 --- a/src/restic/snapshot.go +++ b/src/restic/snapshot.go @@ -5,6 +5,7 @@ import ( "fmt" "os/user" "path/filepath" + "strings" "time" ) @@ -130,36 +131,64 @@ func (sn *Snapshot) RemoveTags(removeTags []string) (changed bool) { return } -// HasTags returns true if the snapshot has at least all of tags. +func (sn *Snapshot) hasTag(tag string) bool { + for _, snTag := range sn.Tags { + if tag == snTag { + return true + } + } + return false +} + +// HasTags returns true if the snapshot has at least one of the tags. Tags +// are compared as strings, unless they contain a comma. Then each of the comma +// separated parts of the tag need to be present. func (sn *Snapshot) HasTags(tags []string) bool { + if len(tags) == 0 { + return true + } nextTag: for _, tag := range tags { - for _, snTag := range sn.Tags { - if tag == snTag { + for _, s := range strings.Split(tag, ",") { + if !sn.hasTag(s) { + // fail, try next tag continue nextTag } } - - return false + return true } - return true + return false } -// HasPaths returns true if the snapshot has at least all of paths. +func (sn *Snapshot) hasPath(path string) bool { + for _, snPath := range sn.Paths { + if path == snPath { + return true + } + } + return false +} + +// HasPaths returns true if the snapshot has at least one of the paths. Paths +// are compared as strings unless they contain a comma. Then each of the comma +// separated parts of the path need to be present. func (sn *Snapshot) HasPaths(paths []string) bool { + if len(paths) == 0 { + return true + } nextPath: for _, path := range paths { - for _, snPath := range sn.Paths { - if path == snPath { + for _, p := range strings.Split(path, ",") { + if !sn.hasPath(p) { + // fail, try next path continue nextPath } } - - return false + return true } - return true + return false } // SamePaths returns true if the snapshot matches the entire paths set diff --git a/src/restic/snapshot_filter_test.go b/src/restic/snapshot_policy_test.go similarity index 99% rename from src/restic/snapshot_filter_test.go rename to src/restic/snapshot_policy_test.go index 1bf1e7460..63dec3583 100644 --- a/src/restic/snapshot_filter_test.go +++ b/src/restic/snapshot_policy_test.go @@ -164,6 +164,7 @@ var expireTests = []restic.ExpirePolicy{ {Yearly: 10}, {Daily: 7, Weekly: 2, Monthly: 3, Yearly: 10}, {Tags: []string{"foo"}}, + {Tags: []string{"foo,bar"}}, {Tags: []string{"foo", "bar"}}, } diff --git a/src/restic/testdata/policy_keep_snapshots_20 b/src/restic/testdata/policy_keep_snapshots_20 new file mode 100644 index 000000000..004b12156 --- /dev/null +++ b/src/restic/testdata/policy_keep_snapshots_20 @@ -0,0 +1,131 @@ +[ + { + "time": "2014-11-15T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo", + "bar" + ] + }, + { + "time": "2014-11-13T10:20:30.1Z", + "tree": null, + "paths": null, + "tags": [ + "bar" + ] + }, + { + "time": "2014-11-13T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-11-12T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-11-10T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-11-08T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-22T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-20T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-11T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-10T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-09T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-08T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-06T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-05T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-02T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + }, + { + "time": "2014-10-01T10:20:30Z", + "tree": null, + "paths": null, + "tags": [ + "foo" + ] + } +] \ No newline at end of file From f5b1c7e5f1686febaff9319541335572dbeb14b4 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 9 Jul 2017 09:24:02 +0200 Subject: [PATCH 2/8] Add TagList --- src/cmds/restic/cmd_forget.go | 7 ++++- src/restic/snapshot.go | 49 ++++++------------------------ src/restic/snapshot_policy.go | 33 ++++++++++++++------ src/restic/snapshot_policy_test.go | 8 ++--- 4 files changed, 44 insertions(+), 53 deletions(-) diff --git a/src/cmds/restic/cmd_forget.go b/src/cmds/restic/cmd_forget.go index ba268162c..8a2a98691 100644 --- a/src/cmds/restic/cmd_forget.go +++ b/src/cmds/restic/cmd_forget.go @@ -122,6 +122,11 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { return nil } + var tagLists []restic.TagList + for _, t := range opts.KeepTags { + tagLists = append(tagLists, restic.SplitTagList(t)) + } + policy := restic.ExpirePolicy{ Last: opts.Last, Hourly: opts.Hourly, @@ -129,7 +134,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { Weekly: opts.Weekly, Monthly: opts.Monthly, Yearly: opts.Yearly, - Tags: opts.KeepTags, + Tags: tagLists, } if policy.Empty() { diff --git a/src/restic/snapshot.go b/src/restic/snapshot.go index a641e5b8f..150f3b132 100644 --- a/src/restic/snapshot.go +++ b/src/restic/snapshot.go @@ -5,7 +5,6 @@ import ( "fmt" "os/user" "path/filepath" - "strings" "time" ) @@ -140,25 +139,15 @@ func (sn *Snapshot) hasTag(tag string) bool { return false } -// HasTags returns true if the snapshot has at least one of the tags. Tags -// are compared as strings, unless they contain a comma. Then each of the comma -// separated parts of the tag need to be present. -func (sn *Snapshot) HasTags(tags []string) bool { - if len(tags) == 0 { - return true - } -nextTag: - for _, tag := range tags { - for _, s := range strings.Split(tag, ",") { - if !sn.hasTag(s) { - // fail, try next tag - continue nextTag - } +// HasTags returns true if the snapshot has all the tags in l. +func (sn *Snapshot) HasTags(l []string) bool { + for _, tag := range l { + if !sn.hasTag(tag) { + return false } - return true } - return false + return true } func (sn *Snapshot) hasPath(path string) bool { @@ -170,33 +159,15 @@ func (sn *Snapshot) hasPath(path string) bool { return false } -// HasPaths returns true if the snapshot has at least one of the paths. Paths -// are compared as strings unless they contain a comma. Then each of the comma -// separated parts of the path need to be present. +// HasPaths returns true if the snapshot has all of the paths. func (sn *Snapshot) HasPaths(paths []string) bool { - if len(paths) == 0 { - return true - } -nextPath: for _, path := range paths { - for _, p := range strings.Split(path, ",") { - if !sn.hasPath(p) { - // fail, try next path - continue nextPath - } + if !sn.hasPath(path) { + return false } - return true } - return false -} - -// SamePaths returns true if the snapshot matches the entire paths set -func (sn *Snapshot) SamePaths(paths []string) bool { - if len(sn.Paths) != len(paths) { - return false - } - return sn.HasPaths(paths) + return true } // Snapshots is a list of snapshots. diff --git a/src/restic/snapshot_policy.go b/src/restic/snapshot_policy.go index 967d388a4..5e674fcc0 100644 --- a/src/restic/snapshot_policy.go +++ b/src/restic/snapshot_policy.go @@ -3,18 +3,32 @@ package restic import ( "reflect" "sort" + "strings" "time" ) +// TagList is a list of tags. +type TagList []string + +// SplitTagList splits a string into a list of tags. The tags in the string +// need to be separated by commas. Whitespace is stripped around the individual +// tags. +func SplitTagList(s string) (l TagList) { + for _, t := range strings.Split(s, ",") { + l = append(l, strings.TrimSpace(t)) + } + return l +} + // ExpirePolicy configures which snapshots should be automatically removed. type ExpirePolicy struct { - Last int // keep the last n snapshots - Hourly int // keep the last n hourly snapshots - Daily int // keep the last n daily snapshots - Weekly int // keep the last n weekly snapshots - Monthly int // keep the last n monthly snapshots - Yearly int // keep the last n yearly snapshots - Tags []string // keep all snapshots with these tags + Last int // keep the last n snapshots + Hourly int // keep the last n hourly snapshots + Daily int // keep the last n daily snapshots + Weekly int // keep the last n weekly snapshots + Monthly int // keep the last n monthly snapshots + Yearly int // keep the last n yearly snapshots + Tags []TagList // keep all snapshots that include at least one of the tag lists. } // Sum returns the maximum number of snapshots to be kept according to this @@ -94,11 +108,12 @@ func ApplyPolicy(list Snapshots, p ExpirePolicy) (keep, remove Snapshots) { var keepSnap bool // Tags are handled specially as they are not counted. - if len(p.Tags) > 0 { - if cur.HasTags(p.Tags) { + for _, l := range p.Tags { + if cur.HasTags(l) { keepSnap = true } } + // Now update the other buckets and see if they have some counts left. for i, b := range buckets { if b.Count > 0 { diff --git a/src/restic/snapshot_policy_test.go b/src/restic/snapshot_policy_test.go index 63dec3583..f7913ccba 100644 --- a/src/restic/snapshot_policy_test.go +++ b/src/restic/snapshot_policy_test.go @@ -27,7 +27,7 @@ func TestExpireSnapshotOps(t *testing.T) { p *restic.ExpirePolicy }{ {true, 0, &restic.ExpirePolicy{}}, - {true, 0, &restic.ExpirePolicy{Tags: []string{}}}, + {true, 0, &restic.ExpirePolicy{Tags: []restic.TagList{}}}, {false, 22, &restic.ExpirePolicy{Daily: 7, Weekly: 2, Monthly: 3, Yearly: 10}}, } for i, d := range data { @@ -163,9 +163,9 @@ var expireTests = []restic.ExpirePolicy{ {Daily: 2, Weekly: 2, Monthly: 6}, {Yearly: 10}, {Daily: 7, Weekly: 2, Monthly: 3, Yearly: 10}, - {Tags: []string{"foo"}}, - {Tags: []string{"foo,bar"}}, - {Tags: []string{"foo", "bar"}}, + {Tags: []restic.TagList{{"foo"}}}, + {Tags: []restic.TagList{{"foo", "bar"}}}, + {Tags: []restic.TagList{{"foo"}, {"bar"}}}, } func TestApplyPolicy(t *testing.T) { From 7362569cf5065f029e5e60b6a572e846fd4dc18f Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 9 Jul 2017 09:47:41 +0200 Subject: [PATCH 3/8] Use TagLists for all commands --- src/cmds/restic/cmd_backup.go | 2 +- src/cmds/restic/cmd_find.go | 6 ++++-- src/cmds/restic/cmd_forget.go | 2 +- src/cmds/restic/cmd_ls.go | 2 +- src/cmds/restic/cmd_mount.go | 3 ++- src/cmds/restic/cmd_restore.go | 2 +- src/cmds/restic/cmd_snapshots.go | 2 +- src/cmds/restic/cmd_tag.go | 2 +- src/cmds/restic/find.go | 2 +- src/restic/fuse/root.go | 2 +- src/restic/snapshot.go | 20 ++++++++++++++++++++ src/restic/snapshot_find.go | 24 +++++++++++++++++------- src/restic/snapshot_policy.go | 10 ++++++++++ 13 files changed, 61 insertions(+), 18 deletions(-) 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 From 78d090aea5d906996234e8a6ac03577bca347ed6 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 9 Jul 2017 12:45:49 +0200 Subject: [PATCH 4/8] Implement TagList and TagLists as pflag.Value --- src/cmds/restic/cmd_find.go | 8 ++--- src/cmds/restic/cmd_forget.go | 17 ++++----- src/cmds/restic/cmd_ls.go | 6 ++-- src/cmds/restic/cmd_mount.go | 6 ++-- src/cmds/restic/cmd_restore.go | 6 ++-- src/cmds/restic/cmd_snapshots.go | 6 ++-- src/cmds/restic/cmd_tag.go | 6 ++-- src/restic/snapshot_policy.go | 24 ------------- src/restic/tag_list.go | 62 ++++++++++++++++++++++++++++++++ 9 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 src/restic/tag_list.go diff --git a/src/cmds/restic/cmd_find.go b/src/cmds/restic/cmd_find.go index e3b8e2784..3d98c3f8a 100644 --- a/src/cmds/restic/cmd_find.go +++ b/src/cmds/restic/cmd_find.go @@ -34,7 +34,7 @@ type FindOptions struct { ListLong bool Host string Paths []string - Tags []string + Tags restic.TagLists } var findOptions FindOptions @@ -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 the list of `tag,tag,...`, when no snapshot-ID is given") + f.Var(&findOptions.Tags, "tag", "only consider snapshots which include this `taglist`, 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,15 +290,13 @@ 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, tagLists, opts.Paths, opts.Snapshots) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, 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 e9d96d7fc..ceb8220ae 100644 --- a/src/cmds/restic/cmd_forget.go +++ b/src/cmds/restic/cmd_forget.go @@ -31,10 +31,10 @@ type ForgetOptions struct { Weekly int Monthly int Yearly int - KeepTags []string + KeepTags restic.TagLists Host string - Tags []string + Tags restic.TagLists Paths []string GroupByTags bool @@ -55,13 +55,13 @@ func init() { f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots") f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots") - f.StringArrayVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "keep snapshots with this `tag` (can be specified multiple times)") + f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)") f.BoolVarP(&forgetOptions.GroupByTags, "group-by-tags", "G", false, "Group by host,paths,tags instead of just host,paths") // Sadly the commonly used shortcut `H` is already used. f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`") // Deprecated since 2017-03-07. f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname` (deprecated)") - f.StringArrayVar(&forgetOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)") + f.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` (can be specified multiple times)") f.StringArrayVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)") f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done") @@ -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, restic.SplitTagLists(opts.Tags), opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { if len(args) > 0 { // When explicit snapshots args are given, remove them immediately. if !opts.DryRun { @@ -122,11 +122,6 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { return nil } - var tagLists []restic.TagList - for _, t := range opts.KeepTags { - tagLists = append(tagLists, restic.SplitTagList(t)) - } - policy := restic.ExpirePolicy{ Last: opts.Last, Hourly: opts.Hourly, @@ -134,7 +129,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { Weekly: opts.Weekly, Monthly: opts.Monthly, Yearly: opts.Yearly, - Tags: tagLists, + Tags: opts.KeepTags, } if policy.Empty() { diff --git a/src/cmds/restic/cmd_ls.go b/src/cmds/restic/cmd_ls.go index c6461e950..83acd5b27 100644 --- a/src/cmds/restic/cmd_ls.go +++ b/src/cmds/restic/cmd_ls.go @@ -28,7 +28,7 @@ The special snapshot-ID "latest" can be used to list files and directories of th type LsOptions struct { ListLong bool Host string - Tags []string + Tags restic.TagLists Paths []string } @@ -41,7 +41,7 @@ func init() { flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode") flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") - flags.StringArrayVar(&lsOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot ID is given") + flags.Var(&lsOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given") flags.StringArrayVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given") } @@ -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, restic.SplitTagLists(opts.Tags), opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, 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 43538ca20..b7872eef8 100644 --- a/src/cmds/restic/cmd_mount.go +++ b/src/cmds/restic/cmd_mount.go @@ -38,7 +38,7 @@ type MountOptions struct { AllowRoot bool AllowOther bool Host string - Tags []string + Tags restic.TagLists Paths []string } @@ -53,7 +53,7 @@ func init() { mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory") mountFlags.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`) - mountFlags.StringArrayVar(&mountOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`") + mountFlags.Var(&mountOptions.Tags, "tag", "only consider snapshots which include this `taglist`") mountFlags.StringArrayVar(&mountOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`") } @@ -104,7 +104,7 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error { cfg := fuse.Config{ OwnerIsRoot: opts.OwnerRoot, Host: opts.Host, - Tags: restic.SplitTagLists(opts.Tags), + Tags: 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 4d304ce14..f78f3010c 100644 --- a/src/cmds/restic/cmd_restore.go +++ b/src/cmds/restic/cmd_restore.go @@ -31,7 +31,7 @@ type RestoreOptions struct { Target string Host string Paths []string - Tags []string + Tags restic.TagLists } var restoreOptions RestoreOptions @@ -45,7 +45,7 @@ func init() { flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to") flags.StringVarP(&restoreOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`) - flags.StringArrayVar(&restoreOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` for snapshot ID \"latest\"") + flags.Var(&restoreOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"") flags.StringArrayVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"") } @@ -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, restic.SplitTagLists(opts.Tags), opts.Host) + id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, 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 c69df0810..8a4c63c2e 100644 --- a/src/cmds/restic/cmd_snapshots.go +++ b/src/cmds/restic/cmd_snapshots.go @@ -26,7 +26,7 @@ The "snapshots" command lists all snapshots stored in the repository. // SnapshotOptions bundles all options for the snapshots command. type SnapshotOptions struct { Host string - Tags []string + Tags restic.TagLists Paths []string } @@ -37,7 +37,7 @@ func init() { f := cmdSnapshots.Flags() f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`") - f.StringArrayVar(&snapshotOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)") + f.Var(&snapshotOptions.Tags, "tag", "only consider snapshots which include this `taglist` (can be specified multiple times)") f.StringArrayVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)") } @@ -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, restic.SplitTagLists(opts.Tags), opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, 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 b2f2dec38..014ade697 100644 --- a/src/cmds/restic/cmd_tag.go +++ b/src/cmds/restic/cmd_tag.go @@ -31,7 +31,7 @@ When no snapshot-ID is given, all snapshots matching the host, tag and path filt type TagOptions struct { Host string Paths []string - Tags []string + Tags restic.TagLists SetTags []string AddTags []string RemoveTags []string @@ -48,7 +48,7 @@ func init() { tagFlags.StringSliceVar(&tagOptions.RemoveTags, "remove", nil, "`tag` which will be removed from the existing tags (can be given multiple times)") tagFlags.StringVarP(&tagOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given") - tagFlags.StringArrayVar(&tagOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given") + tagFlags.Var(&tagOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given") tagFlags.StringArrayVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given") } @@ -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, restic.SplitTagLists(opts.Tags), opts.Paths, args) { + for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, 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/restic/snapshot_policy.go b/src/restic/snapshot_policy.go index 88ec9ed99..0775f4024 100644 --- a/src/restic/snapshot_policy.go +++ b/src/restic/snapshot_policy.go @@ -3,33 +3,9 @@ package restic import ( "reflect" "sort" - "strings" "time" ) -// TagList is a list of tags. -type TagList []string - -// SplitTagList splits a string into a list of tags. The tags in the string -// need to be separated by commas. Whitespace is stripped around the individual -// tags. -func SplitTagList(s string) (l TagList) { - for _, t := range strings.Split(s, ",") { - l = append(l, strings.TrimSpace(t)) - } - 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 diff --git a/src/restic/tag_list.go b/src/restic/tag_list.go new file mode 100644 index 000000000..8e8f5b9f7 --- /dev/null +++ b/src/restic/tag_list.go @@ -0,0 +1,62 @@ +package restic + +import ( + "fmt" + "strings" +) + +// TagList is a list of tags. +type TagList []string + +// SplitTagList splits a string into a list of tags. The tags in the string +// need to be separated by commas. Whitespace is stripped around the individual +// tags. +func SplitTagList(s string) (l TagList) { + for _, t := range strings.Split(s, ",") { + l = append(l, strings.TrimSpace(t)) + } + return l +} + +func (l TagList) String() string { + return "[" + strings.Join(l, ", ") + "]" +} + +// Set updates the TagList's value. +func (l *TagList) Set(s string) error { + *l = SplitTagList(s) + return nil +} + +// Type returns a description of the type. +func (TagList) Type() string { + return "TagList" +} + +// TagLists consists of several TagList. +type TagLists []TagList + +// SplitTagLists splits a slice of strings into a slice of TagLists using +// SplitTagList. +func SplitTagLists(s []string) (l TagLists) { + l = make([]TagList, 0, len(s)) + for _, t := range s { + l = append(l, SplitTagList(t)) + } + return l +} + +func (l TagLists) String() string { + return fmt.Sprintf("%v", []TagList(l)) +} + +// Set updates the TagList's value. +func (l *TagLists) Set(s string) error { + *l = append(*l, SplitTagList(s)) + return nil +} + +// Type returns a description of the type. +func (TagLists) Type() string { + return "TagLists" +} From fda5e1f543b05797e0c798c175c5475fc14a739b Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 9 Jul 2017 20:28:38 +0200 Subject: [PATCH 5/8] Adress code review comments --- src/restic/tag_list.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/restic/tag_list.go b/src/restic/tag_list.go index 8e8f5b9f7..0f8232e5b 100644 --- a/src/restic/tag_list.go +++ b/src/restic/tag_list.go @@ -8,10 +8,10 @@ import ( // TagList is a list of tags. type TagList []string -// SplitTagList splits a string into a list of tags. The tags in the string +// splitTagList splits a string into a list of tags. The tags in the string // need to be separated by commas. Whitespace is stripped around the individual // tags. -func SplitTagList(s string) (l TagList) { +func splitTagList(s string) (l TagList) { for _, t := range strings.Split(s, ",") { l = append(l, strings.TrimSpace(t)) } @@ -24,7 +24,7 @@ func (l TagList) String() string { // Set updates the TagList's value. func (l *TagList) Set(s string) error { - *l = SplitTagList(s) + *l = splitTagList(s) return nil } @@ -36,12 +36,12 @@ func (TagList) Type() string { // TagLists consists of several TagList. type TagLists []TagList -// SplitTagLists splits a slice of strings into a slice of TagLists using +// splitTagLists splits a slice of strings into a slice of TagLists using // SplitTagList. -func SplitTagLists(s []string) (l TagLists) { +func splitTagLists(s []string) (l TagLists) { l = make([]TagList, 0, len(s)) for _, t := range s { - l = append(l, SplitTagList(t)) + l = append(l, splitTagList(t)) } return l } @@ -52,7 +52,7 @@ func (l TagLists) String() string { // Set updates the TagList's value. func (l *TagLists) Set(s string) error { - *l = append(*l, SplitTagList(s)) + *l = append(*l, splitTagList(s)) return nil } From 750ee35dbfd3d226b091b62513b42be7108e15a6 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 16 Jul 2017 15:23:40 +0200 Subject: [PATCH 6/8] Add more examples to the manual --- doc/manual.rst | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/doc/manual.rst b/doc/manual.rst index 6e1d22cfc..0282acebd 100644 --- a/doc/manual.rst +++ b/doc/manual.rst @@ -526,18 +526,20 @@ specified with ``--stdin-filename``, e.g. like this: $ mysqldump [...] | restic -r /tmp/backup backup --stdin --stdin-filename production.sql -Tags -~~~~ +Tags for backup +~~~~~~~~~~~~~~~ Snapshots can have one or more tags, short strings which add identifying -information. Just specify the tags for a snapshot with ``--tag``: +information. Just specify the tags for a snapshot one by one with ``--tag``: .. code-block:: console - $ restic -r /tmp/backup backup --tag projectX ~/shared/work/web + $ restic -r /tmp/backup backup --tag projectX -tag foo --tag bar ~/shared/work/web [...] -The tags can later be used to keep (or forget) snapshots. +The tags can later be used to keep (or forget) snapshots with the ``forget`` +command. The command ``tag`` can be used to modify tags on an existing +snapshot. List all snapshots ------------------ @@ -644,7 +646,7 @@ command does that: .. code-block:: console - $ restic -r /tmp/backup tag --set NL,CH 590c8fc8 + $ restic -r /tmp/backup tag --set NL --set CH 590c8fc8 Create exclusive lock for repository Modified tags on 1 snapshots @@ -872,7 +874,26 @@ The ``forget`` command accepts the following parameters: Additionally, you can restrict removing snapshots to those which have a particular hostname with the ``--hostname`` parameter, or tags with the ``--tag`` option. When multiple tags are specified, only the snapshots -which have all the tags are considered. +which have all the tags are considered. For example, the following command +removes all but the latest snapshot of all snapshots that have the tag ``foo``: + +.. code-block:: console + + $ restic forget --tag foo --keep-last 1 + +This command removes all but the last snapshot of all snapshots that have +either the ``foo`` or ``bar`` tag set: + +.. code-block:: console + + $ restic forget --tag foo --tag bar --keep-last 1 + +To only keep the last snapshot of all snapshots with both the tag ``foo`` and +``bar`` set use: + +.. code-block:: console + + $ restic forget --tag foo,tag bar --keep-last 1 All the ``--keep-*`` options above only count hours/days/weeks/months/years which have a snapshot, so those without a From 7bb1a474df4cb98256d37929622a0757a85a9cc0 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 16 Jul 2017 15:24:27 +0200 Subject: [PATCH 7/8] Add entry to CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb76e9337..b7ab1d828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ Small changes (usually when ctrl+t is pressed). https://github.com/restic/restic/pull/1082 + * The semantic for the `--tags` option to `forget` and `snapshots` was + clarified: + https://github.com/restic/restic/issues/1081 + https://github.com/restic/restic/pull/1090 Important Changes in 0.7.0 ========================== From 20b835b5a444e5dc7f843cbe842b390c897b0d10 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 16 Jul 2017 15:25:28 +0200 Subject: [PATCH 8/8] Improve help text --- src/cmds/restic/cmd_forget.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmds/restic/cmd_forget.go b/src/cmds/restic/cmd_forget.go index ceb8220ae..26388eaa8 100644 --- a/src/cmds/restic/cmd_forget.go +++ b/src/cmds/restic/cmd_forget.go @@ -61,7 +61,7 @@ func init() { f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`") // Deprecated since 2017-03-07. f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname` (deprecated)") - f.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` (can be specified multiple times)") + f.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)") f.StringArrayVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)") f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")