diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index 989fe0975..4debaef3b 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -5,7 +5,6 @@ import ( "encoding/json" "sort" "strings" - "time" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" @@ -28,14 +27,14 @@ data after 'forget' was run successfully, see the 'prune' command. `, // ForgetOptions collects all options for the forget command. type ForgetOptions struct { - Last int - Hourly int - Daily int - Weekly int - Monthly int - Yearly int - NewerThan time.Duration - KeepTags restic.TagLists + Last int + Hourly int + Daily int + Weekly int + Monthly int + Yearly int + WithinDays int + KeepTags restic.TagLists Host string Tags restic.TagLists @@ -60,7 +59,7 @@ func init() { f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots") 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.DurationVar(&forgetOptions.NewerThan, "keep-newer-than", 0, "keep snapshots that were created within this timeframe") + f.IntVar(&forgetOptions.WithinDays, "keep-within", 0, "keep snapshots that were created within `days` before the newest") f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)") // Sadly the commonly used shortcut `H` is already used. @@ -166,20 +165,15 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error { } } - var ageCutoff time.Time - if opts.NewerThan > 0 { - ageCutoff = time.Now().Add(-opts.NewerThan) - } - policy := restic.ExpirePolicy{ - Last: opts.Last, - Hourly: opts.Hourly, - Daily: opts.Daily, - Weekly: opts.Weekly, - Monthly: opts.Monthly, - Yearly: opts.Yearly, - NewerThan: ageCutoff, - Tags: opts.KeepTags, + Last: opts.Last, + Hourly: opts.Hourly, + Daily: opts.Daily, + Weekly: opts.Weekly, + Monthly: opts.Monthly, + Yearly: opts.Yearly, + Within: opts.WithinDays, + Tags: opts.KeepTags, } if policy.Empty() && len(args) == 0 { diff --git a/internal/restic/snapshot_policy.go b/internal/restic/snapshot_policy.go index 88533a2cb..244c19140 100644 --- a/internal/restic/snapshot_policy.go +++ b/internal/restic/snapshot_policy.go @@ -10,14 +10,14 @@ 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 - NewerThan time.Time // keep snapshots newer than this time - Tags []TagList // keep all snapshots that include at least one of the tag lists. + 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 + Within int // keep snapshots made within this number of days since the newest snapshot + Tags []TagList // keep all snapshots that include at least one of the tag lists. } func (e ExpirePolicy) String() (s string) { @@ -40,8 +40,8 @@ func (e ExpirePolicy) String() (s string) { if e.Yearly > 0 { keeps = append(keeps, fmt.Sprintf("%d yearly", e.Yearly)) } - if !e.NewerThan.IsZero() { - keeps = append(keeps, fmt.Sprintf("snapshots newer than %s", e.NewerThan)) + if e.Within != 0 { + keeps = append(keeps, fmt.Sprintf("snapshots within %d days of the newest snapshot", e.Within)) } return fmt.Sprintf("keep the last %s snapshots", strings.Join(keeps, ", ")) @@ -94,6 +94,22 @@ func always(d time.Time, nr int) int { return nr } +// findLatestTimestamp returns the time stamp for the newest snapshot. +func findLatestTimestamp(list Snapshots) time.Time { + if len(list) == 0 { + panic("list of snapshots is empty") + } + + var latest time.Time + for _, sn := range list { + if sn.Time.After(latest) { + latest = sn.Time + } + } + + return latest +} + // ApplyPolicy returns the snapshots from list that are to be kept and removed // according to the policy p. list is sorted in the process. func ApplyPolicy(list Snapshots, p ExpirePolicy) (keep, remove Snapshots) { @@ -120,6 +136,8 @@ func ApplyPolicy(list Snapshots, p ExpirePolicy) (keep, remove Snapshots) { {p.Yearly, y, -1}, } + latest := findLatestTimestamp(list) + for nr, cur := range list { var keepSnap bool @@ -130,9 +148,12 @@ func ApplyPolicy(list Snapshots, p ExpirePolicy) (keep, remove Snapshots) { } } - // If a timestamp is specified, it's a hard cutoff for older snapshots. - if !p.NewerThan.IsZero() && cur.Time.After(p.NewerThan) { - keepSnap = true + // If the timestamp of the snapshot is within the range, then keep it. + if p.Within != 0 { + t := latest.AddDate(0, 0, -p.Within) + if cur.Time.After(t) { + keepSnap = true + } } // Now update the other buckets and see if they have some counts left. diff --git a/internal/restic/snapshot_policy_test.go b/internal/restic/snapshot_policy_test.go index b725dcbad..9aa5200f1 100644 --- a/internal/restic/snapshot_policy_test.go +++ b/internal/restic/snapshot_policy_test.go @@ -21,6 +21,15 @@ func parseTimeUTC(s string) time.Time { return t.UTC() } +func parseDuration(s string) time.Duration { + d, err := time.ParseDuration(s) + if err != nil { + panic(err) + } + + return d +} + func TestExpireSnapshotOps(t *testing.T) { data := []struct { expectEmpty bool @@ -171,7 +180,10 @@ var expireTests = []restic.ExpirePolicy{ {Tags: []restic.TagList{{"foo"}}}, {Tags: []restic.TagList{{"foo", "bar"}}}, {Tags: []restic.TagList{{"foo"}, {"bar"}}}, - {NewerThan: parseTimeUTC("2016-01-01 01:00:00")}, + {Within: 1}, + {Within: 2}, + {Within: 7}, + {Within: 30}, } func TestApplyPolicy(t *testing.T) { diff --git a/internal/restic/testdata/policy_keep_snapshots_21 b/internal/restic/testdata/policy_keep_snapshots_21 index 11be139f5..319c9ab1c 100644 --- a/internal/restic/testdata/policy_keep_snapshots_21 +++ b/internal/restic/testdata/policy_keep_snapshots_21 @@ -3,95 +3,5 @@ "time": "2016-01-18T12:02:03Z", "tree": null, "paths": null - }, - { - "time": "2016-01-12T21:08:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-12T21:02:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-09T21:02:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-08T20:02:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-07T10:02:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-06T08:02:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-05T09:02:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-04T16:23:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-04T12:30:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-04T12:28:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-04T12:24:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-04T12:23:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-04T11:23:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-04T10:23:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-03T07:02:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-01T07:08:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-01T01:03:03Z", - "tree": null, - "paths": null - }, - { - "time": "2016-01-01T01:02:03Z", - "tree": null, - "paths": null } ] \ No newline at end of file diff --git a/internal/restic/testdata/policy_keep_snapshots_22 b/internal/restic/testdata/policy_keep_snapshots_22 new file mode 100644 index 000000000..319c9ab1c --- /dev/null +++ b/internal/restic/testdata/policy_keep_snapshots_22 @@ -0,0 +1,7 @@ +[ + { + "time": "2016-01-18T12:02:03Z", + "tree": null, + "paths": null + } +] \ No newline at end of file diff --git a/internal/restic/testdata/policy_keep_snapshots_23 b/internal/restic/testdata/policy_keep_snapshots_23 new file mode 100644 index 000000000..667fb8b6d --- /dev/null +++ b/internal/restic/testdata/policy_keep_snapshots_23 @@ -0,0 +1,17 @@ +[ + { + "time": "2016-01-18T12:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-12T21:08:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-12T21:02:03Z", + "tree": null, + "paths": null + } +] \ No newline at end of file diff --git a/internal/restic/testdata/policy_keep_snapshots_24 b/internal/restic/testdata/policy_keep_snapshots_24 new file mode 100644 index 000000000..11be139f5 --- /dev/null +++ b/internal/restic/testdata/policy_keep_snapshots_24 @@ -0,0 +1,97 @@ +[ + { + "time": "2016-01-18T12:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-12T21:08:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-12T21:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-09T21:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-08T20:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-07T10:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-06T08:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-05T09:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-04T16:23:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-04T12:30:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-04T12:28:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-04T12:24:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-04T12:23:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-04T11:23:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-04T10:23:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-03T07:02:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-01T07:08:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-01T01:03:03Z", + "tree": null, + "paths": null + }, + { + "time": "2016-01-01T01:02:03Z", + "tree": null, + "paths": null + } +] \ No newline at end of file