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

@ -27,14 +27,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
WithinDays int
KeepTags restic.TagLists
Last int
Hourly int
Daily int
Weekly int
Monthly int
Yearly int
Within restic.Duration
KeepTags restic.TagLists
Host string
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.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) {