forked from TrueCloudLab/restic
restore: add dry-run support
This commit is contained in:
parent
c47bf33884
commit
83351f42e3
4 changed files with 82 additions and 19 deletions
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue