mirror of
https://github.com/octoleo/restic.git
synced 2025-01-27 17:18:34 +00:00
Merge pull request #4839 from MichaelEischer/restore-dry-run
restore: add `--dry-run` and extended progress output
This commit is contained in:
commit
83fdcf21fe
7
changelog/unreleased/pull-4839
Normal file
7
changelog/unreleased/pull-4839
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Add dry-run support to `restore` command
|
||||||
|
|
||||||
|
The `restore` command now supports the `--dry-run` option to perform
|
||||||
|
a dry run. Pass the `--verbose=2` option to see which files would
|
||||||
|
remain unchanged, which would be updated or freshly restored.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4839
|
@ -47,6 +47,7 @@ type RestoreOptions struct {
|
|||||||
includePatternOptions
|
includePatternOptions
|
||||||
Target string
|
Target string
|
||||||
restic.SnapshotFilter
|
restic.SnapshotFilter
|
||||||
|
DryRun bool
|
||||||
Sparse bool
|
Sparse bool
|
||||||
Verify bool
|
Verify bool
|
||||||
Overwrite restorer.OverwriteBehavior
|
Overwrite restorer.OverwriteBehavior
|
||||||
@ -64,6 +65,7 @@ func init() {
|
|||||||
initIncludePatternOptions(flags, &restoreOptions.includePatternOptions)
|
initIncludePatternOptions(flags, &restoreOptions.includePatternOptions)
|
||||||
|
|
||||||
initSingleSnapshotFilter(flags, &restoreOptions.SnapshotFilter)
|
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.Sparse, "sparse", false, "restore files as sparse")
|
||||||
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
|
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)")
|
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 {
|
if hasExcludes && hasIncludes {
|
||||||
return errors.Fatal("exclude and include patterns are mutually exclusive")
|
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]
|
snapshotIDString := args[0]
|
||||||
|
|
||||||
@ -133,13 +138,14 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
|||||||
msg := ui.NewMessage(term, gopts.verbosity)
|
msg := ui.NewMessage(term, gopts.verbosity)
|
||||||
var printer restoreui.ProgressPrinter
|
var printer restoreui.ProgressPrinter
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
printer = restoreui.NewJSONProgress(term)
|
printer = restoreui.NewJSONProgress(term, gopts.verbosity)
|
||||||
} else {
|
} else {
|
||||||
printer = restoreui.NewTextProgress(term)
|
printer = restoreui.NewTextProgress(term, gopts.verbosity)
|
||||||
}
|
}
|
||||||
|
|
||||||
progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON))
|
progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON))
|
||||||
res := restorer.NewRestorer(repo, sn, restorer.Options{
|
res := restorer.NewRestorer(repo, sn, restorer.Options{
|
||||||
|
DryRun: opts.DryRun,
|
||||||
Sparse: opts.Sparse,
|
Sparse: opts.Sparse,
|
||||||
Progress: progress,
|
Progress: progress,
|
||||||
Overwrite: opts.Overwrite,
|
Overwrite: opts.Overwrite,
|
||||||
|
@ -242,7 +242,7 @@ case, specify the ``--skip-if-unchanged`` option.
|
|||||||
Note that when using absolute paths to specify the backup target, then also
|
Note that when using absolute paths to specify the backup target, then also
|
||||||
changes to the parent folders result in a changed snapshot. For example, a backup
|
changes to the parent folders result in a changed snapshot. For example, a backup
|
||||||
of ``/home/user/work`` will create a new snapshot if the metadata of either
|
of ``/home/user/work`` will create a new snapshot if the metadata of either
|
||||||
``/``, ``/home`` or ``/home/user`` change. To avoid this problem run restic from
|
``/``, ``/home`` or ``/home/user`` change. To avoid this problem run restic from
|
||||||
the corresponding folder and use relative paths.
|
the corresponding folder and use relative paths.
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
@ -69,7 +69,7 @@ There are case insensitive variants of ``--exclude`` and ``--include`` called
|
|||||||
ignore the casing of paths.
|
ignore the casing of paths.
|
||||||
|
|
||||||
There are also ``--include-file``, ``--exclude-file``, ``--iinclude-file`` and
|
There are also ``--include-file``, ``--exclude-file``, ``--iinclude-file`` and
|
||||||
``--iexclude-file`` flags that read the include and exclude patterns from a file.
|
``--iexclude-file`` flags that read the include and exclude patterns from a file.
|
||||||
|
|
||||||
Restoring symbolic links on windows is only possible when the user has
|
Restoring symbolic links on windows is only possible when the user has
|
||||||
``SeCreateSymbolicLinkPrivilege`` privilege or is running as admin. This is a
|
``SeCreateSymbolicLinkPrivilege`` privilege or is running as admin. This is a
|
||||||
@ -111,6 +111,28 @@ values are supported:
|
|||||||
newer modification time (mtime).
|
newer modification time (mtime).
|
||||||
* ``--overwrite never``: never overwrite existing files.
|
* ``--overwrite never``: never overwrite existing files.
|
||||||
|
|
||||||
|
Dry run
|
||||||
|
-------
|
||||||
|
|
||||||
|
As restore operations can take a long time, it can be useful to perform a dry-run to
|
||||||
|
see what would be restored without having to run the full restore operation. The
|
||||||
|
restore command supports the ``--dry-run`` option and prints information about the
|
||||||
|
restored files when specifying ``--verbose=2``.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ restic restore --target /tmp/restore-work --dry-run --verbose=2 latest
|
||||||
|
|
||||||
|
unchanged /restic/internal/walker/walker.go with size 2.812 KiB
|
||||||
|
updated /restic/internal/walker/walker_test.go with size 11.143 KiB
|
||||||
|
restored /restic/restic with size 35.318 MiB
|
||||||
|
restored /restic
|
||||||
|
[...]
|
||||||
|
Summary: Restored 9072 files/dirs (153.597 MiB) in 0:00
|
||||||
|
|
||||||
|
Files with already up to date content are reported as ``unchanged``. Files whose content
|
||||||
|
was modified are ``updated`` and files that are new are shown as ``restored``. Directories
|
||||||
|
and other file types like symlinks are always reported as ``restored``.
|
||||||
|
|
||||||
Restore using mount
|
Restore using mount
|
||||||
===================
|
===================
|
||||||
|
@ -511,6 +511,22 @@ Status
|
|||||||
|``bytes_skipped`` | Total size of skipped files |
|
|``bytes_skipped`` | Total size of skipped files |
|
||||||
+----------------------+------------------------------------------------------------+
|
+----------------------+------------------------------------------------------------+
|
||||||
|
|
||||||
|
Verbose Status
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Verbose status provides details about the progress, including details about restored files.
|
||||||
|
Only printed if `--verbose=2` is specified.
|
||||||
|
|
||||||
|
+----------------------+-----------------------------------------------------------+
|
||||||
|
| ``message_type`` | Always "verbose_status" |
|
||||||
|
+----------------------+-----------------------------------------------------------+
|
||||||
|
| ``action`` | Either "restored", "updated" or "unchanged" |
|
||||||
|
+----------------------+-----------------------------------------------------------+
|
||||||
|
| ``item`` | The item in question |
|
||||||
|
+----------------------+-----------------------------------------------------------+
|
||||||
|
| ``size`` | Size of the item in bytes |
|
||||||
|
+----------------------+-----------------------------------------------------------+
|
||||||
|
|
||||||
Summary
|
Summary
|
||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
||||||
|
@ -215,7 +215,7 @@ func (r *fileRestorer) restoreEmptyFileAt(location string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.progress.AddProgress(location, 0, 0)
|
r.progress.AddProgress(location, restore.ActionFileRestored, 0, 0)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,7 +337,11 @@ func (r *fileRestorer) downloadBlobs(ctx context.Context, packID restic.ID,
|
|||||||
createSize = file.size
|
createSize = file.size
|
||||||
}
|
}
|
||||||
writeErr := r.filesWriter.writeToFile(r.targetPath(file.location), blobData, offset, createSize, file.sparse)
|
writeErr := r.filesWriter.writeToFile(r.targetPath(file.location), blobData, offset, createSize, file.sparse)
|
||||||
r.progress.AddProgress(file.location, uint64(len(blobData)), uint64(file.size))
|
action := restore.ActionFileUpdated
|
||||||
|
if file.state == nil {
|
||||||
|
action = restore.ActionFileRestored
|
||||||
|
}
|
||||||
|
r.progress.AddProgress(file.location, action, uint64(len(blobData)), uint64(file.size))
|
||||||
return writeErr
|
return writeErr
|
||||||
}
|
}
|
||||||
err := r.sanitizeError(file, writeToFile())
|
err := r.sanitizeError(file, writeToFile())
|
||||||
|
@ -33,6 +33,7 @@ type Restorer struct {
|
|||||||
var restorerAbortOnAllErrors = func(_ string, err error) error { return err }
|
var restorerAbortOnAllErrors = func(_ string, err error) error { return err }
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
DryRun bool
|
||||||
Sparse bool
|
Sparse bool
|
||||||
Progress *restoreui.Progress
|
Progress *restoreui.Progress
|
||||||
Overwrite OverwriteBehavior
|
Overwrite OverwriteBehavior
|
||||||
@ -220,22 +221,27 @@ func (res *Restorer) traverseTree(ctx context.Context, target, location string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, target, location string) error {
|
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 !res.opts.DryRun {
|
||||||
if err := fs.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
|
debug.Log("restoreNode %v %v %v", node.Name, target, location)
|
||||||
return errors.Wrap(err, "RemoveNode")
|
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)
|
res.opts.Progress.AddProgress(location, restoreui.ActionFileRestored, 0, 0)
|
||||||
if err != nil {
|
|
||||||
debug.Log("node.CreateAt(%s) error %v", target, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.opts.Progress.AddProgress(location, 0, 0)
|
|
||||||
return res.restoreNodeMetadataTo(node, target, location)
|
return res.restoreNodeMetadataTo(node, target, location)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location string) error {
|
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)
|
debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location)
|
||||||
err := node.RestoreMetadata(target, res.Warn)
|
err := node.RestoreMetadata(target, res.Warn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -245,21 +251,26 @@ func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (res *Restorer) restoreHardlinkAt(node *restic.Node, target, path, location string) error {
|
func (res *Restorer) restoreHardlinkAt(node *restic.Node, target, path, location string) error {
|
||||||
if err := fs.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
if !res.opts.DryRun {
|
||||||
return errors.Wrap(err, "RemoveCreateHardlink")
|
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 {
|
err := fs.Link(target, path)
|
||||||
return errors.WithStack(err)
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.opts.Progress.AddProgress(location, 0, 0)
|
res.opts.Progress.AddProgress(location, restoreui.ActionFileRestored, 0, 0)
|
||||||
|
|
||||||
// TODO investigate if hardlinks have separate metadata on any supported system
|
// TODO investigate if hardlinks have separate metadata on any supported system
|
||||||
return res.restoreNodeMetadataTo(node, path, location)
|
return res.restoreNodeMetadataTo(node, path, location)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (res *Restorer) ensureDir(target string) error {
|
func (res *Restorer) ensureDir(target string) error {
|
||||||
|
if res.opts.DryRun {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
fi, err := fs.Lstat(target)
|
fi, err := fs.Lstat(target)
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
return fmt.Errorf("failed to check for directory: %w", err)
|
return fmt.Errorf("failed to check for directory: %w", err)
|
||||||
@ -324,12 +335,21 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||||||
idx.Add(node.Inode, node.DeviceID, location)
|
idx.Add(node.Inode, node.DeviceID, location)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err = res.withOverwriteCheck(node, target, false, buf, func(updateMetadataOnly bool, matches *fileState) error {
|
buf, err = res.withOverwriteCheck(node, target, location, false, buf, func(updateMetadataOnly bool, matches *fileState) error {
|
||||||
if updateMetadataOnly {
|
if updateMetadataOnly {
|
||||||
res.opts.Progress.AddSkippedFile(node.Size)
|
res.opts.Progress.AddSkippedFile(location, node.Size)
|
||||||
} else {
|
} else {
|
||||||
res.opts.Progress.AddFile(node.Size)
|
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 {
|
||||||
|
action := restoreui.ActionFileUpdated
|
||||||
|
if matches == nil {
|
||||||
|
action = restoreui.ActionFileRestored
|
||||||
|
}
|
||||||
|
// immediately mark as completed
|
||||||
|
res.opts.Progress.AddProgress(location, action, node.Size, node.Size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.trackFile(location, updateMetadataOnly)
|
res.trackFile(location, updateMetadataOnly)
|
||||||
return nil
|
return nil
|
||||||
@ -341,9 +361,11 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = filerestorer.restoreFiles(ctx)
|
if !res.opts.DryRun {
|
||||||
if err != nil {
|
err = filerestorer.restoreFiles(ctx)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.Log("second pass for %q", dst)
|
debug.Log("second pass for %q", dst)
|
||||||
@ -353,14 +375,14 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||||||
visitNode: func(node *restic.Node, target, location string) error {
|
visitNode: func(node *restic.Node, target, location string) error {
|
||||||
debug.Log("second pass, visitNode: restore node %q", location)
|
debug.Log("second pass, visitNode: restore node %q", location)
|
||||||
if node.Type != "file" {
|
if node.Type != "file" {
|
||||||
_, err := res.withOverwriteCheck(node, target, false, nil, func(_ bool, _ *fileState) error {
|
_, err := res.withOverwriteCheck(node, target, location, false, nil, func(_ bool, _ *fileState) error {
|
||||||
return res.restoreNodeTo(ctx, node, target, location)
|
return res.restoreNodeTo(ctx, node, target, location)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx.Has(node.Inode, node.DeviceID) && idx.Value(node.Inode, node.DeviceID) != location {
|
if idx.Has(node.Inode, node.DeviceID) && idx.Value(node.Inode, node.DeviceID) != location {
|
||||||
_, err := res.withOverwriteCheck(node, target, true, nil, func(_ bool, _ *fileState) error {
|
_, err := res.withOverwriteCheck(node, target, location, true, nil, func(_ bool, _ *fileState) error {
|
||||||
return res.restoreHardlinkAt(node, filerestorer.targetPath(idx.Value(node.Inode, node.DeviceID)), target, location)
|
return res.restoreHardlinkAt(node, filerestorer.targetPath(idx.Value(node.Inode, node.DeviceID)), target, location)
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
@ -375,7 +397,7 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
|||||||
leaveDir: func(node *restic.Node, target, location string) error {
|
leaveDir: func(node *restic.Node, target, location string) error {
|
||||||
err := res.restoreNodeMetadataTo(node, target, location)
|
err := res.restoreNodeMetadataTo(node, target, location)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
res.opts.Progress.AddProgress(location, 0, 0)
|
res.opts.Progress.AddProgress(location, restoreui.ActionDirRestored, 0, 0)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
@ -392,7 +414,7 @@ func (res *Restorer) hasRestoredFile(location string) (metadataOnly bool, ok boo
|
|||||||
return metadataOnly, ok
|
return metadataOnly, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (res *Restorer) withOverwriteCheck(node *restic.Node, target string, isHardlink bool, buf []byte, cb func(updateMetadataOnly bool, matches *fileState) error) ([]byte, error) {
|
func (res *Restorer) withOverwriteCheck(node *restic.Node, target, location string, isHardlink bool, buf []byte, cb func(updateMetadataOnly bool, matches *fileState) error) ([]byte, error) {
|
||||||
overwrite, err := shouldOverwrite(res.opts.Overwrite, node, target)
|
overwrite, err := shouldOverwrite(res.opts.Overwrite, node, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return buf, err
|
return buf, err
|
||||||
@ -401,7 +423,7 @@ func (res *Restorer) withOverwriteCheck(node *restic.Node, target string, isHard
|
|||||||
if isHardlink {
|
if isHardlink {
|
||||||
size = 0
|
size = 0
|
||||||
}
|
}
|
||||||
res.opts.Progress.AddSkippedFile(size)
|
res.opts.Progress.AddSkippedFile(location, size)
|
||||||
return buf, nil
|
return buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/archiver"
|
"github.com/restic/restic/internal/archiver"
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"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)
|
||||||
|
}
|
||||||
|
@ -76,11 +76,21 @@ type printerMock struct {
|
|||||||
|
|
||||||
func (p *printerMock) Update(_ restoreui.State, _ time.Duration) {
|
func (p *printerMock) Update(_ restoreui.State, _ time.Duration) {
|
||||||
}
|
}
|
||||||
|
func (p *printerMock) CompleteItem(action restoreui.ItemAction, item string, size uint64) {
|
||||||
|
}
|
||||||
func (p *printerMock) Finish(s restoreui.State, _ time.Duration) {
|
func (p *printerMock) Finish(s restoreui.State, _ time.Duration) {
|
||||||
p.s = s
|
p.s = s
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRestorerProgressBar(t *testing.T) {
|
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)
|
repo := repository.TestRepository(t)
|
||||||
|
|
||||||
sn, _ := saveSnapshot(t, repo, Snapshot{
|
sn, _ := saveSnapshot(t, repo, Snapshot{
|
||||||
@ -97,7 +107,7 @@ func TestRestorerProgressBar(t *testing.T) {
|
|||||||
|
|
||||||
mock := &printerMock{}
|
mock := &printerMock{}
|
||||||
progress := restoreui.NewProgress(mock, 0)
|
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) {
|
res.SelectFilter = func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) {
|
||||||
return true, true
|
return true, true
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type jsonPrinter struct {
|
type jsonPrinter struct {
|
||||||
terminal term
|
terminal term
|
||||||
|
verbosity uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJSONProgress(terminal term) ProgressPrinter {
|
func NewJSONProgress(terminal term, verbosity uint) ProgressPrinter {
|
||||||
return &jsonPrinter{
|
return &jsonPrinter{
|
||||||
terminal: terminal,
|
terminal: terminal,
|
||||||
|
verbosity: verbosity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +41,34 @@ func (t *jsonPrinter) Update(p State, duration time.Duration) {
|
|||||||
t.print(status)
|
t.print(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *jsonPrinter) CompleteItem(messageType ItemAction, item string, size uint64) {
|
||||||
|
if t.verbosity < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var action string
|
||||||
|
switch messageType {
|
||||||
|
case ActionDirRestored:
|
||||||
|
action = "restored"
|
||||||
|
case ActionFileRestored:
|
||||||
|
action = "restored"
|
||||||
|
case ActionFileUpdated:
|
||||||
|
action = "updated"
|
||||||
|
case ActionFileUnchanged:
|
||||||
|
action = "unchanged"
|
||||||
|
default:
|
||||||
|
panic("unknown message type")
|
||||||
|
}
|
||||||
|
|
||||||
|
status := verboseUpdate{
|
||||||
|
MessageType: "verbose_status",
|
||||||
|
Action: action,
|
||||||
|
Item: item,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
t.print(status)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *jsonPrinter) Finish(p State, duration time.Duration) {
|
func (t *jsonPrinter) Finish(p State, duration time.Duration) {
|
||||||
status := summaryOutput{
|
status := summaryOutput{
|
||||||
MessageType: "summary",
|
MessageType: "summary",
|
||||||
@ -65,6 +95,13 @@ type statusUpdate struct {
|
|||||||
BytesSkipped uint64 `json:"bytes_skipped,omitempty"`
|
BytesSkipped uint64 `json:"bytes_skipped,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type verboseUpdate struct {
|
||||||
|
MessageType string `json:"message_type"` // "verbose_status"
|
||||||
|
Action string `json:"action"`
|
||||||
|
Item string `json:"item"`
|
||||||
|
Size uint64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
type summaryOutput struct {
|
type summaryOutput struct {
|
||||||
MessageType string `json:"message_type"` // "summary"
|
MessageType string `json:"message_type"` // "summary"
|
||||||
SecondsElapsed uint64 `json:"seconds_elapsed,omitempty"`
|
SecondsElapsed uint64 `json:"seconds_elapsed,omitempty"`
|
||||||
|
@ -7,37 +7,55 @@ import (
|
|||||||
"github.com/restic/restic/internal/test"
|
"github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJSONPrintUpdate(t *testing.T) {
|
func createJSONProgress() (*mockTerm, ProgressPrinter) {
|
||||||
term := &mockTerm{}
|
term := &mockTerm{}
|
||||||
printer := NewJSONProgress(term)
|
printer := NewJSONProgress(term, 3)
|
||||||
|
return term, printer
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONPrintUpdate(t *testing.T) {
|
||||||
|
term, printer := createJSONProgress()
|
||||||
printer.Update(State{3, 11, 0, 29, 47, 0}, 5*time.Second)
|
printer.Update(State{3, 11, 0, 29, 47, 0}, 5*time.Second)
|
||||||
test.Equals(t, []string{"{\"message_type\":\"status\",\"seconds_elapsed\":5,\"percent_done\":0.6170212765957447,\"total_files\":11,\"files_restored\":3,\"total_bytes\":47,\"bytes_restored\":29}\n"}, term.output)
|
test.Equals(t, []string{"{\"message_type\":\"status\",\"seconds_elapsed\":5,\"percent_done\":0.6170212765957447,\"total_files\":11,\"files_restored\":3,\"total_bytes\":47,\"bytes_restored\":29}\n"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJSONPrintUpdateWithSkipped(t *testing.T) {
|
func TestJSONPrintUpdateWithSkipped(t *testing.T) {
|
||||||
term := &mockTerm{}
|
term, printer := createJSONProgress()
|
||||||
printer := NewJSONProgress(term)
|
|
||||||
printer.Update(State{3, 11, 2, 29, 47, 59}, 5*time.Second)
|
printer.Update(State{3, 11, 2, 29, 47, 59}, 5*time.Second)
|
||||||
test.Equals(t, []string{"{\"message_type\":\"status\",\"seconds_elapsed\":5,\"percent_done\":0.6170212765957447,\"total_files\":11,\"files_restored\":3,\"files_skipped\":2,\"total_bytes\":47,\"bytes_restored\":29,\"bytes_skipped\":59}\n"}, term.output)
|
test.Equals(t, []string{"{\"message_type\":\"status\",\"seconds_elapsed\":5,\"percent_done\":0.6170212765957447,\"total_files\":11,\"files_restored\":3,\"files_skipped\":2,\"total_bytes\":47,\"bytes_restored\":29,\"bytes_skipped\":59}\n"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJSONPrintSummaryOnSuccess(t *testing.T) {
|
func TestJSONPrintSummaryOnSuccess(t *testing.T) {
|
||||||
term := &mockTerm{}
|
term, printer := createJSONProgress()
|
||||||
printer := NewJSONProgress(term)
|
|
||||||
printer.Finish(State{11, 11, 0, 47, 47, 0}, 5*time.Second)
|
printer.Finish(State{11, 11, 0, 47, 47, 0}, 5*time.Second)
|
||||||
test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":11,\"total_bytes\":47,\"bytes_restored\":47}\n"}, term.output)
|
test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":11,\"total_bytes\":47,\"bytes_restored\":47}\n"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJSONPrintSummaryOnErrors(t *testing.T) {
|
func TestJSONPrintSummaryOnErrors(t *testing.T) {
|
||||||
term := &mockTerm{}
|
term, printer := createJSONProgress()
|
||||||
printer := NewJSONProgress(term)
|
|
||||||
printer.Finish(State{3, 11, 0, 29, 47, 0}, 5*time.Second)
|
printer.Finish(State{3, 11, 0, 29, 47, 0}, 5*time.Second)
|
||||||
test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":3,\"total_bytes\":47,\"bytes_restored\":29}\n"}, term.output)
|
test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":3,\"total_bytes\":47,\"bytes_restored\":29}\n"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJSONPrintSummaryOnSuccessWithSkipped(t *testing.T) {
|
func TestJSONPrintSummaryOnSuccessWithSkipped(t *testing.T) {
|
||||||
term := &mockTerm{}
|
term, printer := createJSONProgress()
|
||||||
printer := NewJSONProgress(term)
|
|
||||||
printer.Finish(State{11, 11, 2, 47, 47, 59}, 5*time.Second)
|
printer.Finish(State{11, 11, 2, 47, 47, 59}, 5*time.Second)
|
||||||
test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":11,\"files_skipped\":2,\"total_bytes\":47,\"bytes_restored\":47,\"bytes_skipped\":59}\n"}, term.output)
|
test.Equals(t, []string{"{\"message_type\":\"summary\",\"seconds_elapsed\":5,\"total_files\":11,\"files_restored\":11,\"files_skipped\":2,\"total_bytes\":47,\"bytes_restored\":47,\"bytes_skipped\":59}\n"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJSONPrintCompleteItem(t *testing.T) {
|
||||||
|
for _, data := range []struct {
|
||||||
|
action ItemAction
|
||||||
|
size uint64
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{ActionDirRestored, 0, "{\"message_type\":\"verbose_status\",\"action\":\"restored\",\"item\":\"test\",\"size\":0}\n"},
|
||||||
|
{ActionFileRestored, 123, "{\"message_type\":\"verbose_status\",\"action\":\"restored\",\"item\":\"test\",\"size\":123}\n"},
|
||||||
|
{ActionFileUpdated, 123, "{\"message_type\":\"verbose_status\",\"action\":\"updated\",\"item\":\"test\",\"size\":123}\n"},
|
||||||
|
{ActionFileUnchanged, 123, "{\"message_type\":\"verbose_status\",\"action\":\"unchanged\",\"item\":\"test\",\"size\":123}\n"},
|
||||||
|
} {
|
||||||
|
term, printer := createJSONProgress()
|
||||||
|
printer.CompleteItem(data.action, "test", data.size)
|
||||||
|
test.Equals(t, []string{data.expected}, term.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -39,9 +39,20 @@ type term interface {
|
|||||||
|
|
||||||
type ProgressPrinter interface {
|
type ProgressPrinter interface {
|
||||||
Update(progress State, duration time.Duration)
|
Update(progress State, duration time.Duration)
|
||||||
|
CompleteItem(action ItemAction, item string, size uint64)
|
||||||
Finish(progress State, duration time.Duration)
|
Finish(progress State, duration time.Duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ItemAction string
|
||||||
|
|
||||||
|
// Constants for the different CompleteItem actions.
|
||||||
|
const (
|
||||||
|
ActionDirRestored ItemAction = "dir restored"
|
||||||
|
ActionFileRestored ItemAction = "file restored"
|
||||||
|
ActionFileUpdated ItemAction = "file updated"
|
||||||
|
ActionFileUnchanged ItemAction = "file unchanged"
|
||||||
|
)
|
||||||
|
|
||||||
func NewProgress(printer ProgressPrinter, interval time.Duration) *Progress {
|
func NewProgress(printer ProgressPrinter, interval time.Duration) *Progress {
|
||||||
p := &Progress{
|
p := &Progress{
|
||||||
progressInfoMap: make(map[string]progressInfoEntry),
|
progressInfoMap: make(map[string]progressInfoEntry),
|
||||||
@ -77,7 +88,7 @@ func (p *Progress) AddFile(size uint64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddProgress accumulates the number of bytes written for a file
|
// AddProgress accumulates the number of bytes written for a file
|
||||||
func (p *Progress) AddProgress(name string, bytesWrittenPortion uint64, bytesTotal uint64) {
|
func (p *Progress) AddProgress(name string, action ItemAction, bytesWrittenPortion uint64, bytesTotal uint64) {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -96,10 +107,12 @@ func (p *Progress) AddProgress(name string, bytesWrittenPortion uint64, bytesTot
|
|||||||
if entry.bytesWritten == entry.bytesTotal {
|
if entry.bytesWritten == entry.bytesTotal {
|
||||||
delete(p.progressInfoMap, name)
|
delete(p.progressInfoMap, name)
|
||||||
p.s.FilesFinished++
|
p.s.FilesFinished++
|
||||||
|
|
||||||
|
p.printer.CompleteItem(action, name, bytesTotal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Progress) AddSkippedFile(size uint64) {
|
func (p *Progress) AddSkippedFile(name string, size uint64) {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -109,6 +122,8 @@ func (p *Progress) AddSkippedFile(size uint64) {
|
|||||||
|
|
||||||
p.s.FilesSkipped++
|
p.s.FilesSkipped++
|
||||||
p.s.AllBytesSkipped += size
|
p.s.AllBytesSkipped += size
|
||||||
|
|
||||||
|
p.printer.CompleteItem(ActionFileUnchanged, name, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Progress) Finish() {
|
func (p *Progress) Finish() {
|
||||||
|
@ -16,8 +16,16 @@ type printerTraceEntry struct {
|
|||||||
|
|
||||||
type printerTrace []printerTraceEntry
|
type printerTrace []printerTraceEntry
|
||||||
|
|
||||||
|
type itemTraceEntry struct {
|
||||||
|
action ItemAction
|
||||||
|
item string
|
||||||
|
size uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type itemTrace []itemTraceEntry
|
||||||
type mockPrinter struct {
|
type mockPrinter struct {
|
||||||
trace printerTrace
|
trace printerTrace
|
||||||
|
items itemTrace
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockFinishDuration = 42 * time.Second
|
const mockFinishDuration = 42 * time.Second
|
||||||
@ -25,95 +33,109 @@ const mockFinishDuration = 42 * time.Second
|
|||||||
func (p *mockPrinter) Update(progress State, duration time.Duration) {
|
func (p *mockPrinter) Update(progress State, duration time.Duration) {
|
||||||
p.trace = append(p.trace, printerTraceEntry{progress, duration, false})
|
p.trace = append(p.trace, printerTraceEntry{progress, duration, false})
|
||||||
}
|
}
|
||||||
|
func (p *mockPrinter) CompleteItem(action ItemAction, item string, size uint64) {
|
||||||
|
p.items = append(p.items, itemTraceEntry{action, item, size})
|
||||||
|
}
|
||||||
func (p *mockPrinter) Finish(progress State, _ time.Duration) {
|
func (p *mockPrinter) Finish(progress State, _ time.Duration) {
|
||||||
p.trace = append(p.trace, printerTraceEntry{progress, mockFinishDuration, true})
|
p.trace = append(p.trace, printerTraceEntry{progress, mockFinishDuration, true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testProgress(fn func(progress *Progress) bool) printerTrace {
|
func testProgress(fn func(progress *Progress) bool) (printerTrace, itemTrace) {
|
||||||
printer := &mockPrinter{}
|
printer := &mockPrinter{}
|
||||||
progress := NewProgress(printer, 0)
|
progress := NewProgress(printer, 0)
|
||||||
final := fn(progress)
|
final := fn(progress)
|
||||||
progress.update(0, final)
|
progress.update(0, final)
|
||||||
trace := append(printerTrace{}, printer.trace...)
|
trace := append(printerTrace{}, printer.trace...)
|
||||||
|
items := append(itemTrace{}, printer.items...)
|
||||||
// cleanup to avoid goroutine leak, but copy trace first
|
// cleanup to avoid goroutine leak, but copy trace first
|
||||||
progress.Finish()
|
progress.Finish()
|
||||||
return trace
|
return trace, items
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
result := testProgress(func(progress *Progress) bool {
|
result, items := testProgress(func(progress *Progress) bool {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
test.Equals(t, printerTrace{
|
test.Equals(t, printerTrace{
|
||||||
printerTraceEntry{State{0, 0, 0, 0, 0, 0}, 0, false},
|
printerTraceEntry{State{0, 0, 0, 0, 0, 0}, 0, false},
|
||||||
}, result)
|
}, result)
|
||||||
|
test.Equals(t, itemTrace{}, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddFile(t *testing.T) {
|
func TestAddFile(t *testing.T) {
|
||||||
fileSize := uint64(100)
|
fileSize := uint64(100)
|
||||||
|
|
||||||
result := testProgress(func(progress *Progress) bool {
|
result, items := testProgress(func(progress *Progress) bool {
|
||||||
progress.AddFile(fileSize)
|
progress.AddFile(fileSize)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
test.Equals(t, printerTrace{
|
test.Equals(t, printerTrace{
|
||||||
printerTraceEntry{State{0, 1, 0, 0, fileSize, 0}, 0, false},
|
printerTraceEntry{State{0, 1, 0, 0, fileSize, 0}, 0, false},
|
||||||
}, result)
|
}, result)
|
||||||
|
test.Equals(t, itemTrace{}, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFirstProgressOnAFile(t *testing.T) {
|
func TestFirstProgressOnAFile(t *testing.T) {
|
||||||
expectedBytesWritten := uint64(5)
|
expectedBytesWritten := uint64(5)
|
||||||
expectedBytesTotal := uint64(100)
|
expectedBytesTotal := uint64(100)
|
||||||
|
|
||||||
result := testProgress(func(progress *Progress) bool {
|
result, items := testProgress(func(progress *Progress) bool {
|
||||||
progress.AddFile(expectedBytesTotal)
|
progress.AddFile(expectedBytesTotal)
|
||||||
progress.AddProgress("test", expectedBytesWritten, expectedBytesTotal)
|
progress.AddProgress("test", ActionFileUpdated, expectedBytesWritten, expectedBytesTotal)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
test.Equals(t, printerTrace{
|
test.Equals(t, printerTrace{
|
||||||
printerTraceEntry{State{0, 1, 0, expectedBytesWritten, expectedBytesTotal, 0}, 0, false},
|
printerTraceEntry{State{0, 1, 0, expectedBytesWritten, expectedBytesTotal, 0}, 0, false},
|
||||||
}, result)
|
}, result)
|
||||||
|
test.Equals(t, itemTrace{}, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLastProgressOnAFile(t *testing.T) {
|
func TestLastProgressOnAFile(t *testing.T) {
|
||||||
fileSize := uint64(100)
|
fileSize := uint64(100)
|
||||||
|
|
||||||
result := testProgress(func(progress *Progress) bool {
|
result, items := testProgress(func(progress *Progress) bool {
|
||||||
progress.AddFile(fileSize)
|
progress.AddFile(fileSize)
|
||||||
progress.AddProgress("test", 30, fileSize)
|
progress.AddProgress("test", ActionFileUpdated, 30, fileSize)
|
||||||
progress.AddProgress("test", 35, fileSize)
|
progress.AddProgress("test", ActionFileUpdated, 35, fileSize)
|
||||||
progress.AddProgress("test", 35, fileSize)
|
progress.AddProgress("test", ActionFileUpdated, 35, fileSize)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
test.Equals(t, printerTrace{
|
test.Equals(t, printerTrace{
|
||||||
printerTraceEntry{State{1, 1, 0, fileSize, fileSize, 0}, 0, false},
|
printerTraceEntry{State{1, 1, 0, fileSize, fileSize, 0}, 0, false},
|
||||||
}, result)
|
}, result)
|
||||||
|
test.Equals(t, itemTrace{
|
||||||
|
itemTraceEntry{action: ActionFileUpdated, item: "test", size: fileSize},
|
||||||
|
}, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLastProgressOnLastFile(t *testing.T) {
|
func TestLastProgressOnLastFile(t *testing.T) {
|
||||||
fileSize := uint64(100)
|
fileSize := uint64(100)
|
||||||
|
|
||||||
result := testProgress(func(progress *Progress) bool {
|
result, items := testProgress(func(progress *Progress) bool {
|
||||||
progress.AddFile(fileSize)
|
progress.AddFile(fileSize)
|
||||||
progress.AddFile(50)
|
progress.AddFile(50)
|
||||||
progress.AddProgress("test1", 50, 50)
|
progress.AddProgress("test1", ActionFileUpdated, 50, 50)
|
||||||
progress.AddProgress("test2", 50, fileSize)
|
progress.AddProgress("test2", ActionFileUpdated, 50, fileSize)
|
||||||
progress.AddProgress("test2", 50, fileSize)
|
progress.AddProgress("test2", ActionFileUpdated, 50, fileSize)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
test.Equals(t, printerTrace{
|
test.Equals(t, printerTrace{
|
||||||
printerTraceEntry{State{2, 2, 0, 50 + fileSize, 50 + fileSize, 0}, 0, false},
|
printerTraceEntry{State{2, 2, 0, 50 + fileSize, 50 + fileSize, 0}, 0, false},
|
||||||
}, result)
|
}, result)
|
||||||
|
test.Equals(t, itemTrace{
|
||||||
|
itemTraceEntry{action: ActionFileUpdated, item: "test1", size: 50},
|
||||||
|
itemTraceEntry{action: ActionFileUpdated, item: "test2", size: fileSize},
|
||||||
|
}, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSummaryOnSuccess(t *testing.T) {
|
func TestSummaryOnSuccess(t *testing.T) {
|
||||||
fileSize := uint64(100)
|
fileSize := uint64(100)
|
||||||
|
|
||||||
result := testProgress(func(progress *Progress) bool {
|
result, _ := testProgress(func(progress *Progress) bool {
|
||||||
progress.AddFile(fileSize)
|
progress.AddFile(fileSize)
|
||||||
progress.AddFile(50)
|
progress.AddFile(50)
|
||||||
progress.AddProgress("test1", 50, 50)
|
progress.AddProgress("test1", ActionFileUpdated, 50, 50)
|
||||||
progress.AddProgress("test2", fileSize, fileSize)
|
progress.AddProgress("test2", ActionFileUpdated, fileSize, fileSize)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
test.Equals(t, printerTrace{
|
test.Equals(t, printerTrace{
|
||||||
@ -124,11 +146,11 @@ func TestSummaryOnSuccess(t *testing.T) {
|
|||||||
func TestSummaryOnErrors(t *testing.T) {
|
func TestSummaryOnErrors(t *testing.T) {
|
||||||
fileSize := uint64(100)
|
fileSize := uint64(100)
|
||||||
|
|
||||||
result := testProgress(func(progress *Progress) bool {
|
result, _ := testProgress(func(progress *Progress) bool {
|
||||||
progress.AddFile(fileSize)
|
progress.AddFile(fileSize)
|
||||||
progress.AddFile(50)
|
progress.AddFile(50)
|
||||||
progress.AddProgress("test1", 50, 50)
|
progress.AddProgress("test1", ActionFileUpdated, 50, 50)
|
||||||
progress.AddProgress("test2", fileSize/2, fileSize)
|
progress.AddProgress("test2", ActionFileUpdated, fileSize/2, fileSize)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
test.Equals(t, printerTrace{
|
test.Equals(t, printerTrace{
|
||||||
@ -139,11 +161,30 @@ func TestSummaryOnErrors(t *testing.T) {
|
|||||||
func TestSkipFile(t *testing.T) {
|
func TestSkipFile(t *testing.T) {
|
||||||
fileSize := uint64(100)
|
fileSize := uint64(100)
|
||||||
|
|
||||||
result := testProgress(func(progress *Progress) bool {
|
result, items := testProgress(func(progress *Progress) bool {
|
||||||
progress.AddSkippedFile(fileSize)
|
progress.AddSkippedFile("test", fileSize)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
test.Equals(t, printerTrace{
|
test.Equals(t, printerTrace{
|
||||||
printerTraceEntry{State{0, 0, 1, 0, 0, fileSize}, mockFinishDuration, true},
|
printerTraceEntry{State{0, 0, 1, 0, 0, fileSize}, mockFinishDuration, true},
|
||||||
}, result)
|
}, result)
|
||||||
|
test.Equals(t, itemTrace{
|
||||||
|
itemTraceEntry{ActionFileUnchanged, "test", fileSize},
|
||||||
|
}, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProgressTypes(t *testing.T) {
|
||||||
|
fileSize := uint64(100)
|
||||||
|
|
||||||
|
_, items := testProgress(func(progress *Progress) bool {
|
||||||
|
progress.AddFile(fileSize)
|
||||||
|
progress.AddFile(0)
|
||||||
|
progress.AddProgress("dir", ActionDirRestored, fileSize, fileSize)
|
||||||
|
progress.AddProgress("new", ActionFileRestored, 0, 0)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
test.Equals(t, itemTrace{
|
||||||
|
itemTraceEntry{ActionDirRestored, "dir", fileSize},
|
||||||
|
itemTraceEntry{ActionFileRestored, "new", 0},
|
||||||
|
}, items)
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type textPrinter struct {
|
type textPrinter struct {
|
||||||
terminal term
|
terminal term
|
||||||
|
verbosity uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTextProgress(terminal term) ProgressPrinter {
|
func NewTextProgress(terminal term, verbosity uint) ProgressPrinter {
|
||||||
return &textPrinter{
|
return &textPrinter{
|
||||||
terminal: terminal,
|
terminal: terminal,
|
||||||
|
verbosity: verbosity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,6 +33,32 @@ func (t *textPrinter) Update(p State, duration time.Duration) {
|
|||||||
t.terminal.SetStatus([]string{progress})
|
t.terminal.SetStatus([]string{progress})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *textPrinter) CompleteItem(messageType ItemAction, item string, size uint64) {
|
||||||
|
if t.verbosity < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var action string
|
||||||
|
switch messageType {
|
||||||
|
case ActionDirRestored:
|
||||||
|
action = "restored"
|
||||||
|
case ActionFileRestored:
|
||||||
|
action = "restored"
|
||||||
|
case ActionFileUpdated:
|
||||||
|
action = "updated"
|
||||||
|
case ActionFileUnchanged:
|
||||||
|
action = "unchanged"
|
||||||
|
default:
|
||||||
|
panic("unknown message type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageType == ActionDirRestored {
|
||||||
|
t.terminal.Print(fmt.Sprintf("restored %v", item))
|
||||||
|
} else {
|
||||||
|
t.terminal.Print(fmt.Sprintf("%-9v %v with size %v", action, item, ui.FormatBytes(size)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *textPrinter) Finish(p State, duration time.Duration) {
|
func (t *textPrinter) Finish(p State, duration time.Duration) {
|
||||||
t.terminal.SetStatus([]string{})
|
t.terminal.SetStatus([]string{})
|
||||||
|
|
||||||
|
@ -19,37 +19,55 @@ func (m *mockTerm) SetStatus(lines []string) {
|
|||||||
m.output = append([]string{}, lines...)
|
m.output = append([]string{}, lines...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintUpdate(t *testing.T) {
|
func createTextProgress() (*mockTerm, ProgressPrinter) {
|
||||||
term := &mockTerm{}
|
term := &mockTerm{}
|
||||||
printer := NewTextProgress(term)
|
printer := NewTextProgress(term, 3)
|
||||||
|
return term, printer
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintUpdate(t *testing.T) {
|
||||||
|
term, printer := createTextProgress()
|
||||||
printer.Update(State{3, 11, 0, 29, 47, 0}, 5*time.Second)
|
printer.Update(State{3, 11, 0, 29, 47, 0}, 5*time.Second)
|
||||||
test.Equals(t, []string{"[0:05] 61.70% 3 files/dirs 29 B, total 11 files/dirs 47 B"}, term.output)
|
test.Equals(t, []string{"[0:05] 61.70% 3 files/dirs 29 B, total 11 files/dirs 47 B"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintUpdateWithSkipped(t *testing.T) {
|
func TestPrintUpdateWithSkipped(t *testing.T) {
|
||||||
term := &mockTerm{}
|
term, printer := createTextProgress()
|
||||||
printer := NewTextProgress(term)
|
|
||||||
printer.Update(State{3, 11, 2, 29, 47, 59}, 5*time.Second)
|
printer.Update(State{3, 11, 2, 29, 47, 59}, 5*time.Second)
|
||||||
test.Equals(t, []string{"[0:05] 61.70% 3 files/dirs 29 B, total 11 files/dirs 47 B, skipped 2 files/dirs 59 B"}, term.output)
|
test.Equals(t, []string{"[0:05] 61.70% 3 files/dirs 29 B, total 11 files/dirs 47 B, skipped 2 files/dirs 59 B"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintSummaryOnSuccess(t *testing.T) {
|
func TestPrintSummaryOnSuccess(t *testing.T) {
|
||||||
term := &mockTerm{}
|
term, printer := createTextProgress()
|
||||||
printer := NewTextProgress(term)
|
|
||||||
printer.Finish(State{11, 11, 0, 47, 47, 0}, 5*time.Second)
|
printer.Finish(State{11, 11, 0, 47, 47, 0}, 5*time.Second)
|
||||||
test.Equals(t, []string{"Summary: Restored 11 files/dirs (47 B) in 0:05"}, term.output)
|
test.Equals(t, []string{"Summary: Restored 11 files/dirs (47 B) in 0:05"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintSummaryOnErrors(t *testing.T) {
|
func TestPrintSummaryOnErrors(t *testing.T) {
|
||||||
term := &mockTerm{}
|
term, printer := createTextProgress()
|
||||||
printer := NewTextProgress(term)
|
|
||||||
printer.Finish(State{3, 11, 0, 29, 47, 0}, 5*time.Second)
|
printer.Finish(State{3, 11, 0, 29, 47, 0}, 5*time.Second)
|
||||||
test.Equals(t, []string{"Summary: Restored 3 / 11 files/dirs (29 B / 47 B) in 0:05"}, term.output)
|
test.Equals(t, []string{"Summary: Restored 3 / 11 files/dirs (29 B / 47 B) in 0:05"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintSummaryOnSuccessWithSkipped(t *testing.T) {
|
func TestPrintSummaryOnSuccessWithSkipped(t *testing.T) {
|
||||||
term := &mockTerm{}
|
term, printer := createTextProgress()
|
||||||
printer := NewTextProgress(term)
|
|
||||||
printer.Finish(State{11, 11, 2, 47, 47, 59}, 5*time.Second)
|
printer.Finish(State{11, 11, 2, 47, 47, 59}, 5*time.Second)
|
||||||
test.Equals(t, []string{"Summary: Restored 11 files/dirs (47 B) in 0:05, skipped 2 files/dirs 59 B"}, term.output)
|
test.Equals(t, []string{"Summary: Restored 11 files/dirs (47 B) in 0:05, skipped 2 files/dirs 59 B"}, term.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrintCompleteItem(t *testing.T) {
|
||||||
|
for _, data := range []struct {
|
||||||
|
action ItemAction
|
||||||
|
size uint64
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{ActionDirRestored, 0, "restored test"},
|
||||||
|
{ActionFileRestored, 123, "restored test with size 123 B"},
|
||||||
|
{ActionFileUpdated, 123, "updated test with size 123 B"},
|
||||||
|
{ActionFileUnchanged, 123, "unchanged test with size 123 B"},
|
||||||
|
} {
|
||||||
|
term, printer := createTextProgress()
|
||||||
|
printer.CompleteItem(data.action, "test", data.size)
|
||||||
|
test.Equals(t, []string{data.expected}, term.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user