diff --git a/changelog/unreleased/pull-2880 b/changelog/unreleased/pull-2880 new file mode 100644 index 000000000..94e79f625 --- /dev/null +++ b/changelog/unreleased/pull-2880 @@ -0,0 +1,7 @@ +Enhancement: Improve recover command + +Restic recover used to generate a snapshot that contains all root trees +even those which are already referenced by a snapshot. +It now only processes trees not referenced at all. + +https://github.com/restic/restic/pull/2880 diff --git a/cmd/restic/cmd_recover.go b/cmd/restic/cmd_recover.go index c6b111d22..44d93d233 100644 --- a/cmd/restic/cmd_recover.go +++ b/cmd/restic/cmd_recover.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "time" @@ -11,11 +12,11 @@ import ( var cmdRecover = &cobra.Command{ Use: "recover [flags]", - Short: "Recover data from the repository", + Short: "Recover data from the repository not referenced by snapshots", Long: ` The "recover" command builds a new snapshot from all directories it can find in -the raw data of the repository. It can be used if, for example, a snapshot has -been removed by accident with "forget". +the raw data of the repository which are not referenced in an existing snapshot. +It can be used if, for example, a snapshot has been removed by accident with "forget". EXIT STATUS =========== @@ -59,24 +60,14 @@ func runRecover(gopts GlobalOptions) error { trees := make(map[restic.ID]bool) for blob := range repo.Index().Each(gopts.ctx) { - if blob.Blob.Type != restic.TreeBlob { - continue + if blob.Type == restic.TreeBlob { + trees[blob.Blob.ID] = false } - trees[blob.Blob.ID] = false } - cur := 0 - max := len(trees) - Verbosef("load %d trees\n\n", len(trees)) - + Verbosef("load %d trees\n", len(trees)) + bar := newProgressMax(!gopts.Quiet, uint64(len(trees)), "trees loaded") for id := range trees { - cur++ - Verbosef("\rtree (%v/%v)", cur, max) - - if !trees[id] { - trees[id] = false - } - tree, err := repo.LoadTree(gopts.ctx, id) if err != nil { Warnf("unable to load tree %v: %v\n", id.Str(), err) @@ -84,26 +75,37 @@ func runRecover(gopts GlobalOptions) error { } for _, node := range tree.Nodes { - if node.Type != "dir" || node.Subtree == nil { - continue + if node.Type == "dir" && node.Subtree != nil { + trees[*node.Subtree] = true } - - subtree := *node.Subtree - trees[subtree] = true } + bar.Add(1) } - Verbosef("\ndone\n") + bar.Done() + + Verbosef("load snapshots\n") + err = restic.ForAllSnapshots(gopts.ctx, repo, nil, func(id restic.ID, sn *restic.Snapshot, err error) error { + trees[*sn.Tree] = true + return nil + }) + if err != nil { + return err + } + Verbosef("done\n") roots := restic.NewIDSet() for id, seen := range trees { - if seen { - continue + if !seen { + Verboseff("found root tree %v\n", id.Str()) + roots.Insert(id) } - - roots.Insert(id) } + Printf("\nfound %d unreferenced roots\n", len(roots)) - Verbosef("found %d roots\n", len(roots)) + if len(roots) == 0 { + Verbosef("no snapshot to write.\n") + return nil + } tree := restic.NewTree() for id := range roots { @@ -117,7 +119,7 @@ func runRecover(gopts GlobalOptions) error { ModTime: time.Now(), ChangeTime: time.Now(), } - err = tree.Insert(&node) + err := tree.Insert(&node) if err != nil { return err } @@ -133,19 +135,23 @@ func runRecover(gopts GlobalOptions) error { return errors.Fatalf("unable to save blobs to the repo: %v", err) } - sn, err := restic.NewSnapshot([]string{"/recover"}, []string{}, hostname, time.Now()) + return createSnapshot(gopts.ctx, "/recover", hostname, []string{"recovered"}, repo, &treeID) + +} + +func createSnapshot(ctx context.Context, name, hostname string, tags []string, repo restic.Repository, tree *restic.ID) error { + sn, err := restic.NewSnapshot([]string{name}, tags, hostname, time.Now()) if err != nil { return errors.Fatalf("unable to save snapshot: %v", err) } - sn.Tree = &treeID + sn.Tree = tree - id, err := repo.SaveJSONUnpacked(gopts.ctx, restic.SnapshotFile, sn) + id, err := repo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn) if err != nil { return errors.Fatalf("unable to save snapshot: %v", err) } Printf("saved new snapshot %v\n", id.Str()) - return nil }