2016-08-20 15:43:25 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2016-10-10 18:55:02 +00:00
|
|
|
"encoding/hex"
|
2016-08-20 15:43:25 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"restic"
|
2016-08-20 15:59:10 +00:00
|
|
|
"strings"
|
2016-09-17 10:36:05 +00:00
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
2016-08-20 15:43:25 +00:00
|
|
|
)
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
var cmdForget = &cobra.Command{
|
|
|
|
Use: "forget [flags] [snapshot ID] [...]",
|
|
|
|
Short: "forget removes snapshots from the repository",
|
|
|
|
Long: `
|
|
|
|
The "forget" command removes snapshots according to a policy. Please note that
|
|
|
|
this command really only deletes the snapshot object in the repository, which
|
|
|
|
is a reference to data stored there. In order to remove this (now unreferenced)
|
|
|
|
data after 'forget' was run successfully, see the 'prune' command. `,
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
return runForget(forgetOptions, globalOptions, args)
|
|
|
|
},
|
|
|
|
}
|
2016-08-20 15:43:25 +00:00
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
// ForgetOptions collects all options for the forget command.
|
|
|
|
type ForgetOptions struct {
|
|
|
|
Last int
|
|
|
|
Hourly int
|
|
|
|
Daily int
|
|
|
|
Weekly int
|
|
|
|
Monthly int
|
|
|
|
Yearly int
|
2016-09-13 18:37:11 +00:00
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
KeepTags []string
|
2016-08-20 15:59:47 +00:00
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
Hostname string
|
|
|
|
Tags []string
|
2016-08-20 15:43:25 +00:00
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
DryRun bool
|
2017-02-21 09:58:30 +00:00
|
|
|
Prune bool
|
2016-08-20 15:43:25 +00:00
|
|
|
}
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
var forgetOptions ForgetOptions
|
|
|
|
|
2016-08-20 15:43:25 +00:00
|
|
|
func init() {
|
2016-09-17 10:36:05 +00:00
|
|
|
cmdRoot.AddCommand(cmdForget)
|
|
|
|
|
|
|
|
f := cmdForget.Flags()
|
2016-09-29 18:39:55 +00:00
|
|
|
f.IntVarP(&forgetOptions.Last, "keep-last", "l", 0, "keep the last `n` snapshots")
|
|
|
|
f.IntVarP(&forgetOptions.Hourly, "keep-hourly", "H", 0, "keep the last `n` hourly snapshots")
|
|
|
|
f.IntVarP(&forgetOptions.Daily, "keep-daily", "d", 0, "keep the last `n` daily snapshots")
|
|
|
|
f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly 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.StringSliceVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "always keep snapshots with this `tag` (can be specified multiple times)")
|
2016-09-17 10:36:05 +00:00
|
|
|
f.StringVar(&forgetOptions.Hostname, "hostname", "", "only forget snapshots for the given hostname")
|
2016-09-29 18:39:55 +00:00
|
|
|
f.StringSliceVar(&forgetOptions.Tags, "tag", []string{}, "only forget snapshots with the `tag` (can be specified multiple times)")
|
2016-08-20 15:43:25 +00:00
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
|
2017-02-21 09:58:30 +00:00
|
|
|
f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
|
2016-08-20 15:43:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func printSnapshots(w io.Writer, snapshots restic.Snapshots) {
|
|
|
|
tab := NewTable()
|
2016-09-13 18:56:18 +00:00
|
|
|
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %-10s %s", "ID", "Date", "Host", "Tags", "Directory")
|
|
|
|
tab.RowFormat = "%-8s %-19s %-10s %-10s %s"
|
2016-08-20 15:43:25 +00:00
|
|
|
|
|
|
|
for _, sn := range snapshots {
|
|
|
|
if len(sn.Paths) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2016-09-13 18:56:18 +00:00
|
|
|
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]
|
2016-08-20 15:43:25 +00:00
|
|
|
}
|
2016-09-13 18:56:18 +00:00
|
|
|
|
|
|
|
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, path})
|
2016-08-20 15:43:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
tab.Write(w)
|
|
|
|
}
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
|
|
|
repo, err := OpenRepository(gopts)
|
2016-08-20 15:43:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
lock, err := lockRepoExclusive(repo)
|
|
|
|
defer unlockRepo(lock)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-10-10 18:55:02 +00:00
|
|
|
// parse arguments as hex strings
|
|
|
|
var ids []string
|
2016-08-20 15:43:25 +00:00
|
|
|
for _, s := range args {
|
2016-10-10 18:55:02 +00:00
|
|
|
_, err := hex.DecodeString(s)
|
|
|
|
if err != nil {
|
|
|
|
Warnf("argument %q is not a snapshot ID, ignoring\n", s)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
ids = append(ids, s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// process all snapshot IDs given as arguments
|
|
|
|
for _, s := range ids {
|
2016-08-20 15:43:25 +00:00
|
|
|
id, err := restic.FindSnapshot(repo, s)
|
|
|
|
if err != nil {
|
2017-02-08 23:43:10 +00:00
|
|
|
Warnf("could not find a snapshot for ID %q, ignoring\n", s)
|
2016-10-10 18:55:02 +00:00
|
|
|
continue
|
2016-08-20 15:43:25 +00:00
|
|
|
}
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
if !opts.DryRun {
|
2017-01-25 16:48:35 +00:00
|
|
|
h := restic.Handle{Type: restic.SnapshotFile, Name: id.String()}
|
|
|
|
err = repo.Backend().Remove(h)
|
2016-08-20 15:43:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
Verbosef("removed snapshot %v\n", id.Str())
|
2016-08-20 15:43:25 +00:00
|
|
|
} else {
|
2016-12-02 16:33:05 +00:00
|
|
|
Verbosef("would remove snapshot %v\n", id.Str())
|
2016-08-20 15:43:25 +00:00
|
|
|
}
|
2016-08-20 15:53:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
policy := restic.ExpirePolicy{
|
2016-09-17 10:36:05 +00:00
|
|
|
Last: opts.Last,
|
|
|
|
Hourly: opts.Hourly,
|
|
|
|
Daily: opts.Daily,
|
|
|
|
Weekly: opts.Weekly,
|
|
|
|
Monthly: opts.Monthly,
|
|
|
|
Yearly: opts.Yearly,
|
|
|
|
Tags: opts.KeepTags,
|
2016-08-20 15:53:03 +00:00
|
|
|
}
|
2016-08-20 15:43:25 +00:00
|
|
|
|
2016-08-20 15:53:03 +00:00
|
|
|
if policy.Empty() {
|
|
|
|
return nil
|
2016-08-20 15:43:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// then, load all remaining snapshots
|
|
|
|
snapshots, err := restic.LoadAllSnapshots(repo)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// group by hostname and dirs
|
|
|
|
type key struct {
|
|
|
|
Hostname string
|
|
|
|
Dirs string
|
|
|
|
}
|
|
|
|
|
|
|
|
snapshotGroups := make(map[key]restic.Snapshots)
|
|
|
|
|
|
|
|
for _, sn := range snapshots {
|
2016-09-17 10:36:05 +00:00
|
|
|
if opts.Hostname != "" && sn.Hostname != opts.Hostname {
|
2016-08-20 15:59:47 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
if !sn.HasTags(opts.Tags) {
|
2016-09-13 18:20:55 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2016-08-20 15:59:10 +00:00
|
|
|
k := key{Hostname: sn.Hostname, Dirs: strings.Join(sn.Paths, ":")}
|
2016-08-20 15:43:25 +00:00
|
|
|
list := snapshotGroups[k]
|
|
|
|
list = append(list, sn)
|
|
|
|
snapshotGroups[k] = list
|
|
|
|
}
|
|
|
|
|
2017-02-21 09:58:30 +00:00
|
|
|
removeSnapshots := 0
|
2016-08-20 15:43:25 +00:00
|
|
|
for key, snapshotGroup := range snapshotGroups {
|
2016-09-17 10:36:05 +00:00
|
|
|
Printf("snapshots for host %v, directories %v:\n\n", key.Hostname, key.Dirs)
|
2016-08-20 15:43:25 +00:00
|
|
|
keep, remove := restic.ApplyPolicy(snapshotGroup, policy)
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
Printf("keep %d snapshots:\n", len(keep))
|
|
|
|
printSnapshots(globalOptions.stdout, keep)
|
|
|
|
Printf("\n")
|
2016-08-20 15:43:25 +00:00
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
Printf("remove %d snapshots:\n", len(remove))
|
|
|
|
printSnapshots(globalOptions.stdout, remove)
|
|
|
|
Printf("\n")
|
2016-08-20 15:43:25 +00:00
|
|
|
|
2017-02-21 09:58:30 +00:00
|
|
|
removeSnapshots += len(remove)
|
|
|
|
|
2016-09-17 10:36:05 +00:00
|
|
|
if !opts.DryRun {
|
2016-08-20 15:43:25 +00:00
|
|
|
for _, sn := range remove {
|
2017-01-25 16:48:35 +00:00
|
|
|
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
|
|
|
err = repo.Backend().Remove(h)
|
2016-08-20 15:43:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-21 09:58:30 +00:00
|
|
|
if removeSnapshots > 0 && opts.Prune {
|
|
|
|
Printf("%d snapshots have been removed, running prune\n", removeSnapshots)
|
|
|
|
if !opts.DryRun {
|
|
|
|
return pruneRepository(gopts, repo)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-20 15:43:25 +00:00
|
|
|
return nil
|
|
|
|
}
|