repair snapshots: port to filterAndReplaceSnapshot

The previous approach of rewriting all snapshots first, then flushing
the repository data and finally removing old snapshots has the downside
that an interrupted command execution leaves behind broken snapshots as
not all new data is already flushed.
This commit is contained in:
Michael Eischer 2022-12-27 21:31:04 +01:00
parent e17ee40a31
commit 4ce87a7f64
2 changed files with 42 additions and 76 deletions

View File

@ -4,10 +4,9 @@ import (
"context"
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/walker"
"golang.org/x/sync/errgroup"
"github.com/spf13/cobra"
)
@ -97,16 +96,6 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt
return err
}
// get snapshots to check & repair
var snapshots []*restic.Snapshot
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) {
snapshots = append(snapshots, sn)
}
return repairSnapshots(ctx, opts, repo, snapshots)
}
func repairSnapshots(ctx context.Context, opts RepairOptions, repo restic.Repository, snapshots []*restic.Snapshot) error {
// Three error cases are checked:
// - tree is a nil tree (-> will be replaced by an empty tree)
// - trees which cannot be loaded (-> the tree contents will be removed)
@ -157,72 +146,35 @@ func repairSnapshots(ctx context.Context, opts RepairOptions, repo restic.Reposi
AllowUnstableSerialization: true,
})
deleteSn := restic.NewIDSet()
Verbosef("check and repair %d snapshots\n", len(snapshots))
bar := newProgressMax(!globalOptions.Quiet, uint64(len(snapshots)), "snapshots")
wg, ctx := errgroup.WithContext(ctx)
repo.StartPackUploader(ctx, wg)
wg.Go(func() error {
for _, sn := range snapshots {
debug.Log("process snapshot %v", sn.ID())
Printf("%v:\n", sn)
newID, err := rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
switch {
case err != nil:
return err
case newID.IsNull():
Printf("the root tree is damaged -> delete snapshot.\n")
deleteSn.Insert(*sn.ID())
case !newID.Equal(*sn.Tree):
err = changeSnapshot(ctx, opts.DryRun, repo, sn, &newID)
if err != nil {
return err
}
deleteSn.Insert(*sn.ID())
default:
Printf("is ok.\n")
}
debug.Log("processed snapshot %v", sn.ID())
bar.Add(1)
}
bar.Done()
return repo.Flush(ctx)
})
err := wg.Wait()
if err != nil {
return err
}
if len(deleteSn) > 0 && opts.Forget {
Verbosef("delete %d snapshots...\n", len(deleteSn))
if !opts.DryRun {
DeleteFiles(ctx, globalOptions, repo, deleteSn, restic.SnapshotFile)
}
}
return nil
}
// changeSnapshot creates a modified snapshot:
// - set the tree to newID
// - add the rag opts.AddTag
// - preserve original ID
// if opts.DryRun is set, it doesn't change anything but only
func changeSnapshot(ctx context.Context, dryRun bool, repo restic.Repository, sn *restic.Snapshot, newID *restic.ID) error {
sn.AddTags([]string{"repaired"})
// Always set the original snapshot id as this essentially a new snapshot.
sn.Original = sn.ID()
sn.Tree = newID
if !dryRun {
newID, err := restic.SaveSnapshot(ctx, repo, sn)
changedCount := 0
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) {
Verbosef("\nsnapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time)
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
func(ctx context.Context, sn *restic.Snapshot) (restic.ID, error) {
return rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
}, opts.DryRun, opts.Forget, "repaired")
if err != nil {
return err
return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err)
}
if changed {
changedCount++
}
Printf("snapshot repaired -> %v created.\n", newID.Str())
} else {
Printf("would have repaired snapshot %v.\n", sn.ID().Str())
}
Verbosef("\n")
if changedCount == 0 {
if !opts.DryRun {
Verbosef("no snapshots were modified\n")
} else {
Verbosef("no snapshots would be modified\n")
}
} else {
if !opts.DryRun {
Verbosef("modified %v snapshots\n", changedCount)
} else {
Verbosef("would modify %v snapshots\n", changedCount)
}
}
return nil
}

View File

@ -124,6 +124,20 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r
return false, err
}
if filteredTree.IsNull() {
if dryRun {
Verbosef("would delete empty snapshot\n")
} else {
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
if err = repo.Backend().Remove(ctx, h); err != nil {
return false, err
}
debug.Log("removed empty snapshot %v", sn.ID())
Verbosef("removed empty snapshot %v\n", sn.ID().Str())
}
return true, nil
}
if filteredTree == *sn.Tree {
debug.Log("Snapshot %v not modified", sn)
return false, nil