mirror of
https://github.com/octoleo/restic.git
synced 2024-11-26 14:56:29 +00:00
Add --path
for snapshot filtering by path.
Add `--group-by-tags` for grouping on host,tags,dirs instead of host,dirs. Borrow the snapshot printing from cmd_snapshot. Closes #841
This commit is contained in:
parent
f5a55a81f7
commit
e9a2982ecd
@ -2,11 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"restic"
|
"restic"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"restic/errors"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,11 +36,13 @@ type ForgetOptions struct {
|
|||||||
|
|
||||||
KeepTags []string
|
KeepTags []string
|
||||||
|
|
||||||
Hostname string
|
Host string
|
||||||
Tags []string
|
Tags []string
|
||||||
|
Paths []string
|
||||||
|
|
||||||
DryRun bool
|
GroupByTags bool
|
||||||
Prune bool
|
DryRun bool
|
||||||
|
Prune bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var forgetOptions ForgetOptions
|
var forgetOptions ForgetOptions
|
||||||
@ -54,54 +58,19 @@ func init() {
|
|||||||
f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots")
|
f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots")
|
||||||
f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
|
f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
|
||||||
|
|
||||||
f.StringSliceVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "always keep snapshots with this `tag` (can be specified multiple times)")
|
f.StringSliceVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "keep snapshots with this `tag` (can be specified multiple times)")
|
||||||
f.StringVar(&forgetOptions.Hostname, "hostname", "", "only forget snapshots for the given hostname")
|
f.BoolVarP(&forgetOptions.GroupByTags, "group-by-tags", "G", false, "Group by host,paths,tags instead of just host,paths")
|
||||||
f.StringSliceVar(&forgetOptions.Tags, "tag", []string{}, "only forget snapshots with the `tag` (can be specified multiple times)")
|
// Sadly the commonly used shortcut `H` is already used.
|
||||||
|
f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
|
||||||
|
// Deprecated since 2017-03-07.
|
||||||
|
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname` (deprecated)")
|
||||||
|
f.StringSliceVar(&forgetOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)")
|
||||||
|
f.StringSliceVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
|
||||||
|
|
||||||
f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
|
f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
|
||||||
f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
|
f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSnapshots(w io.Writer, snapshots restic.Snapshots) {
|
|
||||||
tab := NewTable()
|
|
||||||
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %-10s %s", "ID", "Date", "Host", "Tags", "Directory")
|
|
||||||
tab.RowFormat = "%-8s %-19s %-10s %-10s %s"
|
|
||||||
|
|
||||||
for _, sn := range snapshots {
|
|
||||||
if len(sn.Paths) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
firstTag := ""
|
|
||||||
if len(sn.Tags) > 0 {
|
|
||||||
firstTag = sn.Tags[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, sn.Paths[0]})
|
|
||||||
|
|
||||||
rows := len(sn.Paths)
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, path})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tab.Write(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||||
repo, err := OpenRepository(gopts)
|
repo, err := OpenRepository(gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -114,37 +83,33 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse arguments as hex strings
|
// Process all snapshot IDs given as arguments.
|
||||||
var ids []string
|
if len(args) != 0 {
|
||||||
for _, s := range args {
|
for _, s := range args {
|
||||||
_, err := hex.DecodeString(s)
|
// Parse argument as hex string.
|
||||||
if err != nil {
|
if _, err := hex.DecodeString(s); err != nil {
|
||||||
Warnf("argument %q is not a snapshot ID, ignoring\n", s)
|
Warnf("argument %q is not a snapshot ID, ignoring\n", s)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
id, err := restic.FindSnapshot(repo, s)
|
||||||
ids = append(ids, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// process all snapshot IDs given as arguments
|
|
||||||
for _, s := range ids {
|
|
||||||
id, err := restic.FindSnapshot(repo, s)
|
|
||||||
if err != nil {
|
|
||||||
Warnf("could not find a snapshot for ID %q, ignoring\n", s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.DryRun {
|
|
||||||
h := restic.Handle{Type: restic.SnapshotFile, Name: id.String()}
|
|
||||||
err = repo.Backend().Remove(h)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
Warnf("could not find a snapshot for ID %q, ignoring\n", s)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("removed snapshot %v\n", id.Str())
|
if !opts.DryRun {
|
||||||
} else {
|
h := restic.Handle{Type: restic.SnapshotFile, Name: id.String()}
|
||||||
Verbosef("would remove snapshot %v\n", id.Str())
|
err = repo.Backend().Remove(h)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Verbosef("removed snapshot %v\n", id.Str())
|
||||||
|
} else {
|
||||||
|
Verbosef("would remove snapshot %v\n", id.Str())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
policy := restic.ExpirePolicy{
|
policy := restic.ExpirePolicy{
|
||||||
@ -157,26 +122,22 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|||||||
Tags: opts.KeepTags,
|
Tags: opts.KeepTags,
|
||||||
}
|
}
|
||||||
|
|
||||||
if policy.Empty() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// then, load all remaining snapshots
|
|
||||||
snapshots, err := restic.LoadAllSnapshots(repo)
|
snapshots, err := restic.LoadAllSnapshots(repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// group by hostname and dirs
|
// Group snapshots by hostname and dirs.
|
||||||
type key struct {
|
type key struct {
|
||||||
Hostname string
|
Hostname string
|
||||||
Dirs string
|
Paths []string
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotGroups := make(map[key]restic.Snapshots)
|
snapshotGroups := make(map[string]restic.Snapshots)
|
||||||
|
|
||||||
for _, sn := range snapshots {
|
for _, sn := range snapshots {
|
||||||
if opts.Hostname != "" && sn.Hostname != opts.Hostname {
|
if opts.Host != "" && sn.Hostname != opts.Host {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,24 +145,48 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
k := key{Hostname: sn.Hostname, Dirs: strings.Join(sn.Paths, ":")}
|
if !sn.HasPaths(opts.Paths) {
|
||||||
list := snapshotGroups[k]
|
continue
|
||||||
list = append(list, sn)
|
}
|
||||||
snapshotGroups[k] = list
|
|
||||||
|
var tags []string
|
||||||
|
if opts.GroupByTags {
|
||||||
|
sort.StringSlice(sn.Tags).Sort()
|
||||||
|
tags = sn.Tags
|
||||||
|
}
|
||||||
|
sort.StringSlice(sn.Paths).Sort()
|
||||||
|
k, _ := json.Marshal(key{Hostname: sn.Hostname, Tags: tags, Paths: sn.Paths})
|
||||||
|
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
|
||||||
|
}
|
||||||
|
if len(snapshotGroups) == 0 {
|
||||||
|
return errors.Fatal("no snapshots remained after filtering")
|
||||||
|
}
|
||||||
|
if policy.Empty() {
|
||||||
|
Verbosef("no policy was specified, no snapshots will be removed\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSnapshots := 0
|
removeSnapshots := 0
|
||||||
for key, snapshotGroup := range snapshotGroups {
|
for k, snapshotGroup := range snapshotGroups {
|
||||||
Printf("snapshots for host %v, directories %v:\n\n", key.Hostname, key.Dirs)
|
var key key
|
||||||
|
json.Unmarshal([]byte(k), &key)
|
||||||
|
if opts.GroupByTags {
|
||||||
|
Printf("snapshots for host %v, tags [%v], paths: [%v]:\n\n", key.Hostname, strings.Join(key.Tags, ", "), strings.Join(key.Paths, ", "))
|
||||||
|
} else {
|
||||||
|
Printf("snapshots for host %v, paths: [%v]:\n\n", key.Hostname, strings.Join(key.Paths, ", "))
|
||||||
|
}
|
||||||
keep, remove := restic.ApplyPolicy(snapshotGroup, policy)
|
keep, remove := restic.ApplyPolicy(snapshotGroup, policy)
|
||||||
|
|
||||||
Printf("keep %d snapshots:\n", len(keep))
|
if len(keep) != 0 {
|
||||||
printSnapshots(globalOptions.stdout, keep)
|
Printf("keep %d snapshots:\n", len(keep))
|
||||||
Printf("\n")
|
PrintSnapshots(globalOptions.stdout, keep)
|
||||||
|
Printf("\n")
|
||||||
|
}
|
||||||
|
|
||||||
Printf("remove %d snapshots:\n", len(remove))
|
if len(remove) != 0 {
|
||||||
printSnapshots(globalOptions.stdout, remove)
|
Printf("remove %d snapshots:\n", len(remove))
|
||||||
Printf("\n")
|
PrintSnapshots(globalOptions.stdout, remove)
|
||||||
|
Printf("\n")
|
||||||
|
}
|
||||||
|
|
||||||
removeSnapshots += len(remove)
|
removeSnapshots += len(remove)
|
||||||
|
|
||||||
|
@ -91,13 +91,13 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
printSnapshotsReadable(gopts.stdout, list)
|
PrintSnapshots(gopts.stdout, list)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// printSnapshotsReadable prints a text table of the snapshots in list to stdout.
|
// PrintSnapshots prints a text table of the snapshots in list to stdout.
|
||||||
func printSnapshotsReadable(stdout io.Writer, list []*restic.Snapshot) {
|
func PrintSnapshots(stdout io.Writer, list []*restic.Snapshot) {
|
||||||
|
|
||||||
// Determine the max widths for host and tag.
|
// Determine the max widths for host and tag.
|
||||||
maxHost, maxTag := 10, 6
|
maxHost, maxTag := 10, 6
|
||||||
|
Loading…
Reference in New Issue
Block a user