From cadcab5a196b768add304eb3a1aeb2c67ad3bfef Mon Sep 17 00:00:00 2001 From: Jan Niklas Richter Date: Wed, 14 Nov 2018 16:23:00 +0100 Subject: [PATCH] Add GroupBy option to snapshots command This commit adds a --group-by option to the snapshots command, which behaves similar to the --group-by option of forget. Valid option values are "host, paths, tags". If this option is given, the output of snapshots will be divided into multiple tables, according to the value given (i.e. "host" will create a table of snapshots for each host, that has a snapshot in the list). Also the JSON output will be grouped. The default behavior (when --group-by is not given) has not changed. More to this discussion can be found in issue #2037. --- cmd/restic/cmd_snapshots.go | 192 +++++++++++++++++++++++++++++++----- 1 file changed, 167 insertions(+), 25 deletions(-) diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index d9623b942..5ae5cfe85 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui/table" "github.com/spf13/cobra" @@ -32,6 +33,7 @@ type SnapshotOptions struct { Paths []string Compact bool Last bool + GroupBy string } var snapshotOptions SnapshotOptions @@ -45,6 +47,13 @@ func init() { f.StringArrayVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)") f.BoolVarP(&snapshotOptions.Compact, "compact", "c", false, "use compact format") f.BoolVar(&snapshotOptions.Last, "last", false, "only show the last snapshot for each host and path") + f.StringVarP(&snapshotOptions.GroupBy, "group-by", "g", "", "string for grouping snapshots by host,paths,tags") +} + +type groupKey struct { + Hostname string + Paths []string + Tags []string } func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error { @@ -61,28 +70,87 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro } } + // group by hostname and dirs + snapshotGroups := make(map[string]restic.Snapshots) + + var GroupByTag bool + var GroupByHost bool + var GroupByPath bool + var GroupOptionList []string + + GroupOptionList = strings.Split(opts.GroupBy, ",") + + for _, option := range GroupOptionList { + switch option { + case "host": + GroupByHost = true + case "paths": + GroupByPath = true + case "tags": + GroupByTag = true + case "": + default: + return errors.Fatal("unknown grouping option: '" + option + "'") + } + } + ctx, cancel := context.WithCancel(gopts.ctx) defer cancel() - var list restic.Snapshots for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { - list = append(list, sn) + // Determining grouping-keys + var tags []string + var hostname string + var paths []string + + if GroupByTag { + tags = sn.Tags + sort.StringSlice(tags).Sort() + } + if GroupByHost { + hostname = sn.Hostname + } + if GroupByPath { + paths = sn.Paths + } + + sort.StringSlice(sn.Paths).Sort() + var k []byte + var err error + + k, err = json.Marshal(groupKey{Tags: tags, Hostname: hostname, Paths: paths}) + + if err != nil { + return err + } + snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn) } - if opts.Last { - list = FilterLastSnapshots(list) + for k, list := range snapshotGroups { + if opts.Last { + list = FilterLastSnapshots(list) + } + sort.Sort(sort.Reverse(list)) + snapshotGroups[k] = list } - sort.Sort(sort.Reverse(list)) - if gopts.JSON { - err := printSnapshotsJSON(gopts.stdout, list) + err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, GroupByTag || GroupByHost || GroupByPath) if err != nil { - Warnf("error printing snapshot: %v\n", err) + Warnf("error printing snapshots: %v\n", err) } return nil } - PrintSnapshots(gopts.stdout, list, nil, opts.Compact) + + for k, list := range snapshotGroups { + err := PrintSnapshotGroupHeader(gopts.stdout, k, GroupByTag, GroupByHost, GroupByPath) + if err != nil { + Warnf("error printing snapshots: %v\n", err) + return nil + } + + PrintSnapshots(gopts.stdout, list, nil, opts.Compact) + } return nil } @@ -223,6 +291,40 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke tab.Write(stdout) } +// PrintSnapshotGroupHeader prints which group of the group-by option the +// following snapshots belong to. +// Prints nothing, if we did not group at all. +func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string, GroupByTag bool, GroupByHost bool, GroupByPath bool) error { + if GroupByTag || GroupByHost || GroupByPath { + var key groupKey + var err error + + err = json.Unmarshal([]byte(groupKeyJSON), &key) + if err != nil { + return err + } + + // Info + fmt.Fprintf(stdout, "snapshots") + var infoStrings []string + if GroupByTag { + infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]") + } + if GroupByHost { + infoStrings = append(infoStrings, "host ["+key.Hostname+"]") + } + if GroupByPath { + infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]") + } + if infoStrings != nil { + fmt.Fprintf(stdout, " for (%s)", strings.Join(infoStrings, ", ")) + } + fmt.Fprintf(stdout, ":\n") + } + + return nil +} + // Snapshot helps to print Snaphots as JSON with their ID included. type Snapshot struct { *restic.Snapshot @@ -231,20 +333,60 @@ type Snapshot struct { ShortID string `json:"short_id"` } -// printSnapshotsJSON writes the JSON representation of list to stdout. -func printSnapshotsJSON(stdout io.Writer, list restic.Snapshots) error { - - var snapshots []Snapshot - - for _, sn := range list { - - k := Snapshot{ - Snapshot: sn, - ID: sn.ID(), - ShortID: sn.ID().Str(), - } - snapshots = append(snapshots, k) - } - - return json.NewEncoder(stdout).Encode(snapshots) +// SnapshotGroup helps to print SnaphotGroups as JSON with their GroupReasons included. +type SnapshotGroup struct { + GroupKey groupKey + Snapshots []Snapshot +} + +// printSnapshotsJSON writes the JSON representation of list to stdout. +func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]restic.Snapshots, grouped bool) error { + + if grouped { + var snapshotGroups []SnapshotGroup + + for k, list := range snGroups { + var key groupKey + var err error + var snapshots []Snapshot + + err = json.Unmarshal([]byte(k), &key) + if err != nil { + return err + } + + for _, sn := range list { + k := Snapshot{ + Snapshot: sn, + ID: sn.ID(), + ShortID: sn.ID().Str(), + } + snapshots = append(snapshots, k) + } + + group := SnapshotGroup{ + GroupKey: key, + Snapshots: snapshots, + } + snapshotGroups = append(snapshotGroups, group) + } + + return json.NewEncoder(stdout).Encode(snapshotGroups) + } else { + // Old behavior + var snapshots []Snapshot + + for _, list := range snGroups { + for _, sn := range list { + k := Snapshot{ + Snapshot: sn, + ID: sn.ID(), + ShortID: sn.ID().Str(), + } + snapshots = append(snapshots, k) + } + } + + return json.NewEncoder(stdout).Encode(snapshots) + } }