diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index 79cc9f449..989fe0975 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -5,6 +5,7 @@ import ( "encoding/json" "sort" "strings" + "time" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" @@ -27,13 +28,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 - KeepTags restic.TagLists + Last int + Hourly int + Daily int + Weekly int + Monthly int + Yearly int + NewerThan time.Duration + KeepTags restic.TagLists Host string Tags restic.TagLists @@ -58,6 +60,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.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)") // Sadly the commonly used shortcut `H` is already used. @@ -163,14 +166,20 @@ 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, - Tags: opts.KeepTags, + Last: opts.Last, + Hourly: opts.Hourly, + Daily: opts.Daily, + Weekly: opts.Weekly, + Monthly: opts.Monthly, + Yearly: opts.Yearly, + NewerThan: ageCutoff, + Tags: opts.KeepTags, } if policy.Empty() && len(args) == 0 { diff --git a/internal/restic/snapshot_policy.go b/internal/restic/snapshot_policy.go index 8dd7e5ed6..88533a2cb 100644 --- a/internal/restic/snapshot_policy.go +++ b/internal/restic/snapshot_policy.go @@ -10,13 +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 - 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 + NewerThan time.Time // keep snapshots newer than this time + Tags []TagList // keep all snapshots that include at least one of the tag lists. } func (e ExpirePolicy) String() (s string) { @@ -39,15 +40,11 @@ func (e ExpirePolicy) String() (s string) { if e.Yearly > 0 { keeps = append(keeps, fmt.Sprintf("%d yearly", e.Yearly)) } - - s = "keep the last " - for _, k := range keeps { - s += k + ", " + if !e.NewerThan.IsZero() { + keeps = append(keeps, fmt.Sprintf("snapshots newer than %s", e.NewerThan)) } - s = strings.Trim(s, ", ") - s += " snapshots" - return s + return fmt.Sprintf("keep the last %s snapshots", strings.Join(keeps, ", ")) } // Sum returns the maximum number of snapshots to be kept according to this @@ -133,6 +130,11 @@ 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 + } + // 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/internal/restic/snapshot_policy_test.go b/internal/restic/snapshot_policy_test.go index 69111fab1..b725dcbad 100644 --- a/internal/restic/snapshot_policy_test.go +++ b/internal/restic/snapshot_policy_test.go @@ -171,6 +171,7 @@ 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")}, } func TestApplyPolicy(t *testing.T) { diff --git a/internal/restic/testdata/policy_keep_snapshots_21 b/internal/restic/testdata/policy_keep_snapshots_21 new file mode 100644 index 000000000..11be139f5 --- /dev/null +++ b/internal/restic/testdata/policy_keep_snapshots_21 @@ -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