diff --git a/CHANGELOG.md b/CHANGELOG.md index abdbd7e29..17d6e7a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,11 @@ Small changes https://github.com/restic/restic/pull/1080 https://github.com/restic/restic/pull/1112 + * 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 ========================== 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 diff --git a/src/cmds/restic/cmd_backup.go b/src/cmds/restic/cmd_backup.go index 53204f2d2..773645693 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)") } @@ -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 36932491c..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 @@ -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.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") } type findPattern struct { diff --git a/src/cmds/restic/cmd_forget.go b/src/cmds/restic/cmd_forget.go index f4c03b4f1..26388eaa8 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,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.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.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.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") 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..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,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.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") } 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..b7872eef8 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" @@ -37,7 +38,7 @@ type MountOptions struct { AllowRoot bool AllowOther bool Host string - Tags []string + Tags restic.TagLists Paths []string } @@ -52,8 +53,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.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`") } 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..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 @@ -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.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\"") } 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..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,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.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)") } 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..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,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.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") } func changeTags(repo *repository.Repository, sn *restic.Snapshot, setTags, addTags, removeTags []string) (bool, error) { 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 11693bd52..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" ) @@ -130,46 +131,65 @@ func (sn *Snapshot) RemoveTags(removeTags []string) (changed bool) { return } -// HasTags returns true if the snapshot has at least all of tags. -func (sn *Snapshot) HasTags(tags []string) bool { -nextTag: - for _, tag := range tags { - for _, snTag := range sn.Tags { - if tag == snTag { - continue nextTag - } +func (sn *Snapshot) hasTag(tag string) bool { + for _, snTag := range sn.Tags { + if tag == snTag { + return true } + } + return false +} - return false +// 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 } -// HasPaths returns true if the snapshot has at least all of paths. +// 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 { + return true + } + } + return false +} + +// HasPaths returns true if the snapshot has all of the paths. func (sn *Snapshot) HasPaths(paths []string) bool { -nextPath: for _, path := range paths { - for _, snPath := range sn.Paths { - if path == snPath { - continue nextPath - } + if !sn.hasPath(path) { + return false } - - return false } return true } -// 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) -} - // Snapshots is a list of snapshots. type Snapshots []*Snapshot 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 967d388a4..0775f4024 100644 --- a/src/restic/snapshot_policy.go +++ b/src/restic/snapshot_policy.go @@ -8,13 +8,13 @@ import ( // 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 +94,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_filter_test.go b/src/restic/snapshot_policy_test.go similarity index 97% rename from src/restic/snapshot_filter_test.go rename to src/restic/snapshot_policy_test.go index 1bf1e7460..f7913ccba 100644 --- a/src/restic/snapshot_filter_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,8 +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: []restic.TagList{{"foo"}}}, + {Tags: []restic.TagList{{"foo", "bar"}}}, + {Tags: []restic.TagList{{"foo"}, {"bar"}}}, } func TestApplyPolicy(t *testing.T) { diff --git a/src/restic/tag_list.go b/src/restic/tag_list.go new file mode 100644 index 000000000..0f8232e5b --- /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" +} 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