forked from TrueCloudLab/restic
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:
parent
e17ee40a31
commit
4ce87a7f64
2 changed files with 42 additions and 76 deletions
|
@ -4,10 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend"
|
"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/restic"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -97,16 +96,6 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt
|
||||||
return err
|
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:
|
// Three error cases are checked:
|
||||||
// - tree is a nil tree (-> will be replaced by an empty tree)
|
// - tree is a nil tree (-> will be replaced by an empty tree)
|
||||||
// - trees which cannot be loaded (-> the tree contents will be removed)
|
// - 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,
|
AllowUnstableSerialization: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
deleteSn := restic.NewIDSet()
|
changedCount := 0
|
||||||
|
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) {
|
||||||
Verbosef("check and repair %d snapshots\n", len(snapshots))
|
Verbosef("\nsnapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time)
|
||||||
bar := newProgressMax(!globalOptions.Quiet, uint64(len(snapshots)), "snapshots")
|
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
|
||||||
wg, ctx := errgroup.WithContext(ctx)
|
func(ctx context.Context, sn *restic.Snapshot) (restic.ID, error) {
|
||||||
repo.StartPackUploader(ctx, wg)
|
return rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
|
||||||
wg.Go(func() error {
|
}, opts.DryRun, opts.Forget, "repaired")
|
||||||
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)
|
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,20 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r
|
||||||
return false, err
|
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 {
|
if filteredTree == *sn.Tree {
|
||||||
debug.Log("Snapshot %v not modified", sn)
|
debug.Log("Snapshot %v not modified", sn)
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
Loading…
Reference in a new issue