Refactor `forget` and `snapshots` command

Implement filtering by using `FindFilteredSnapshots()` to iterate over the snapshots

Refactor cmd_snapshots' `PrintSnapshots()` so its pretty printing can be used from
both `forget` and `snapshots`.

Use contexts.
This commit is contained in:
Pauline Middelink 2017-03-08 20:24:58 +01:00
parent 11d237c252
commit 3c6c17abcd
2 changed files with 57 additions and 106 deletions

View File

@ -1,14 +1,12 @@
package main package main
import ( import (
"encoding/hex" "context"
"encoding/json" "encoding/json"
"restic" "restic"
"sort" "sort"
"strings" "strings"
"restic/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -27,13 +25,12 @@ 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
KeepTags []string KeepTags []string
Host string Host string
@ -83,32 +80,43 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
return err return err
} }
// Process all snapshot IDs given as arguments. // group by hostname and dirs
if len(args) != 0 { type key struct {
for _, s := range args { Hostname string
// Parse argument as hex string. Paths []string
if _, err := hex.DecodeString(s); err != nil { Tags []string
Warnf("argument %q is not a snapshot ID, ignoring\n", s) }
continue snapshotGroups := make(map[string]restic.Snapshots)
}
id, err := restic.FindSnapshot(repo, s)
if err != nil {
Warnf("could not find a snapshot for ID %q, ignoring\n", s)
continue
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
if len(args) > 0 {
// When explicit snapshots args are given, remove them immediately.
if !opts.DryRun { if !opts.DryRun {
h := restic.Handle{Type: restic.SnapshotFile, Name: id.String()} h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
err = repo.Backend().Remove(h) if err = repo.Backend().Remove(h); err != nil {
if err != nil {
return err return err
} }
Verbosef("removed snapshot %v\n", sn.ID().Str())
Verbosef("removed snapshot %v\n", id.Str())
} else { } else {
Verbosef("would remove snapshot %v\n", id.Str()) Verbosef("would have removed snapshot %v\n", sn.ID().Str())
} }
} else {
var tags []string
if opts.GroupByTags {
tags = sn.Tags
sort.StringSlice(tags).Sort()
}
sort.StringSlice(sn.Paths).Sort()
k, err := json.Marshal(key{Hostname: sn.Hostname, Tags: tags, Paths: sn.Paths})
if err != nil {
return err
}
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
} }
}
if len(args) > 0 {
return nil return nil
} }
@ -122,53 +130,17 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
Tags: opts.KeepTags, Tags: opts.KeepTags,
} }
snapshots, err := restic.LoadAllSnapshots(repo)
if err != nil {
return err
}
// Group snapshots by hostname and dirs.
type key struct {
Hostname string
Paths []string
Tags []string
}
snapshotGroups := make(map[string]restic.Snapshots)
for _, sn := range snapshots {
if opts.Host != "" && sn.Hostname != opts.Host {
continue
}
if !sn.HasTags(opts.Tags) {
continue
}
if !sn.HasPaths(opts.Paths) {
continue
}
var tags []string
if opts.GroupByTags {
sort.StringSlice(sn.Tags).Sort()
tags = sn.Tags
}
sort.StringSlice(sn.Paths).Sort()
k, _ := json.Marshal(key{Hostname: sn.Hostname, Tags: tags, Paths: sn.Paths})
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
}
if len(snapshotGroups) == 0 {
return errors.Fatal("no snapshots remained after filtering")
}
if policy.Empty() { if policy.Empty() {
Verbosef("no policy was specified, no snapshots will be removed\n") Verbosef("no policy was specified, no snapshots will be removed\n")
return nil
} }
removeSnapshots := 0 removeSnapshots := 0
for k, snapshotGroup := range snapshotGroups { for k, snapshotGroup := range snapshotGroups {
var key key var key key
json.Unmarshal([]byte(k), &key) if json.Unmarshal([]byte(k), &key) != nil {
return err
}
if opts.GroupByTags { if opts.GroupByTags {
Printf("snapshots for host %v, tags [%v], paths: [%v]:\n\n", key.Hostname, strings.Join(key.Tags, ", "), strings.Join(key.Paths, ", ")) Printf("snapshots for host %v, tags [%v], paths: [%v]:\n\n", key.Hostname, strings.Join(key.Tags, ", "), strings.Join(key.Paths, ", "))
} else { } else {

View File

@ -1,19 +1,19 @@
package main package main
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"restic/errors"
"sort" "sort"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"encoding/json"
"restic" "restic"
) )
var cmdSnapshots = &cobra.Command{ var cmdSnapshots = &cobra.Command{
Use: "snapshots", Use: "snapshots [snapshotID ...]",
Short: "list all snapshots", Short: "list all snapshots",
Long: ` Long: `
The "snapshots" command lists all snapshots stored in the repository. The "snapshots" command lists all snapshots stored in the repository.
@ -26,6 +26,7 @@ The "snapshots" command lists all snapshots stored in the repository.
// SnapshotOptions bundles all options for the snapshots command. // SnapshotOptions bundles all options for the snapshots command.
type SnapshotOptions struct { type SnapshotOptions struct {
Host string Host string
Tags []string
Paths []string Paths []string
} }
@ -35,15 +36,12 @@ func init() {
cmdRoot.AddCommand(cmdSnapshots) cmdRoot.AddCommand(cmdSnapshots)
f := cmdSnapshots.Flags() f := cmdSnapshots.Flags()
f.StringVar(&snapshotOptions.Host, "host", "", "only print snapshots for this host") f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`")
f.StringSliceVar(&snapshotOptions.Paths, "path", []string{}, "only print snapshots for this `path` (can be specified multiple times)") f.StringSliceVar(&snapshotOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)")
f.StringSliceVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)")
} }
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error { func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 {
return errors.Fatal("wrong number of arguments")
}
repo, err := OpenRepository(gopts) repo, err := OpenRepository(gopts)
if err != nil { if err != nil {
return err return err
@ -57,32 +55,14 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
} }
} }
done := make(chan struct{}) ctx, cancel := context.WithCancel(gopts.ctx)
defer close(done) defer cancel()
list := []*restic.Snapshot{}
for id := range repo.List(restic.SnapshotFile, done) {
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
Warnf("error loading snapshot %s: %v\n", id, err)
continue
}
if (opts.Host == "" || opts.Host == sn.Hostname) && sn.HasPaths(opts.Paths) {
pos := sort.Search(len(list), func(i int) bool {
return list[i].Time.After(sn.Time)
})
if pos < len(list) {
list = append(list, nil)
copy(list[pos+1:], list[pos:])
list[pos] = sn
} else {
list = append(list, sn)
}
}
var list restic.Snapshots
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
list = append(list, sn)
} }
sort.Sort(sort.Reverse(list))
if gopts.JSON { if gopts.JSON {
err := printSnapshotsJSON(gopts.stdout, list) err := printSnapshotsJSON(gopts.stdout, list)
@ -97,7 +77,7 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
} }
// PrintSnapshots prints a text table of the snapshots in list to stdout. // PrintSnapshots prints a text table of the snapshots in list to stdout.
func PrintSnapshots(stdout io.Writer, list []*restic.Snapshot) { func PrintSnapshots(stdout io.Writer, list restic.Snapshots) {
// Determine the max widths for host and tag. // Determine the max widths for host and tag.
maxHost, maxTag := 10, 6 maxHost, maxTag := 10, 6
@ -165,7 +145,7 @@ func PrintSnapshots(stdout io.Writer, list []*restic.Snapshot) {
tab.Write(stdout) tab.Write(stdout)
} }
// Snapshot helps to print Snaphots as JSON // Snapshot helps to print Snaphots as JSON with their ID included.
type Snapshot struct { type Snapshot struct {
*restic.Snapshot *restic.Snapshot
@ -173,7 +153,7 @@ type Snapshot struct {
} }
// printSnapshotsJSON writes the JSON representation of list to stdout. // printSnapshotsJSON writes the JSON representation of list to stdout.
func printSnapshotsJSON(stdout io.Writer, list []*restic.Snapshot) error { func printSnapshotsJSON(stdout io.Writer, list restic.Snapshots) error {
var snapshots []Snapshot var snapshots []Snapshot
@ -187,5 +167,4 @@ func printSnapshotsJSON(stdout io.Writer, list []*restic.Snapshot) error {
} }
return json.NewEncoder(stdout).Encode(snapshots) return json.NewEncoder(stdout).Encode(snapshots)
} }