Add custom Duration type

This commit is contained in:
Alexander Neumann 2018-05-13 12:02:21 +02:00
parent 5a0f0e3faa
commit cc627e832b
5 changed files with 250 additions and 22 deletions

View File

@ -33,7 +33,7 @@ type ForgetOptions struct {
Weekly int
Monthly int
Yearly int
WithinDays int
Within restic.Duration
KeepTags restic.TagLists
Host string
@ -59,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.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)")
// 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,
Monthly: opts.Monthly,
Yearly: opts.Yearly,
Within: opts.WithinDays,
Within: opts.Within,
Tags: opts.KeepTags,
}

131
internal/restic/duration.go Normal file
View 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
}

View 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)
}
})
}
}

View File

@ -16,7 +16,7 @@ type ExpirePolicy struct {
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
Within Duration // keep snapshots made within this duration
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 {
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
@ -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 p.Within != 0 {
t := latest.AddDate(0, 0, -p.Within)
if !p.Within.Zero() {
t := latest.AddDate(-p.Within.Years, -p.Within.Months, -p.Within.Days)
if cur.Time.After(t) {
keepSnap = true
}

View File

@ -21,8 +21,8 @@ func parseTimeUTC(s string) time.Time {
return t.UTC()
}
func parseDuration(s string) time.Duration {
d, err := time.ParseDuration(s)
func parseDuration(s string) restic.Duration {
d, err := restic.ParseDuration(s)
if err != nil {
panic(err)
}
@ -180,10 +180,10 @@ var expireTests = []restic.ExpirePolicy{
{Tags: []restic.TagList{{"foo"}}},
{Tags: []restic.TagList{{"foo", "bar"}}},
{Tags: []restic.TagList{{"foo"}, {"bar"}}},
{Within: 1},
{Within: 2},
{Within: 7},
{Within: 30},
{Within: parseDuration("1d")},
{Within: parseDuration("2d")},
{Within: parseDuration("7d")},
{Within: parseDuration("1m")},
}
func TestApplyPolicy(t *testing.T) {