mirror of
https://github.com/octoleo/restic.git
synced 2024-11-29 16:23:59 +00:00
4ce87a7f64
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.
248 lines
6.6 KiB
Go
248 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/restic/restic/internal/backend"
|
|
"github.com/restic/restic/internal/debug"
|
|
"github.com/restic/restic/internal/errors"
|
|
"github.com/restic/restic/internal/repository"
|
|
"github.com/restic/restic/internal/restic"
|
|
"github.com/restic/restic/internal/walker"
|
|
)
|
|
|
|
var cmdRewrite = &cobra.Command{
|
|
Use: "rewrite [flags] [snapshotID ...]",
|
|
Short: "Rewrite snapshots to exclude unwanted files",
|
|
Long: `
|
|
The "rewrite" command excludes files from existing snapshots. It creates new
|
|
snapshots containing the same data as the original ones, but without the files
|
|
you specify to exclude. All metadata (time, host, tags) will be preserved.
|
|
|
|
The snapshots to rewrite are specified using the --host, --tag and --path options,
|
|
or by providing a list of snapshot IDs. Please note that specifying neither any of
|
|
these options nor a snapshot ID will cause the command to rewrite all snapshots.
|
|
|
|
The special tag 'rewrite' will be added to the new snapshots to distinguish
|
|
them from the original ones, unless --forget is used. If the --forget option is
|
|
used, the original snapshots will instead be directly removed from the repository.
|
|
|
|
Please note that the --forget option only removes the snapshots and not the actual
|
|
data stored in the repository. In order to delete the no longer referenced data,
|
|
use the "prune" command.
|
|
|
|
EXIT STATUS
|
|
===========
|
|
|
|
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
|
`,
|
|
DisableAutoGenTag: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runRewrite(cmd.Context(), rewriteOptions, globalOptions, args)
|
|
},
|
|
}
|
|
|
|
// RewriteOptions collects all options for the rewrite command.
|
|
type RewriteOptions struct {
|
|
Forget bool
|
|
DryRun bool
|
|
|
|
restic.SnapshotFilter
|
|
excludePatternOptions
|
|
}
|
|
|
|
var rewriteOptions RewriteOptions
|
|
|
|
func init() {
|
|
cmdRoot.AddCommand(cmdRewrite)
|
|
|
|
f := cmdRewrite.Flags()
|
|
f.BoolVarP(&rewriteOptions.Forget, "forget", "", false, "remove original snapshots after creating new ones")
|
|
f.BoolVarP(&rewriteOptions.DryRun, "dry-run", "n", false, "do not do anything, just print what would be done")
|
|
|
|
initMultiSnapshotFilter(f, &rewriteOptions.SnapshotFilter, true)
|
|
initExcludePatternOptions(f, &rewriteOptions.excludePatternOptions)
|
|
}
|
|
|
|
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, opts RewriteOptions) (bool, error) {
|
|
if sn.Tree == nil {
|
|
return false, errors.Errorf("snapshot %v has nil tree", sn.ID().Str())
|
|
}
|
|
|
|
rejectByNameFuncs, err := opts.excludePatternOptions.CollectPatterns()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
selectByName := func(nodepath string) bool {
|
|
for _, reject := range rejectByNameFuncs {
|
|
if reject(nodepath) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
rewriter := walker.NewTreeRewriter(walker.RewriteOpts{
|
|
RewriteNode: func(node *restic.Node, path string) *restic.Node {
|
|
if selectByName(path) {
|
|
return node
|
|
}
|
|
Verbosef(fmt.Sprintf("excluding %s\n", path))
|
|
return nil
|
|
},
|
|
DisableNodeCache: true,
|
|
})
|
|
|
|
return 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, "rewrite")
|
|
}
|
|
|
|
func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *restic.Snapshot, filter func(ctx context.Context, sn *restic.Snapshot) (restic.ID, error), dryRun bool, forget bool, addTag string) (bool, error) {
|
|
|
|
wg, wgCtx := errgroup.WithContext(ctx)
|
|
repo.StartPackUploader(wgCtx, wg)
|
|
|
|
var filteredTree restic.ID
|
|
wg.Go(func() error {
|
|
var err error
|
|
filteredTree, err = filter(ctx, sn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return repo.Flush(wgCtx)
|
|
})
|
|
err := wg.Wait()
|
|
if err != nil {
|
|
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
|
|
}
|
|
|
|
debug.Log("Snapshot %v modified", sn)
|
|
if dryRun {
|
|
Verbosef("would save new snapshot\n")
|
|
|
|
if forget {
|
|
Verbosef("would remove old snapshot\n")
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// Always set the original snapshot id as this essentially a new snapshot.
|
|
sn.Original = sn.ID()
|
|
sn.Tree = &filteredTree
|
|
|
|
if !forget {
|
|
sn.AddTags([]string{addTag})
|
|
}
|
|
|
|
// Save the new snapshot.
|
|
id, err := restic.SaveSnapshot(ctx, repo, sn)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
Verbosef("saved new snapshot %v\n", id.Str())
|
|
|
|
if forget {
|
|
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 old snapshot %v", sn.ID())
|
|
Verbosef("removed old snapshot %v\n", sn.ID().Str())
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, args []string) error {
|
|
if opts.excludePatternOptions.Empty() {
|
|
return errors.Fatal("Nothing to do: no excludes provided")
|
|
}
|
|
|
|
repo, err := OpenRepository(ctx, gopts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !opts.DryRun {
|
|
var lock *restic.Lock
|
|
var err error
|
|
if opts.Forget {
|
|
Verbosef("create exclusive lock for repository\n")
|
|
lock, ctx, err = lockRepoExclusive(ctx, repo, gopts.RetryLock, gopts.JSON)
|
|
} else {
|
|
lock, ctx, err = lockRepo(ctx, repo, gopts.RetryLock, gopts.JSON)
|
|
}
|
|
defer unlockRepo(lock)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
repo.SetDryRun()
|
|
}
|
|
|
|
snapshotLister, err := backend.MemorizeList(ctx, repo.Backend(), restic.SnapshotFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = repo.LoadIndex(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
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 := rewriteSnapshot(ctx, repo, sn, opts)
|
|
if err != nil {
|
|
return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err)
|
|
}
|
|
if changed {
|
|
changedCount++
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|