restic/cmd/restic/cmd_snapshots.go

351 lines
8.8 KiB
Go
Raw Normal View History

2014-08-04 20:55:54 +00:00
package main
import (
"context"
"encoding/json"
2014-08-04 20:55:54 +00:00
"fmt"
"io"
"sort"
"strings"
2016-09-17 10:36:05 +00:00
2017-07-24 15:42:25 +00:00
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/table"
2016-09-17 10:36:05 +00:00
"github.com/spf13/cobra"
2014-08-04 20:55:54 +00:00
)
2016-09-17 10:36:05 +00:00
var cmdSnapshots = &cobra.Command{
Use: "snapshots [flags] [snapshotID ...]",
Short: "List all snapshots",
2016-09-17 10:36:05 +00:00
Long: `
The "snapshots" command lists all snapshots stored in the repository.
EXIT STATUS
===========
Exit status is 0 if the command was successful, and non-zero if there was any error.
2016-09-17 10:36:05 +00:00
`,
DisableAutoGenTag: true,
2016-09-17 10:36:05 +00:00
RunE: func(cmd *cobra.Command, args []string) error {
return runSnapshots(snapshotOptions, globalOptions, args)
},
2014-11-25 21:38:14 +00:00
}
2017-03-08 19:09:24 +00:00
// SnapshotOptions bundles all options for the snapshots command.
2016-09-17 10:36:05 +00:00
type SnapshotOptions struct {
Hosts []string
2017-09-10 22:09:28 +00:00
Tags restic.TagLists
Paths []string
Compact bool
Last bool
GroupBy string
2014-11-25 21:38:14 +00:00
}
2016-09-17 10:36:05 +00:00
var snapshotOptions SnapshotOptions
2014-12-07 15:30:52 +00:00
2014-11-30 21:39:58 +00:00
func init() {
2016-09-17 10:36:05 +00:00
cmdRoot.AddCommand(cmdSnapshots)
2014-12-07 15:30:52 +00:00
2016-09-17 10:36:05 +00:00
f := cmdSnapshots.Flags()
f.StringArrayVarP(&snapshotOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host` (can be specified multiple times)")
f.Var(&snapshotOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")
f.StringArrayVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)")
2020-10-02 13:55:56 +00:00
f.BoolVarP(&snapshotOptions.Compact, "compact", "c", false, "use compact output 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")
}
2016-09-17 10:36:05 +00:00
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
repo, err := OpenRepository(gopts)
2014-12-07 15:30:52 +00:00
if err != nil {
return err
2014-08-04 20:55:54 +00:00
}
2016-09-17 10:36:05 +00:00
if !gopts.NoLock {
2020-08-09 11:24:47 +00:00
lock, err := lockRepo(gopts.ctx, repo)
2016-09-17 10:36:05 +00:00
defer unlockRepo(lock)
if err != nil {
return err
}
2015-06-27 12:40:18 +00:00
}
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
var snapshots restic.Snapshots
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
snapshots = append(snapshots, sn)
}
snapshotGroups, grouped, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
if err != nil {
return err
}
for k, list := range snapshotGroups {
if opts.Last {
list = FilterLastSnapshots(list)
}
sort.Sort(sort.Reverse(list))
snapshotGroups[k] = list
}
2014-08-04 20:55:54 +00:00
if gopts.JSON {
err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, grouped)
if err != nil {
Warnf("error printing snapshots: %v\n", err)
}
return nil
}
for k, list := range snapshotGroups {
if grouped {
err := PrintSnapshotGroupHeader(gopts.stdout, k)
if err != nil {
Warnf("error printing snapshots: %v\n", err)
return nil
}
}
PrintSnapshots(gopts.stdout, list, nil, opts.Compact)
}
return nil
}
// filterLastSnapshotsKey is used by FilterLastSnapshots.
type filterLastSnapshotsKey struct {
Hostname string
JoinedPaths string
}
// newFilterLastSnapshotsKey initializes a filterLastSnapshotsKey from a Snapshot
func newFilterLastSnapshotsKey(sn *restic.Snapshot) filterLastSnapshotsKey {
// Shallow slice copy
var paths = make([]string, len(sn.Paths))
copy(paths, sn.Paths)
sort.Strings(paths)
return filterLastSnapshotsKey{sn.Hostname, strings.Join(paths, "|")}
}
// FilterLastSnapshots filters a list of snapshots to only return the last
// entry for each hostname and path. If the snapshot contains multiple paths,
// they will be joined and treated as one item.
func FilterLastSnapshots(list restic.Snapshots) restic.Snapshots {
// Sort the snapshots so that the newer ones are listed first
sort.SliceStable(list, func(i, j int) bool {
return list[i].Time.After(list[j].Time)
})
var results restic.Snapshots
seen := make(map[filterLastSnapshotsKey]bool)
for _, sn := range list {
key := newFilterLastSnapshotsKey(sn)
if !seen[key] {
seen[key] = true
results = append(results, sn)
}
}
return results
}
// PrintSnapshots prints a text table of the snapshots in list to stdout.
func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.KeepReason, compact bool) {
// keep the reasons a snasphot is being kept in a map, so that it doesn't
// get lost when the list of snapshots is sorted
keepReasons := make(map[restic.ID]restic.KeepReason, len(reasons))
if len(reasons) > 0 {
for i, sn := range list {
id := sn.ID()
keepReasons[*id] = reasons[i]
}
}
// always sort the snapshots so that the newer ones are listed last
sort.SliceStable(list, func(i, j int) bool {
return list[i].Time.Before(list[j].Time)
})
// Determine the max widths for host and tag.
maxHost, maxTag := 10, 6
for _, sn := range list {
if len(sn.Hostname) > maxHost {
maxHost = len(sn.Hostname)
}
for _, tag := range sn.Tags {
if len(tag) > maxTag {
maxTag = len(tag)
}
}
}
tab := table.New()
if compact {
tab.AddColumn("ID", "{{ .ID }}")
tab.AddColumn("Time", "{{ .Timestamp }}")
tab.AddColumn("Host", "{{ .Hostname }}")
tab.AddColumn("Tags ", `{{ join .Tags "\n" }}`)
} else {
tab.AddColumn("ID", "{{ .ID }}")
tab.AddColumn("Time", "{{ .Timestamp }}")
tab.AddColumn("Host ", "{{ .Hostname }}")
tab.AddColumn("Tags ", `{{ join .Tags "," }}`)
if len(reasons) > 0 {
tab.AddColumn("Reasons", `{{ join .Reasons "\n" }}`)
2015-03-02 13:48:47 +00:00
}
tab.AddColumn("Paths", `{{ join .Paths "\n" }}`)
}
2015-03-02 13:48:47 +00:00
type snapshot struct {
ID string
Timestamp string
Hostname string
Tags []string
Reasons []string
Paths []string
}
var multiline bool
for _, sn := range list {
data := snapshot{
ID: sn.ID().Str(),
2018-11-02 19:36:15 +00:00
Timestamp: sn.Time.Local().Format(TimeFormat),
Hostname: sn.Hostname,
Tags: sn.Tags,
Paths: sn.Paths,
}
if len(reasons) > 0 {
id := sn.ID()
data.Reasons = keepReasons[*id].Matches
}
if len(sn.Paths) > 1 && !compact {
multiline = true
}
tab.AddRow(data)
}
tab.AddFooter(fmt.Sprintf("%d snapshots", len(list)))
if multiline {
// print an additional blank line between snapshots
var last int
tab.PrintData = func(w io.Writer, idx int, s string) error {
var err error
if idx == last {
_, err = fmt.Fprintf(w, "%s\n", s)
} else {
_, err = fmt.Fprintf(w, "\n%s\n", s)
}
last = idx
return err
2015-03-02 13:48:47 +00:00
}
}
2021-01-30 16:25:10 +00:00
err := tab.Write(stdout)
if err != nil {
Warnf("error printing: %v\n", err)
}
}
// 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) error {
var key restic.SnapshotGroupKey
err := json.Unmarshal([]byte(groupKeyJSON), &key)
if err != nil {
return err
}
if key.Hostname == "" && key.Tags == nil && key.Paths == nil {
return nil
}
// Info
fmt.Fprintf(stdout, "snapshots")
var infoStrings []string
if key.Hostname != "" {
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
}
if key.Tags != nil {
infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]")
}
if key.Paths != nil {
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
ID *restic.ID `json:"id"`
ShortID string `json:"short_id"`
}
// SnapshotGroup helps to print SnaphotGroups as JSON with their GroupReasons included.
type SnapshotGroup struct {
GroupKey restic.SnapshotGroupKey `json:"group_key"`
Snapshots []Snapshot `json:"snapshots"`
}
// printSnapshotsJSON writes the JSON representation of list to stdout.
func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]restic.Snapshots, grouped bool) error {
if grouped {
snapshotGroups := []SnapshotGroup{}
for k, list := range snGroups {
var key restic.SnapshotGroupKey
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)
}
// Old behavior
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)
2014-08-04 20:55:54 +00:00
}