forked from TrueCloudLab/restic
Merge pull request #4426 from MichaelEischer/consistent-forget-policy
forget: replace `--keep-* -1` with `--keep-* unlimited`
This commit is contained in:
commit
95050117eb
3 changed files with 128 additions and 62 deletions
|
@ -1,8 +1,9 @@
|
||||||
Bugfix: Restic forget --keep-* options now interpret "-1" as "forever"
|
Bugfix: Support "unlimited" in `forget --keep-*` options
|
||||||
|
|
||||||
Restic would forget snapshots that should have been kept when "-1" was
|
Restic would forget snapshots that should have been kept when a negative value
|
||||||
used as a value for --keep-* options. It now interprets "-1" as forever,
|
was passed to the `--keep-*` options. Negative values are now forbidden. To
|
||||||
e.g. an option like --keep-monthly -1 will keep all monthly snapshots.
|
keep all snapshots, the special value `unlimited` is now supported. For
|
||||||
|
example, `--keep-monthly unlimited` will keep all monthly snapshots.
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/2565
|
https://github.com/restic/restic/issues/2565
|
||||||
https://github.com/restic/restic/pull/4234
|
https://github.com/restic/restic/pull/4234
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
@ -36,14 +37,49 @@ Exit status is 0 if the command was successful, and non-zero if there was any er
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ForgetPolicyCount int
|
||||||
|
|
||||||
|
var ErrNegativePolicyCount = errors.New("negative values not allowed, use 'unlimited' instead")
|
||||||
|
|
||||||
|
func (c *ForgetPolicyCount) Set(s string) error {
|
||||||
|
switch s {
|
||||||
|
case "unlimited":
|
||||||
|
*c = -1
|
||||||
|
default:
|
||||||
|
val, err := strconv.ParseInt(s, 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if val < 0 {
|
||||||
|
return ErrNegativePolicyCount
|
||||||
|
}
|
||||||
|
*c = ForgetPolicyCount(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ForgetPolicyCount) String() string {
|
||||||
|
switch *c {
|
||||||
|
case -1:
|
||||||
|
return "unlimited"
|
||||||
|
default:
|
||||||
|
return strconv.FormatInt(int64(*c), 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ForgetPolicyCount) Type() string {
|
||||||
|
return "n"
|
||||||
|
}
|
||||||
|
|
||||||
// ForgetOptions collects all options for the forget command.
|
// ForgetOptions collects all options for the forget command.
|
||||||
type ForgetOptions struct {
|
type ForgetOptions struct {
|
||||||
Last int
|
Last ForgetPolicyCount
|
||||||
Hourly int
|
Hourly ForgetPolicyCount
|
||||||
Daily int
|
Daily ForgetPolicyCount
|
||||||
Weekly int
|
Weekly ForgetPolicyCount
|
||||||
Monthly int
|
Monthly ForgetPolicyCount
|
||||||
Yearly int
|
Yearly ForgetPolicyCount
|
||||||
Within restic.Duration
|
Within restic.Duration
|
||||||
WithinHourly restic.Duration
|
WithinHourly restic.Duration
|
||||||
WithinDaily restic.Duration
|
WithinDaily restic.Duration
|
||||||
|
@ -67,12 +103,12 @@ func init() {
|
||||||
cmdRoot.AddCommand(cmdForget)
|
cmdRoot.AddCommand(cmdForget)
|
||||||
|
|
||||||
f := cmdForget.Flags()
|
f := cmdForget.Flags()
|
||||||
f.IntVarP(&forgetOptions.Last, "keep-last", "l", 0, "keep the last `n` snapshots (use '-1' to keep all snapshots)")
|
f.VarP(&forgetOptions.Last, "keep-last", "l", "keep the last `n` snapshots (use 'unlimited' to keep all snapshots)")
|
||||||
f.IntVarP(&forgetOptions.Hourly, "keep-hourly", "H", 0, "keep the last `n` hourly snapshots (use '-1' to keep all hourly snapshots)")
|
f.VarP(&forgetOptions.Hourly, "keep-hourly", "H", "keep the last `n` hourly snapshots (use 'unlimited' to keep all hourly snapshots)")
|
||||||
f.IntVarP(&forgetOptions.Daily, "keep-daily", "d", 0, "keep the last `n` daily snapshots (use '-1' to keep all daily snapshots)")
|
f.VarP(&forgetOptions.Daily, "keep-daily", "d", "keep the last `n` daily snapshots (use 'unlimited' to keep all daily snapshots)")
|
||||||
f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots (use '-1' to keep all weekly snapshots)")
|
f.VarP(&forgetOptions.Weekly, "keep-weekly", "w", "keep the last `n` weekly snapshots (use 'unlimited' to keep all weekly snapshots)")
|
||||||
f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots (use '-1' to keep all monthly snapshots)")
|
f.VarP(&forgetOptions.Monthly, "keep-monthly", "m", "keep the last `n` monthly snapshots (use 'unlimited' to keep all monthly snapshots)")
|
||||||
f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots (use '-1' to keep all yearly snapshots)")
|
f.VarP(&forgetOptions.Yearly, "keep-yearly", "y", "keep the last `n` yearly snapshots (use 'unlimited' to keep all yearly snapshots)")
|
||||||
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
f.VarP(&forgetOptions.WithinHourly, "keep-within-hourly", "", "keep hourly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
f.VarP(&forgetOptions.WithinHourly, "keep-within-hourly", "", "keep hourly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
f.VarP(&forgetOptions.WithinDaily, "keep-within-daily", "", "keep daily snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
f.VarP(&forgetOptions.WithinDaily, "keep-within-daily", "", "keep daily snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
|
@ -165,12 +201,12 @@ func runForget(ctx context.Context, opts ForgetOptions, gopts GlobalOptions, arg
|
||||||
}
|
}
|
||||||
|
|
||||||
policy := restic.ExpirePolicy{
|
policy := restic.ExpirePolicy{
|
||||||
Last: opts.Last,
|
Last: int(opts.Last),
|
||||||
Hourly: opts.Hourly,
|
Hourly: int(opts.Hourly),
|
||||||
Daily: opts.Daily,
|
Daily: int(opts.Daily),
|
||||||
Weekly: opts.Weekly,
|
Weekly: int(opts.Weekly),
|
||||||
Monthly: opts.Monthly,
|
Monthly: int(opts.Monthly),
|
||||||
Yearly: opts.Yearly,
|
Yearly: int(opts.Yearly),
|
||||||
Within: opts.Within,
|
Within: opts.Within,
|
||||||
WithinHourly: opts.WithinHourly,
|
WithinHourly: opts.WithinHourly,
|
||||||
WithinDaily: opts.WithinDaily,
|
WithinDaily: opts.WithinDaily,
|
||||||
|
|
|
@ -7,55 +7,84 @@ import (
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestForgetPolicyValues(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
input string
|
||||||
|
value ForgetPolicyCount
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{"0", ForgetPolicyCount(0), ""},
|
||||||
|
{"1", ForgetPolicyCount(1), ""},
|
||||||
|
{"unlimited", ForgetPolicyCount(-1), ""},
|
||||||
|
{"", ForgetPolicyCount(0), "strconv.ParseInt: parsing \"\": invalid syntax"},
|
||||||
|
{"-1", ForgetPolicyCount(0), ErrNegativePolicyCount.Error()},
|
||||||
|
{"abc", ForgetPolicyCount(0), "strconv.ParseInt: parsing \"abc\": invalid syntax"},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
var count ForgetPolicyCount
|
||||||
|
err := count.Set(testCase.input)
|
||||||
|
|
||||||
|
if testCase.err != "" {
|
||||||
|
rtest.Assert(t, err != nil, "should have returned error for input %+v", testCase.input)
|
||||||
|
rtest.Equals(t, testCase.err, err.Error())
|
||||||
|
} else {
|
||||||
|
rtest.Assert(t, err == nil, "expected no error for input %+v, got %v", testCase.input, err)
|
||||||
|
rtest.Equals(t, testCase.value, count)
|
||||||
|
rtest.Equals(t, testCase.input, count.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestForgetOptionValues(t *testing.T) {
|
func TestForgetOptionValues(t *testing.T) {
|
||||||
const negValErrorMsg = "Fatal: negative values other than -1 are not allowed for --keep-*"
|
const negValErrorMsg = "Fatal: negative values other than -1 are not allowed for --keep-*"
|
||||||
const negDurationValErrorMsg = "Fatal: durations containing negative values are not allowed for --keep-within*"
|
const negDurationValErrorMsg = "Fatal: durations containing negative values are not allowed for --keep-within*"
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
input ForgetOptions
|
input ForgetOptions
|
||||||
expectsError bool
|
|
||||||
errorMsg string
|
errorMsg string
|
||||||
}{
|
}{
|
||||||
{ForgetOptions{Last: 1}, false, ""},
|
{ForgetOptions{Last: 1}, ""},
|
||||||
{ForgetOptions{Hourly: 1}, false, ""},
|
{ForgetOptions{Hourly: 1}, ""},
|
||||||
{ForgetOptions{Daily: 1}, false, ""},
|
{ForgetOptions{Daily: 1}, ""},
|
||||||
{ForgetOptions{Weekly: 1}, false, ""},
|
{ForgetOptions{Weekly: 1}, ""},
|
||||||
{ForgetOptions{Monthly: 1}, false, ""},
|
{ForgetOptions{Monthly: 1}, ""},
|
||||||
{ForgetOptions{Yearly: 1}, false, ""},
|
{ForgetOptions{Yearly: 1}, ""},
|
||||||
{ForgetOptions{Last: 0}, false, ""},
|
{ForgetOptions{Last: 0}, ""},
|
||||||
{ForgetOptions{Hourly: 0}, false, ""},
|
{ForgetOptions{Hourly: 0}, ""},
|
||||||
{ForgetOptions{Daily: 0}, false, ""},
|
{ForgetOptions{Daily: 0}, ""},
|
||||||
{ForgetOptions{Weekly: 0}, false, ""},
|
{ForgetOptions{Weekly: 0}, ""},
|
||||||
{ForgetOptions{Monthly: 0}, false, ""},
|
{ForgetOptions{Monthly: 0}, ""},
|
||||||
{ForgetOptions{Yearly: 0}, false, ""},
|
{ForgetOptions{Yearly: 0}, ""},
|
||||||
{ForgetOptions{Last: -1}, false, ""},
|
{ForgetOptions{Last: -1}, ""},
|
||||||
{ForgetOptions{Hourly: -1}, false, ""},
|
{ForgetOptions{Hourly: -1}, ""},
|
||||||
{ForgetOptions{Daily: -1}, false, ""},
|
{ForgetOptions{Daily: -1}, ""},
|
||||||
{ForgetOptions{Weekly: -1}, false, ""},
|
{ForgetOptions{Weekly: -1}, ""},
|
||||||
{ForgetOptions{Monthly: -1}, false, ""},
|
{ForgetOptions{Monthly: -1}, ""},
|
||||||
{ForgetOptions{Yearly: -1}, false, ""},
|
{ForgetOptions{Yearly: -1}, ""},
|
||||||
{ForgetOptions{Last: -2}, true, negValErrorMsg},
|
{ForgetOptions{Last: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Hourly: -2}, true, negValErrorMsg},
|
{ForgetOptions{Hourly: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Daily: -2}, true, negValErrorMsg},
|
{ForgetOptions{Daily: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Weekly: -2}, true, negValErrorMsg},
|
{ForgetOptions{Weekly: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Monthly: -2}, true, negValErrorMsg},
|
{ForgetOptions{Monthly: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Yearly: -2}, true, negValErrorMsg},
|
{ForgetOptions{Yearly: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Within: restic.ParseDurationOrPanic("1y2m3d3h")}, false, ""},
|
{ForgetOptions{Within: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||||
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y2m3d3h")}, false, ""},
|
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||||
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m3d3h")}, false, ""},
|
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||||
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d3h")}, false, ""},
|
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||||
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("2y4m6d8h")}, false, ""},
|
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
||||||
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y4m6d8h")}, false, ""},
|
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
||||||
{ForgetOptions{Within: restic.ParseDurationOrPanic("-1y2m3d3h")}, true, negDurationValErrorMsg},
|
{ForgetOptions{Within: restic.ParseDurationOrPanic("-1y2m3d3h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y-2m3d3h")}, true, negDurationValErrorMsg},
|
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y-2m3d3h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m-3d3h")}, true, negDurationValErrorMsg},
|
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m-3d3h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d-3h")}, true, negDurationValErrorMsg},
|
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d-3h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("-2y4m6d8h")}, true, negDurationValErrorMsg},
|
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("-2y4m6d8h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y-4m6d8h")}, true, negDurationValErrorMsg},
|
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y-4m6d8h")}, negDurationValErrorMsg},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
err := verifyForgetOptions(&testCase.input)
|
err := verifyForgetOptions(&testCase.input)
|
||||||
if testCase.expectsError {
|
if testCase.errorMsg != "" {
|
||||||
rtest.Assert(t, err != nil, "should have returned error for input %+v", testCase.input)
|
rtest.Assert(t, err != nil, "should have returned error for input %+v", testCase.input)
|
||||||
rtest.Equals(t, testCase.errorMsg, err.Error())
|
rtest.Equals(t, testCase.errorMsg, err.Error())
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue