ea59896bd6
Fixes restic#719 If the option is passed, restic will wait the specified duration of time and retry locking the repo every 10 seconds (or more often if the total timeout is relatively small). - Play nice with json output - Reduce wait time in lock tests - Rework timeout last attempt - Reduce test wait time to 0.1s - Use exponential back off for the retry lock - Don't pass gopts to lockRepo functions - Use global variable for retry sleep setup - Exit retry lock on cancel - Better wording for flag help - Reorder debug statement - Refactor tests - Lower max sleep time to 1m - Test that we cancel/timeout in time - Use non blocking sleep function - Refactor into minDuration func Co-authored-by: Julian Brost <julian@0x4a42.net>
356 lines
8.9 KiB
Go
356 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/restic/restic/internal/restic"
|
|
"github.com/restic/restic/internal/ui/table"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var cmdSnapshots = &cobra.Command{
|
|
Use: "snapshots [flags] [snapshotID ...]",
|
|
Short: "List all snapshots",
|
|
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.
|
|
`,
|
|
DisableAutoGenTag: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runSnapshots(cmd.Context(), snapshotOptions, globalOptions, args)
|
|
},
|
|
}
|
|
|
|
// SnapshotOptions bundles all options for the snapshots command.
|
|
type SnapshotOptions struct {
|
|
restic.SnapshotFilter
|
|
Compact bool
|
|
Last bool // This option should be removed in favour of Latest.
|
|
Latest int
|
|
GroupBy restic.SnapshotGroupByOptions
|
|
}
|
|
|
|
var snapshotOptions SnapshotOptions
|
|
|
|
func init() {
|
|
cmdRoot.AddCommand(cmdSnapshots)
|
|
|
|
f := cmdSnapshots.Flags()
|
|
initMultiSnapshotFilter(f, &snapshotOptions.SnapshotFilter, true)
|
|
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")
|
|
err := f.MarkDeprecated("last", "use --latest 1")
|
|
if err != nil {
|
|
// MarkDeprecated only returns an error when the flag is not found
|
|
panic(err)
|
|
}
|
|
f.IntVar(&snapshotOptions.Latest, "latest", 0, "only show the last `n` snapshots for each host and path")
|
|
f.VarP(&snapshotOptions.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma")
|
|
}
|
|
|
|
func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions, args []string) error {
|
|
repo, err := OpenRepository(ctx, gopts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !gopts.NoLock {
|
|
var lock *restic.Lock
|
|
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
|
|
defer unlockRepo(lock)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var snapshots restic.Snapshots
|
|
for sn := range FindFilteredSnapshots(ctx, repo.Backend(), repo, &opts.SnapshotFilter, 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 {
|
|
// This branch should be removed in the same time
|
|
// that --last.
|
|
list = FilterLastestSnapshots(list, 1)
|
|
} else if opts.Latest > 0 {
|
|
list = FilterLastestSnapshots(list, opts.Latest)
|
|
}
|
|
sort.Sort(sort.Reverse(list))
|
|
snapshotGroups[k] = list
|
|
}
|
|
|
|
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, "|")}
|
|
}
|
|
|
|
// FilterLastestSnapshots filters a list of snapshots to only return
|
|
// the limit last entries for each hostname and path. If the snapshot
|
|
// contains multiple paths, they will be joined and treated as one
|
|
// item.
|
|
func FilterLastestSnapshots(list restic.Snapshots, limit int) 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]int)
|
|
for _, sn := range list {
|
|
key := newFilterLastSnapshotsKey(sn)
|
|
if seen[key] < limit {
|
|
seen[key]++
|
|
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" }}`)
|
|
}
|
|
tab.AddColumn("Paths", `{{ join .Paths "\n" }}`)
|
|
}
|
|
|
|
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(),
|
|
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
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|