github.com/mckael/restic@v0.8.3/cmd/restic/cmd_snapshots.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/restic/restic/internal/restic"
    12  	"github.com/spf13/cobra"
    13  )
    14  
    15  var cmdSnapshots = &cobra.Command{
    16  	Use:   "snapshots [snapshotID ...]",
    17  	Short: "List all snapshots",
    18  	Long: `
    19  The "snapshots" command lists all snapshots stored in the repository.
    20  `,
    21  	DisableAutoGenTag: true,
    22  	RunE: func(cmd *cobra.Command, args []string) error {
    23  		return runSnapshots(snapshotOptions, globalOptions, args)
    24  	},
    25  }
    26  
    27  // SnapshotOptions bundles all options for the snapshots command.
    28  type SnapshotOptions struct {
    29  	Host    string
    30  	Tags    restic.TagLists
    31  	Paths   []string
    32  	Compact bool
    33  	Last    bool
    34  }
    35  
    36  var snapshotOptions SnapshotOptions
    37  
    38  func init() {
    39  	cmdRoot.AddCommand(cmdSnapshots)
    40  
    41  	f := cmdSnapshots.Flags()
    42  	f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`")
    43  	f.Var(&snapshotOptions.Tags, "tag", "only consider snapshots which include this `taglist` (can be specified multiple times)")
    44  	f.StringArrayVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)")
    45  	f.BoolVarP(&snapshotOptions.Compact, "compact", "c", false, "use compact format")
    46  	f.BoolVar(&snapshotOptions.Last, "last", false, "only show the last snapshot for each host and path")
    47  }
    48  
    49  func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
    50  	repo, err := OpenRepository(gopts)
    51  	if err != nil {
    52  		return err
    53  	}
    54  
    55  	if !gopts.NoLock {
    56  		lock, err := lockRepo(repo)
    57  		defer unlockRepo(lock)
    58  		if err != nil {
    59  			return err
    60  		}
    61  	}
    62  
    63  	ctx, cancel := context.WithCancel(gopts.ctx)
    64  	defer cancel()
    65  
    66  	var list restic.Snapshots
    67  	for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
    68  		list = append(list, sn)
    69  	}
    70  
    71  	if opts.Last {
    72  		list = FilterLastSnapshots(list)
    73  	}
    74  
    75  	sort.Sort(sort.Reverse(list))
    76  
    77  	if gopts.JSON {
    78  		err := printSnapshotsJSON(gopts.stdout, list)
    79  		if err != nil {
    80  			Warnf("error printing snapshot: %v\n", err)
    81  		}
    82  		return nil
    83  	}
    84  	PrintSnapshots(gopts.stdout, list, opts.Compact)
    85  
    86  	return nil
    87  }
    88  
    89  // filterLastSnapshotsKey is used by FilterLastSnapshots.
    90  type filterLastSnapshotsKey struct {
    91  	Hostname    string
    92  	JoinedPaths string
    93  }
    94  
    95  // newFilterLastSnapshotsKey initializes a filterLastSnapshotsKey from a Snapshot
    96  func newFilterLastSnapshotsKey(sn *restic.Snapshot) filterLastSnapshotsKey {
    97  	// Shallow slice copy
    98  	var paths = make([]string, len(sn.Paths))
    99  	copy(paths, sn.Paths)
   100  	sort.Strings(paths)
   101  	return filterLastSnapshotsKey{sn.Hostname, strings.Join(paths, "|")}
   102  }
   103  
   104  // FilterLastSnapshots filters a list of snapshots to only return the last
   105  // entry for each hostname and path. If the snapshot contains multiple paths,
   106  // they will be joined and treated as one item.
   107  func FilterLastSnapshots(list restic.Snapshots) restic.Snapshots {
   108  	// Sort the snapshots so that the newer ones are listed first
   109  	sort.SliceStable(list, func(i, j int) bool {
   110  		return list[i].Time.After(list[j].Time)
   111  	})
   112  
   113  	var results restic.Snapshots
   114  	seen := make(map[filterLastSnapshotsKey]bool)
   115  	for _, sn := range list {
   116  		key := newFilterLastSnapshotsKey(sn)
   117  		if !seen[key] {
   118  			seen[key] = true
   119  			results = append(results, sn)
   120  		}
   121  	}
   122  	return results
   123  }
   124  
   125  // PrintSnapshots prints a text table of the snapshots in list to stdout.
   126  func PrintSnapshots(stdout io.Writer, list restic.Snapshots, compact bool) {
   127  
   128  	// always sort the snapshots so that the newer ones are listed last
   129  	sort.SliceStable(list, func(i, j int) bool {
   130  		return list[i].Time.Before(list[j].Time)
   131  	})
   132  
   133  	// Determine the max widths for host and tag.
   134  	maxHost, maxTag := 10, 6
   135  	for _, sn := range list {
   136  		if len(sn.Hostname) > maxHost {
   137  			maxHost = len(sn.Hostname)
   138  		}
   139  		for _, tag := range sn.Tags {
   140  			if len(tag) > maxTag {
   141  				maxTag = len(tag)
   142  			}
   143  		}
   144  	}
   145  
   146  	tab := NewTable()
   147  	if !compact {
   148  		tab.Header = fmt.Sprintf("%-8s  %-19s  %-*s  %-*s  %-3s %s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags", "", "Directory")
   149  		tab.RowFormat = fmt.Sprintf("%%-8s  %%-19s  %%%ds  %%%ds  %%-3s %%s", -maxHost, -maxTag)
   150  	} else {
   151  		tab.Header = fmt.Sprintf("%-8s  %-19s  %-*s  %-*s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags")
   152  		tab.RowFormat = fmt.Sprintf("%%-8s  %%-19s  %%%ds  %%s", -maxHost)
   153  	}
   154  
   155  	for _, sn := range list {
   156  		if len(sn.Paths) == 0 {
   157  			continue
   158  		}
   159  
   160  		firstTag := ""
   161  		if len(sn.Tags) > 0 {
   162  			firstTag = sn.Tags[0]
   163  		}
   164  
   165  		rows := len(sn.Paths)
   166  		if rows < len(sn.Tags) {
   167  			rows = len(sn.Tags)
   168  		}
   169  
   170  		treeElement := "   "
   171  		if rows != 1 {
   172  			treeElement = "┌──"
   173  		}
   174  
   175  		if !compact {
   176  			tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, treeElement, sn.Paths[0]})
   177  		} else {
   178  			allTags := ""
   179  			for _, tag := range sn.Tags {
   180  				allTags += tag + " "
   181  			}
   182  			tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, allTags})
   183  			continue
   184  		}
   185  
   186  		if len(sn.Tags) > rows {
   187  			rows = len(sn.Tags)
   188  		}
   189  
   190  		for i := 1; i < rows; i++ {
   191  			path := ""
   192  			if len(sn.Paths) > i {
   193  				path = sn.Paths[i]
   194  			}
   195  
   196  			tag := ""
   197  			if len(sn.Tags) > i {
   198  				tag = sn.Tags[i]
   199  			}
   200  
   201  			treeElement := "│"
   202  			if i == (rows - 1) {
   203  				treeElement = "└──"
   204  			}
   205  
   206  			tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, treeElement, path})
   207  		}
   208  	}
   209  
   210  	tab.Footer = fmt.Sprintf("%d snapshots", len(list))
   211  
   212  	tab.Write(stdout)
   213  }
   214  
   215  // Snapshot helps to print Snaphots as JSON with their ID included.
   216  type Snapshot struct {
   217  	*restic.Snapshot
   218  
   219  	ID      *restic.ID `json:"id"`
   220  	ShortID string     `json:"short_id"`
   221  }
   222  
   223  // printSnapshotsJSON writes the JSON representation of list to stdout.
   224  func printSnapshotsJSON(stdout io.Writer, list restic.Snapshots) error {
   225  
   226  	var snapshots []Snapshot
   227  
   228  	for _, sn := range list {
   229  
   230  		k := Snapshot{
   231  			Snapshot: sn,
   232  			ID:       sn.ID(),
   233  			ShortID:  sn.ID().Str(),
   234  		}
   235  		snapshots = append(snapshots, k)
   236  	}
   237  
   238  	return json.NewEncoder(stdout).Encode(snapshots)
   239  }