mirror of
https://github.com/octoleo/restic.git
synced 2024-11-10 23:31:09 +00:00
c5da90a5b7
Add --last flag to snapshots command to only show the last entry for any (hostname, paths) combination. This makes it easier to check when various paths were last backed up.
240 lines
5.8 KiB
Go
240 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/restic/restic/internal/restic"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var cmdSnapshots = &cobra.Command{
|
|
Use: "snapshots [snapshotID ...]",
|
|
Short: "List all snapshots",
|
|
Long: `
|
|
The "snapshots" command lists all snapshots stored in the repository.
|
|
`,
|
|
DisableAutoGenTag: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runSnapshots(snapshotOptions, globalOptions, args)
|
|
},
|
|
}
|
|
|
|
// SnapshotOptions bundles all options for the snapshots command.
|
|
type SnapshotOptions struct {
|
|
Host string
|
|
Tags restic.TagLists
|
|
Paths []string
|
|
Compact bool
|
|
Last bool
|
|
}
|
|
|
|
var snapshotOptions SnapshotOptions
|
|
|
|
func init() {
|
|
cmdRoot.AddCommand(cmdSnapshots)
|
|
|
|
f := cmdSnapshots.Flags()
|
|
f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`")
|
|
f.Var(&snapshotOptions.Tags, "tag", "only consider snapshots which include this `taglist` (can be specified multiple times)")
|
|
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")
|
|
}
|
|
|
|
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
|
|
repo, err := OpenRepository(gopts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !gopts.NoLock {
|
|
lock, err := lockRepo(repo)
|
|
defer unlockRepo(lock)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if opts.Last {
|
|
list = FilterLastSnapshots(list)
|
|
}
|
|
|
|
sort.Sort(sort.Reverse(list))
|
|
|
|
if gopts.JSON {
|
|
err := printSnapshotsJSON(gopts.stdout, list)
|
|
if err != nil {
|
|
Warnf("error printing snapshot: %v\n", err)
|
|
}
|
|
return nil
|
|
}
|
|
PrintSnapshots(gopts.stdout, list, 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, compact bool) {
|
|
|
|
// 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 := NewTable()
|
|
if !compact {
|
|
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s %-3s %s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags", "", "Directory")
|
|
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%%ds %%-3s %%s", -maxHost, -maxTag)
|
|
} else {
|
|
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags")
|
|
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%s", -maxHost)
|
|
}
|
|
|
|
for _, sn := range list {
|
|
if len(sn.Paths) == 0 {
|
|
continue
|
|
}
|
|
|
|
firstTag := ""
|
|
if len(sn.Tags) > 0 {
|
|
firstTag = sn.Tags[0]
|
|
}
|
|
|
|
rows := len(sn.Paths)
|
|
if rows < len(sn.Tags) {
|
|
rows = len(sn.Tags)
|
|
}
|
|
|
|
treeElement := " "
|
|
if rows != 1 {
|
|
treeElement = "┌──"
|
|
}
|
|
|
|
if !compact {
|
|
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, treeElement, sn.Paths[0]})
|
|
} else {
|
|
allTags := ""
|
|
for _, tag := range sn.Tags {
|
|
allTags += tag + " "
|
|
}
|
|
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, allTags})
|
|
continue
|
|
}
|
|
|
|
if len(sn.Tags) > rows {
|
|
rows = len(sn.Tags)
|
|
}
|
|
|
|
for i := 1; i < rows; i++ {
|
|
path := ""
|
|
if len(sn.Paths) > i {
|
|
path = sn.Paths[i]
|
|
}
|
|
|
|
tag := ""
|
|
if len(sn.Tags) > i {
|
|
tag = sn.Tags[i]
|
|
}
|
|
|
|
treeElement := "│"
|
|
if i == (rows - 1) {
|
|
treeElement = "└──"
|
|
}
|
|
|
|
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, treeElement, path})
|
|
}
|
|
}
|
|
|
|
tab.Footer = fmt.Sprintf("%d snapshots", len(list))
|
|
|
|
tab.Write(stdout)
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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)
|
|
}
|