forked from TrueCloudLab/restic
Add custom Duration type
This commit is contained in:
parent
5a0f0e3faa
commit
cc627e832b
5 changed files with 250 additions and 22 deletions
|
@ -27,14 +27,14 @@ data after 'forget' was run successfully, see the 'prune' command. `,
|
||||||
|
|
||||||
// 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 int
|
||||||
Hourly int
|
Hourly int
|
||||||
Daily int
|
Daily int
|
||||||
Weekly int
|
Weekly int
|
||||||
Monthly int
|
Monthly int
|
||||||
Yearly int
|
Yearly int
|
||||||
WithinDays int
|
Within restic.Duration
|
||||||
KeepTags restic.TagLists
|
KeepTags restic.TagLists
|
||||||
|
|
||||||
Host string
|
Host string
|
||||||
Tags restic.TagLists
|
Tags restic.TagLists
|
||||||
|
@ -59,7 +59,7 @@ func init() {
|
||||||
f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots")
|
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.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.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
|
||||||
f.IntVar(&forgetOptions.WithinDays, "keep-within", 0, "keep snapshots that were created within `days` before the newest")
|
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that were created within `duration` before the newest (e.g. 1y5m7d)")
|
||||||
|
|
||||||
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
|
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.
|
// Sadly the commonly used shortcut `H` is already used.
|
||||||
|
@ -172,7 +172,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||||
Weekly: opts.Weekly,
|
Weekly: opts.Weekly,
|
||||||
Monthly: opts.Monthly,
|
Monthly: opts.Monthly,
|
||||||
Yearly: opts.Yearly,
|
Yearly: opts.Yearly,
|
||||||
Within: opts.WithinDays,
|
Within: opts.Within,
|
||||||
Tags: opts.KeepTags,
|
Tags: opts.KeepTags,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
131
internal/restic/duration.go
Normal file
131
internal/restic/duration.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Duration is similar to time.Duration, except it only supports larger ranges
|
||||||
|
// like days, months, and years.
|
||||||
|
type Duration struct {
|
||||||
|
Days, Months, Years int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) String() string {
|
||||||
|
var s string
|
||||||
|
if d.Years != 0 {
|
||||||
|
s += fmt.Sprintf("%dy", d.Years)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Months != 0 {
|
||||||
|
s += fmt.Sprintf("%dm", d.Months)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Days != 0 {
|
||||||
|
s += fmt.Sprintf("%dd", d.Days)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextNumber(input string) (num int, rest string, err error) {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return 0, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
n string
|
||||||
|
negative bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if input[0] == '-' {
|
||||||
|
negative = true
|
||||||
|
input = input[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range input {
|
||||||
|
if !unicode.IsNumber(s) {
|
||||||
|
rest = input[i:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
n += string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n) == 0 {
|
||||||
|
return 0, input, errors.New("no number found")
|
||||||
|
}
|
||||||
|
|
||||||
|
num, err = strconv.Atoi(n)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if negative {
|
||||||
|
num = -num
|
||||||
|
}
|
||||||
|
|
||||||
|
return num, rest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDuration parses a duration from a string. The format is:
|
||||||
|
// 6y5m234d
|
||||||
|
func ParseDuration(s string) (Duration, error) {
|
||||||
|
var (
|
||||||
|
d Duration
|
||||||
|
num int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
|
for s != "" {
|
||||||
|
num, s, err = nextNumber(s)
|
||||||
|
if err != nil {
|
||||||
|
return Duration{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) == 0 {
|
||||||
|
return Duration{}, errors.Errorf("no unit found after number %d", num)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s[0] {
|
||||||
|
case 'y':
|
||||||
|
d.Years = num
|
||||||
|
case 'm':
|
||||||
|
d.Months = num
|
||||||
|
case 'd':
|
||||||
|
d.Days = num
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set calls ParseDuration and updates d.
|
||||||
|
func (d *Duration) Set(s string) error {
|
||||||
|
v, err := ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*d = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the type of Duration, usable within github.com/spf13/pflag and
|
||||||
|
// in help texts.
|
||||||
|
func (d Duration) Type() string {
|
||||||
|
return "duration"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero returns true if the duration is empty (all values are set to zero).
|
||||||
|
func (d Duration) Zero() bool {
|
||||||
|
return d.Years == 0 && d.Months == 0 && d.Days == 0
|
||||||
|
}
|
82
internal/restic/duration_test.go
Normal file
82
internal/restic/duration_test.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package restic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNextNumber(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
input string
|
||||||
|
num int
|
||||||
|
rest string
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "3d", num: 3, rest: "d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "7m5d", num: 7, rest: "m5d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "-23y7m5d", num: -23, rest: "y7m5d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: " 5d", num: 0, rest: " 5d", err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "5d ", num: 5, rest: "d ",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
num, rest, err := nextNumber(test.input)
|
||||||
|
|
||||||
|
if err != nil && !test.err {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if num != test.num {
|
||||||
|
t.Errorf("wrong num, want %d, got %d", test.num, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rest != test.rest {
|
||||||
|
t.Errorf("wrong rest, want %q, got %q", test.rest, rest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDuration(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
input string
|
||||||
|
d Duration
|
||||||
|
output string
|
||||||
|
}{
|
||||||
|
{"3d", Duration{Days: 3}, "3d"},
|
||||||
|
{"7m5d", Duration{Months: 7, Days: 5}, "7m5d"},
|
||||||
|
{"5d7m", Duration{Months: 7, Days: 5}, "7m5d"},
|
||||||
|
{"-7m5d", Duration{Months: -7, Days: 5}, "-7m5d"},
|
||||||
|
{"2y7m-5d", Duration{Years: 2, Months: 7, Days: -5}, "2y7m-5d"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
d, err := ParseDuration(test.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmp.Equal(d, test.d) {
|
||||||
|
t.Error(cmp.Diff(test.d, d))
|
||||||
|
}
|
||||||
|
|
||||||
|
s := d.String()
|
||||||
|
if s != test.output {
|
||||||
|
t.Errorf("unexpected return of String(), want %q, got %q", test.output, s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ type ExpirePolicy struct {
|
||||||
Weekly int // keep the last n weekly snapshots
|
Weekly int // keep the last n weekly snapshots
|
||||||
Monthly int // keep the last n monthly snapshots
|
Monthly int // keep the last n monthly snapshots
|
||||||
Yearly int // keep the last n yearly snapshots
|
Yearly int // keep the last n yearly snapshots
|
||||||
Within int // keep snapshots made within this number of days since the newest snapshot
|
Within Duration // keep snapshots made within this duration
|
||||||
Tags []TagList // keep all snapshots that include at least one of the tag lists.
|
Tags []TagList // keep all snapshots that include at least one of the tag lists.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,11 +40,26 @@ func (e ExpirePolicy) String() (s string) {
|
||||||
if e.Yearly > 0 {
|
if e.Yearly > 0 {
|
||||||
keeps = append(keeps, fmt.Sprintf("%d yearly", e.Yearly))
|
keeps = append(keeps, fmt.Sprintf("%d yearly", e.Yearly))
|
||||||
}
|
}
|
||||||
if e.Within != 0 {
|
|
||||||
keeps = append(keeps, fmt.Sprintf("snapshots within %d days of the newest snapshot", e.Within))
|
if len(keeps) > 0 {
|
||||||
|
s = fmt.Sprintf("keep the last %s snapshots", strings.Join(keeps, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("keep the last %s snapshots", strings.Join(keeps, ", "))
|
if len(e.Tags) > 0 {
|
||||||
|
if s != "" {
|
||||||
|
s += " and "
|
||||||
|
}
|
||||||
|
s += fmt.Sprintf("all snapshots with tags %s", e.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.Within.Zero() {
|
||||||
|
if s != "" {
|
||||||
|
s += " and "
|
||||||
|
}
|
||||||
|
s += fmt.Sprintf("all snapshots within %s of the newest", e.Within)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum returns the maximum number of snapshots to be kept according to this
|
// Sum returns the maximum number of snapshots to be kept according to this
|
||||||
|
@ -149,8 +164,8 @@ func ApplyPolicy(list Snapshots, p ExpirePolicy) (keep, remove Snapshots) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the timestamp of the snapshot is within the range, then keep it.
|
// If the timestamp of the snapshot is within the range, then keep it.
|
||||||
if p.Within != 0 {
|
if !p.Within.Zero() {
|
||||||
t := latest.AddDate(0, 0, -p.Within)
|
t := latest.AddDate(-p.Within.Years, -p.Within.Months, -p.Within.Days)
|
||||||
if cur.Time.After(t) {
|
if cur.Time.After(t) {
|
||||||
keepSnap = true
|
keepSnap = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ func parseTimeUTC(s string) time.Time {
|
||||||
return t.UTC()
|
return t.UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDuration(s string) time.Duration {
|
func parseDuration(s string) restic.Duration {
|
||||||
d, err := time.ParseDuration(s)
|
d, err := restic.ParseDuration(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -180,10 +180,10 @@ var expireTests = []restic.ExpirePolicy{
|
||||||
{Tags: []restic.TagList{{"foo"}}},
|
{Tags: []restic.TagList{{"foo"}}},
|
||||||
{Tags: []restic.TagList{{"foo", "bar"}}},
|
{Tags: []restic.TagList{{"foo", "bar"}}},
|
||||||
{Tags: []restic.TagList{{"foo"}, {"bar"}}},
|
{Tags: []restic.TagList{{"foo"}, {"bar"}}},
|
||||||
{Within: 1},
|
{Within: parseDuration("1d")},
|
||||||
{Within: 2},
|
{Within: parseDuration("2d")},
|
||||||
{Within: 7},
|
{Within: parseDuration("7d")},
|
||||||
{Within: 30},
|
{Within: parseDuration("1m")},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyPolicy(t *testing.T) {
|
func TestApplyPolicy(t *testing.T) {
|
||||||
|
|
Loading…
Reference in a new issue