restore: add dry-run support

This commit is contained in:
Michael Eischer 2024-05-31 20:57:28 +02:00
parent c47bf33884
commit 83351f42e3
4 changed files with 82 additions and 19 deletions

View file

@ -47,6 +47,7 @@ type RestoreOptions struct {
includePatternOptions
Target string
restic.SnapshotFilter
DryRun bool
Sparse bool
Verify bool
Overwrite restorer.OverwriteBehavior
@ -64,6 +65,7 @@ func init() {
initIncludePatternOptions(flags, &restoreOptions.includePatternOptions)
initSingleSnapshotFilter(flags, &restoreOptions.SnapshotFilter)
flags.BoolVar(&restoreOptions.DryRun, "dry-run", false, "do not write any data, just show what would be done")
flags.BoolVar(&restoreOptions.Sparse, "sparse", false, "restore files as sparse")
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
flags.Var(&restoreOptions.Overwrite, "overwrite", "overwrite behavior, one of (always|if-changed|if-newer|never) (default: always)")
@ -99,6 +101,9 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
if hasExcludes && hasIncludes {
return errors.Fatal("exclude and include patterns are mutually exclusive")
}
if opts.DryRun && opts.Verify {
return errors.Fatal("--dry-run and --verify are mutually exclusive")
}
snapshotIDString := args[0]
@ -140,6 +145,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON))
res := restorer.NewRestorer(repo, sn, restorer.Options{
DryRun: opts.DryRun,
Sparse: opts.Sparse,
Progress: progress,
Overwrite: opts.Overwrite,

View file

@ -33,6 +33,7 @@ type Restorer struct {
var restorerAbortOnAllErrors = func(_ string, err error) error { return err }
type Options struct {
DryRun bool
Sparse bool
Progress *restoreui.Progress
Overwrite OverwriteBehavior
@ -220,15 +221,17 @@ func (res *Restorer) traverseTree(ctx context.Context, target, location string,
}
func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, target, location string) error {
debug.Log("restoreNode %v %v %v", node.Name, target, location)
if err := fs.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrap(err, "RemoveNode")
}
if !res.opts.DryRun {
debug.Log("restoreNode %v %v %v", node.Name, target, location)
if err := fs.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrap(err, "RemoveNode")
}
err := node.CreateAt(ctx, target, res.repo)
if err != nil {
debug.Log("node.CreateAt(%s) error %v", target, err)
return err
err := node.CreateAt(ctx, target, res.repo)
if err != nil {
debug.Log("node.CreateAt(%s) error %v", target, err)
return err
}
}
res.opts.Progress.AddProgress(location, false, true, 0, 0)
@ -236,6 +239,9 @@ func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, targe
}
func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location string) error {
if res.opts.DryRun {
return nil
}
debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location)
err := node.RestoreMetadata(target, res.Warn)
if err != nil {
@ -245,12 +251,14 @@ func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location s
}
func (res *Restorer) restoreHardlinkAt(node *restic.Node, target, path, location string) error {
if err := fs.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrap(err, "RemoveCreateHardlink")
}
err := fs.Link(target, path)
if err != nil {
return errors.WithStack(err)
if !res.opts.DryRun {
if err := fs.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.Wrap(err, "RemoveCreateHardlink")
}
err := fs.Link(target, path)
if err != nil {
return errors.WithStack(err)
}
}
res.opts.Progress.AddProgress(location, false, true, 0, 0)
@ -259,6 +267,10 @@ func (res *Restorer) restoreHardlinkAt(node *restic.Node, target, path, location
}
func (res *Restorer) ensureDir(target string) error {
if res.opts.DryRun {
return nil
}
fi, err := fs.Lstat(target)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to check for directory: %w", err)
@ -328,7 +340,12 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
res.opts.Progress.AddSkippedFile(location, node.Size)
} else {
res.opts.Progress.AddFile(node.Size)
filerestorer.addFile(location, node.Content, int64(node.Size), matches)
if !res.opts.DryRun {
filerestorer.addFile(location, node.Content, int64(node.Size), matches)
} else {
// immediately mark as completed
res.opts.Progress.AddProgress(location, false, matches == nil, node.Size, node.Size)
}
}
res.trackFile(location, updateMetadataOnly)
return nil
@ -340,9 +357,11 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
return err
}
err = filerestorer.restoreFiles(ctx)
if err != nil {
return err
if !res.opts.DryRun {
err = filerestorer.restoreFiles(ctx)
if err != nil {
return err
}
}
debug.Log("second pass for %q", dst)

View file

@ -15,6 +15,7 @@ import (
"time"
"github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
@ -1166,3 +1167,32 @@ func TestRestoreIfChanged(t *testing.T) {
}
}
}
func TestRestoreDryRun(t *testing.T) {
snapshot := Snapshot{
Nodes: map[string]Node{
"foo": File{Data: "content: foo\n", Links: 2, Inode: 42},
"foo2": File{Data: "content: foo\n", Links: 2, Inode: 42},
"dirtest": Dir{
Nodes: map[string]Node{
"file": File{Data: "content: file\n"},
},
},
"link": Symlink{Target: "foo"},
},
}
repo := repository.TestRepository(t)
tempdir := filepath.Join(rtest.TempDir(t), "target")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sn, id := saveSnapshot(t, repo, snapshot, noopGetGenericAttributes)
t.Logf("snapshot saved as %v", id.Str())
res := NewRestorer(repo, sn, Options{DryRun: true})
rtest.OK(t, res.RestoreTo(ctx, tempdir))
_, err := os.Stat(tempdir)
rtest.Assert(t, errors.Is(err, os.ErrNotExist), "expected no file to be created, got %v", err)
}

View file

@ -83,6 +83,14 @@ func (p *printerMock) Finish(s restoreui.State, _ time.Duration) {
}
func TestRestorerProgressBar(t *testing.T) {
testRestorerProgressBar(t, false)
}
func TestRestorerProgressBarDryRun(t *testing.T) {
testRestorerProgressBar(t, true)
}
func testRestorerProgressBar(t *testing.T, dryRun bool) {
repo := repository.TestRepository(t)
sn, _ := saveSnapshot(t, repo, Snapshot{
@ -99,7 +107,7 @@ func TestRestorerProgressBar(t *testing.T) {
mock := &printerMock{}
progress := restoreui.NewProgress(mock, 0)
res := NewRestorer(repo, sn, Options{Progress: progress})
res := NewRestorer(repo, sn, Options{Progress: progress, DryRun: dryRun})
res.SelectFilter = func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) {
return true, true
}