diff --git a/changelog/unreleased/issue-5157 b/changelog/unreleased/issue-5157 new file mode 100644 index 000000000..0b209a1cc --- /dev/null +++ b/changelog/unreleased/issue-5157 @@ -0,0 +1,7 @@ +Enhancement: Added `--keep-unique` flag to `forget` command + +Restic `forget` command can now remove duplicate snapshots with the +`--keep-unique` flag set. + +https://github.com/restic/restic/issues/5157 +https://github.com/restic/restic/pull/5364 \ No newline at end of file diff --git a/cmd/restic/cmd_forget.go b/cmd/restic/cmd_forget.go index 976db4d0d..387b3f90a 100644 --- a/cmd/restic/cmd_forget.go +++ b/cmd/restic/cmd_forget.go @@ -112,6 +112,7 @@ type ForgetOptions struct { WithinMonthly data.Duration WithinYearly data.Duration KeepTags data.TagLists + Unique bool UnsafeAllowRemoveAll bool @@ -138,6 +139,7 @@ func (opts *ForgetOptions) AddFlags(f *pflag.FlagSet) { f.VarP(&opts.WithinMonthly, "keep-within-monthly", "", "keep monthly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot") f.VarP(&opts.WithinYearly, "keep-within-yearly", "", "keep yearly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot") f.Var(&opts.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)") + f.BoolVar(&opts.Unique, "keep-unique", false, "keep the only one snapshot per tree") f.BoolVar(&opts.UnsafeAllowRemoveAll, "unsafe-allow-remove-all", false, "allow deleting all snapshots of a snapshot group") f.StringArrayVar(&opts.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)") @@ -233,6 +235,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption WithinMonthly: opts.WithinMonthly, WithinYearly: opts.WithinYearly, Tags: opts.KeepTags, + Unique: opts.Unique, } if policy.Empty() { diff --git a/doc/060_forget.rst b/doc/060_forget.rst index 9dcb45531..5fd29f295 100644 --- a/doc/060_forget.rst +++ b/doc/060_forget.rst @@ -205,6 +205,7 @@ The ``forget`` command accepts the following policy options: specified duration of the latest snapshot. - ``--keep-within-yearly duration`` keep all yearly snapshots made within the specified duration of the latest snapshot. +- ``--keep-unique`` keep only one snapshot per tree. .. note:: All calendar related options (``--keep-{hourly,daily,...}``) work on natural time boundaries and *not* relative to when you run ``forget``. Weeks diff --git a/internal/data/snapshot_policy.go b/internal/data/snapshot_policy.go index 1ee5af984..5708a342d 100644 --- a/internal/data/snapshot_policy.go +++ b/internal/data/snapshot_policy.go @@ -8,6 +8,7 @@ import ( "time" "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/restic" ) // ExpirePolicy configures which snapshots should be automatically removed. @@ -25,6 +26,7 @@ type ExpirePolicy struct { WithinMonthly Duration // keep monthly snapshots made within this duration WithinYearly Duration // keep yearly snapshots made within this duration Tags []TagList // keep all snapshots that include at least one of the tag lists. + Unique bool // keep the only one snapshot per tree } func (e ExpirePolicy) String() (s string) { @@ -164,6 +166,15 @@ func findLatestTimestamp(list Snapshots) time.Time { return latest } +func findParentSnapshot(list Snapshots, id restic.ID) *Snapshot { + for _, sn := range list { + if sn.ID().Equal(id) { + return sn + } + } + return nil +} + // KeepReason specifies why a particular snapshot was kept, and the counters at // that point in the policy evaluation. type KeepReason struct { @@ -288,6 +299,17 @@ func ApplyPolicy(list Snapshots, p ExpirePolicy) (keep, remove Snapshots, reason } } + if p.Unique { + if cur.Parent != nil { + parent := findParentSnapshot(keep, *cur.Parent) + if parent != nil { + if parent.Tree == cur.Tree { + keepSnap = false + } + } + } + } + if keepSnap { keep = append(keep, cur) kr := KeepReason{