2014-08-04 20:55:54 +00:00
package main
import (
2017-03-08 19:24:58 +00:00
"context"
"encoding/json"
2014-08-04 20:55:54 +00:00
"fmt"
2017-03-05 04:24:11 +00:00
"io"
2014-11-24 20:12:32 +00:00
"sort"
2017-10-26 06:02:29 +00:00
"strings"
2016-09-17 10:36:05 +00:00
2017-07-24 15:42:25 +00:00
"github.com/restic/restic/internal/restic"
2018-07-04 21:05:38 +00:00
"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 {
2020-08-27 03:29:43 +00:00
Use : "snapshots [flags] [snapshotID ...]" ,
2017-09-11 16:32:44 +00:00
Short : "List all snapshots" ,
2016-09-17 10:36:05 +00:00
Long : `
2017-02-13 15:05:25 +00:00
The "snapshots" command lists all snapshots stored in the repository .
2019-11-05 06:03:38 +00:00
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
` ,
2017-08-06 19:02:16 +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 {
2020-02-26 21:17:59 +00:00
Hosts [ ] string
2017-09-10 22:09:28 +00:00
Tags restic . TagLists
Paths [ ] string
2017-09-09 15:51:10 +00:00
Compact bool
2020-12-16 09:09:03 +00:00
Last bool // This option should be removed in favour of Latest.
Latest int
2018-11-14 15:23:00 +00:00
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 ( )
2020-02-26 21:17:59 +00:00
f . StringArrayVarP ( & snapshotOptions . Hosts , "host" , "H" , nil , "only consider snapshots for this `host` (can be specified multiple times)" )
2020-04-13 12:16:02 +00:00
f . Var ( & snapshotOptions . Tags , "tag" , "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)" )
2017-07-07 01:19:06 +00:00
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" )
2017-10-26 06:02:29 +00:00
f . BoolVar ( & snapshotOptions . Last , "last" , false , "only show the last snapshot for each host and path" )
2020-12-16 09:09:03 +00:00
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" )
2022-04-15 23:13:13 +00:00
f . StringVarP ( & snapshotOptions . GroupBy , "group-by" , "g" , "" , "`group` snapshots by host, paths and/or tags, separated by comma" )
2018-11-14 15:23:00 +00:00
}
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
}
2017-03-08 19:24:58 +00:00
ctx , cancel := context . WithCancel ( gopts . ctx )
defer cancel ( )
2016-05-10 19:23:18 +00:00
2018-11-30 14:37:49 +00:00
var snapshots restic . Snapshots
2021-11-06 00:14:24 +00:00
for sn := range FindFilteredSnapshots ( ctx , repo . Backend ( ) , repo , opts . Hosts , opts . Tags , opts . Paths , args ) {
2018-11-30 14:37:49 +00:00
snapshots = append ( snapshots , sn )
}
2019-01-04 18:24:03 +00:00
snapshotGroups , grouped , err := restic . GroupSnapshots ( snapshots , opts . GroupBy )
2018-11-30 14:37:49 +00:00
if err != nil {
return err
2017-10-26 06:02:29 +00:00
}
2018-11-14 15:23:00 +00:00
for k , list := range snapshotGroups {
if opts . Last {
2020-12-16 09:09:03 +00:00
// 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 )
2018-11-14 15:23:00 +00:00
}
sort . Sort ( sort . Reverse ( list ) )
snapshotGroups [ k ] = list
}
2014-08-04 20:55:54 +00:00
2017-02-12 20:43:39 +00:00
if gopts . JSON {
2018-11-30 14:37:49 +00:00
err := printSnapshotGroupJSON ( gopts . stdout , snapshotGroups , grouped )
2017-02-12 20:43:39 +00:00
if err != nil {
2018-11-14 15:23:00 +00:00
Warnf ( "error printing snapshots: %v\n" , err )
2017-02-12 20:43:39 +00:00
}
return nil
}
2018-11-14 15:23:00 +00:00
for k , list := range snapshotGroups {
2018-11-30 14:37:49 +00:00
if grouped {
err := PrintSnapshotGroupHeader ( gopts . stdout , k )
if err != nil {
Warnf ( "error printing snapshots: %v\n" , err )
return nil
}
2018-11-14 15:23:00 +00:00
}
PrintSnapshots ( gopts . stdout , list , nil , opts . Compact )
}
2017-02-12 20:43:39 +00:00
return nil
}
2017-10-26 06:02:29 +00:00
// 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 , "|" ) }
}
2020-12-16 09:09:03 +00:00
// 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 {
2017-10-26 06:02:29 +00:00
// 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
2020-12-16 09:09:03 +00:00
seen := make ( map [ filterLastSnapshotsKey ] int )
2017-10-26 06:02:29 +00:00
for _ , sn := range list {
key := newFilterLastSnapshotsKey ( sn )
2020-12-16 09:09:03 +00:00
if seen [ key ] < limit {
seen [ key ] ++
2017-10-26 06:02:29 +00:00
results = append ( results , sn )
}
}
return results
}
2017-03-07 13:19:36 +00:00
// PrintSnapshots prints a text table of the snapshots in list to stdout.
2018-07-04 21:05:38 +00:00
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 ]
}
}
2017-02-12 20:43:39 +00:00
2017-09-24 10:45:14 +00:00
// 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 )
} )
2017-03-05 04:32:01 +00:00
// 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 )
}
}
}
2018-07-04 21:05:38 +00:00
tab := table . New ( )
2017-02-12 20:43:39 +00:00
2018-07-04 21:05:38 +00:00
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
}
2018-07-04 21:05:38 +00:00
tab . AddColumn ( "Paths" , ` {{ join .Paths "\n" }} ` )
}
2015-03-02 13:48:47 +00:00
2018-07-04 21:05:38 +00:00
type snapshot struct {
ID string
Timestamp string
Hostname string
Tags [ ] string
Reasons [ ] string
Paths [ ] string
}
2016-09-13 18:13:04 +00:00
2018-07-04 21:05:38 +00:00
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 ) ,
2018-07-04 21:05:38 +00:00
Hostname : sn . Hostname ,
Tags : sn . Tags ,
Paths : sn . Paths ,
2017-03-06 01:49:15 +00:00
}
2017-01-18 19:46:00 +00:00
2018-07-04 21:05:38 +00:00
if len ( reasons ) > 0 {
id := sn . ID ( )
data . Reasons = keepReasons [ * id ] . Matches
2017-01-18 19:46:00 +00:00
}
2018-10-13 18:17:19 +00:00
if len ( sn . Paths ) > 1 && ! compact {
2018-07-04 21:05:38 +00:00
multiline = true
2017-09-09 15:51:10 +00:00
}
2017-01-18 19:46:00 +00:00
2018-07-04 21:05:38 +00:00
tab . AddRow ( data )
}
2016-09-13 18:13:04 +00:00
2018-07-04 21:05:38 +00:00
tab . AddFooter ( fmt . Sprintf ( "%d snapshots" , len ( list ) ) )
2016-09-13 18:13:04 +00:00
2018-07-04 21:05:38 +00:00
if multiline {
// print an additional blank line between snapshots
2016-09-13 18:13:04 +00:00
2018-07-04 21:05:38 +00:00
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 )
2017-01-18 19:46:00 +00:00
}
2018-07-04 21:05:38 +00:00
last = idx
return err
2015-03-02 13:48:47 +00:00
}
2014-11-24 20:12:32 +00:00
}
2021-01-30 16:25:10 +00:00
err := tab . Write ( stdout )
if err != nil {
Warnf ( "error printing: %v\n" , err )
}
2017-02-12 20:43:39 +00:00
}
2018-11-14 15:23:00 +00:00
// PrintSnapshotGroupHeader prints which group of the group-by option the
// following snapshots belong to.
// Prints nothing, if we did not group at all.
2018-11-30 14:37:49 +00:00
func PrintSnapshotGroupHeader ( stdout io . Writer , groupKeyJSON string ) error {
2019-01-04 18:24:03 +00:00
var key restic . SnapshotGroupKey
2018-11-14 15:23:00 +00:00
2020-03-06 22:27:37 +00:00
err := json . Unmarshal ( [ ] byte ( groupKeyJSON ) , & key )
2018-11-30 13:34:19 +00:00
if err != nil {
return err
}
2018-11-14 15:23:00 +00:00
2018-11-30 14:37:49 +00:00
if key . Hostname == "" && key . Tags == nil && key . Paths == nil {
return nil
}
2018-11-30 13:34:19 +00:00
// Info
fmt . Fprintf ( stdout , "snapshots" )
var infoStrings [ ] string
2018-11-30 14:37:49 +00:00
if key . Hostname != "" {
2018-11-30 13:34:19 +00:00
infoStrings = append ( infoStrings , "host [" + key . Hostname + "]" )
}
2018-11-30 14:37:49 +00:00
if key . Tags != nil {
infoStrings = append ( infoStrings , "tags [" + strings . Join ( key . Tags , ", " ) + "]" )
}
if key . Paths != nil {
2018-11-30 13:34:19 +00:00
infoStrings = append ( infoStrings , "paths [" + strings . Join ( key . Paths , ", " ) + "]" )
}
if infoStrings != nil {
fmt . Fprintf ( stdout , " for (%s)" , strings . Join ( infoStrings , ", " ) )
}
fmt . Fprintf ( stdout , ":\n" )
2018-11-14 15:23:00 +00:00
return nil
}
2017-03-08 19:24:58 +00:00
// Snapshot helps to print Snaphots as JSON with their ID included.
2017-02-12 20:43:39 +00:00
type Snapshot struct {
* restic . Snapshot
2017-10-03 13:24:09 +00:00
ID * restic . ID ` json:"id" `
ShortID string ` json:"short_id" `
2017-02-12 20:43:39 +00:00
}
2018-11-14 15:23:00 +00:00
// SnapshotGroup helps to print SnaphotGroups as JSON with their GroupReasons included.
type SnapshotGroup struct {
2019-01-04 18:24:03 +00:00
GroupKey restic . SnapshotGroupKey ` json:"group_key" `
Snapshots [ ] Snapshot ` json:"snapshots" `
2018-11-14 15:23:00 +00:00
}
2017-02-12 20:43:39 +00:00
// printSnapshotsJSON writes the JSON representation of list to stdout.
2018-11-14 15:23:00 +00:00
func printSnapshotGroupJSON ( stdout io . Writer , snGroups map [ string ] restic . Snapshots , grouped bool ) error {
if grouped {
2020-10-05 11:04:24 +00:00
snapshotGroups := [ ] SnapshotGroup { }
2017-02-12 20:43:39 +00:00
2018-11-14 15:23:00 +00:00
for k , list := range snGroups {
2019-01-04 18:24:03 +00:00
var key restic . SnapshotGroupKey
2018-11-14 15:23:00 +00:00
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 )
}
2017-02-12 20:43:39 +00:00
2018-11-14 15:23:00 +00:00
return json . NewEncoder ( stdout ) . Encode ( snapshotGroups )
2018-11-30 13:34:19 +00:00
}
2018-11-14 15:23:00 +00:00
2018-11-30 13:34:19 +00:00
// Old behavior
2020-10-05 11:04:24 +00:00
snapshots := [ ] Snapshot { }
2018-11-30 13:34:19 +00:00
for _ , list := range snGroups {
for _ , sn := range list {
k := Snapshot {
Snapshot : sn ,
ID : sn . ID ( ) ,
ShortID : sn . ID ( ) . Str ( ) ,
2018-11-14 15:23:00 +00:00
}
2018-11-30 13:34:19 +00:00
snapshots = append ( snapshots , k )
2017-02-12 20:43:39 +00:00
}
2018-11-14 15:23:00 +00:00
}
2018-11-30 13:34:19 +00:00
return json . NewEncoder ( stdout ) . Encode ( snapshots )
2014-08-04 20:55:54 +00:00
}