Skip to content

Commit

Permalink
backup: add --dry-run/-n flag to show what would happen.
Browse files Browse the repository at this point in the history
This can be used to check how large a backup is or validate exclusions.
It does not actually write any data to the underlying backend. This is
implemented as a simple overlay backend that accepts writes without
forwarding them, passes through reads, and generally does the minimal
necessary to pretend that progress is actually happening.

Fixes restic#1542

Example usage:

$ restic -vv --dry-run . | grep add
new       /changelog/unreleased/issue-1542, saved in 0.000s (350 B added)
modified  /cmd/restic/cmd_backup.go, saved in 0.000s (16.543 KiB added)
modified  /cmd/restic/global.go, saved in 0.000s (0 B added)
new       /internal/backend/dry/dry_backend_test.go, saved in 0.000s (3.866 KiB added)
new       /internal/backend/dry/dry_backend.go, saved in 0.000s (3.744 KiB added)
modified  /internal/backend/test/tests.go, saved in 0.000s (0 B added)
modified  /internal/repository/repository.go, saved in 0.000s (20.707 KiB added)
modified  /internal/ui/backup.go, saved in 0.000s (9.110 KiB added)
modified  /internal/ui/jsonstatus/status.go, saved in 0.001s (11.055 KiB added)
modified  /restic, saved in 0.131s (25.542 MiB added)
Would add to the repo: 25.892 MiB
  • Loading branch information
rmmh committed May 19, 2020
1 parent 32ac548 commit 566118e
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 3 deletions.
9 changes: 9 additions & 0 deletions changelog/unreleased/issue-1542
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Enhancement: Add --dry-run/-n option to backup command.

We added a new --dry-run/-n option to backup, which performs all the normal
steps of a backup without actually writing data. Passing -vv will log
information about files that would be added, allowing fast verification of
backup options without any unnecessary write activity.

https://github.com/restic/restic/issues/1542
https://github.com/restic/restic/pull/2308
10 changes: 9 additions & 1 deletion cmd/restic/cmd_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type BackupOptions struct {
TimeStamp string
WithAtime bool
IgnoreInode bool
DryRun bool
}

var backupOptions BackupOptions
Expand Down Expand Up @@ -123,6 +124,7 @@ func init() {
f.StringVar(&backupOptions.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories")
f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number changes when checking for modified files")
f.BoolVarP(&backupOptions.DryRun, "dry-run", "n", false, "do not write anything, just print what would be done")
}

// filterExisting returns a slice of all existing items, or an error if no
Expand Down Expand Up @@ -433,6 +435,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
Run(ctx context.Context) error
Error(item string, fi os.FileInfo, err error) error
Finish(snapshotID restic.ID)
SetDryRun()

// ui.StdioWrapper
Stdout() io.WriteCloser
Expand All @@ -452,6 +455,11 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
p = ui.NewBackup(term, gopts.verbosity)
}

if opts.DryRun {
repo.SetDryRun()
p.SetDryRun()
}

// use the terminal for stdout/stderr
prevStdout, prevStderr := gopts.stdout, gopts.stderr
defer func() {
Expand Down Expand Up @@ -609,7 +617,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina

// Report finished execution
p.Finish(id)
if !gopts.JSON {
if !gopts.JSON && !opts.DryRun {
p.P("snapshot %s saved\n", id.Str())
}

Expand Down
15 changes: 14 additions & 1 deletion cmd/restic/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,23 @@ func TestBackup(t *testing.T) {

rtest.SetupTarTestFixture(t, env.testdata, datafile)
opts := BackupOptions{}
dryOpts := BackupOptions{DryRun: true}

// dry run before first backup
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
snapshotIDs := testRunList(t, "snapshots", env.gopts)
rtest.Assert(t, len(snapshotIDs) == 0,
"expected no snapshot, got %v", snapshotIDs)

// first backup
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
snapshotIDs := testRunList(t, "snapshots", env.gopts)
snapshotIDs = testRunList(t, "snapshots", env.gopts)
rtest.Assert(t, len(snapshotIDs) == 1,
"expected one snapshot, got %v", snapshotIDs)

// dry run between backups
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
snapshotIDs = testRunList(t, "snapshots", env.gopts)
rtest.Assert(t, len(snapshotIDs) == 1,
"expected one snapshot, got %v", snapshotIDs)

Expand Down
17 changes: 17 additions & 0 deletions doc/040_backup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ Now is a good time to run ``restic check`` to verify that all data
is properly stored in the repository. You should run this command regularly
to make sure the internal structure of the repository is free of errors.

Dry Runs
********

You can perform a backup in dry run mode to see what would happen without
modifying the repo.

- ``--dry-run``/``-n`` do not write anything, just print what would be done

Combined with ``--verbose``, you can see a list of changes:

.. code-block:: console
$ restic -r /srv/restic-repo backup ~/work --dry-run -vv | grep added
modified /plan.txt, saved in 0.000s (9.110 KiB added)
modified /archive.tar.gz, saved in 0.140s (25.542 MiB added)
Would be added to the repo: 25.551 MiB
Excluding Files
***************

Expand Down
188 changes: 188 additions & 0 deletions internal/backend/dryrun/dry_backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package dryrun

import (
"context"
"io"
"io/ioutil"
"sync"

"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic"

"github.com/restic/restic/internal/debug"
)

type sizeMap map[restic.Handle]int

var errNotFound = errors.New("not found")

// Backend passes reads through to an underlying layer and only records
// metadata about writes. This is used for `backup --dry-run`.
// It is directly derivted from the mem backend.
type Backend struct {
be restic.Backend
data sizeMap
m sync.Mutex
}

// New returns a new backend that saves all data in a map in memory.
func New(be restic.Backend) *Backend {
b := &Backend{
be: be,
data: make(sizeMap),
}

debug.Log("created new dry backend")

return b
}

// Test returns whether a file exists.
func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
be.m.Lock()
defer be.m.Unlock()

debug.Log("Test %v", h)

if _, ok := be.data[h]; ok {
return true, nil
}

return be.be.Test(ctx, h)
}

// IsNotExist returns true if the file does not exist.
func (be *Backend) IsNotExist(err error) bool {
return errors.Cause(err) == errNotFound || be.be.IsNotExist(err)
}

// Save adds new Data to the backend.
func (be *Backend) Save(ctx context.Context, h restic.Handle, rd restic.RewindReader) error {
if err := h.Valid(); err != nil {
return err
}

be.m.Lock()
defer be.m.Unlock()

if h.Type == restic.ConfigFile {
h.Name = ""
}

if _, ok := be.data[h]; ok {
return errors.New("file already exists")
}

buf, err := ioutil.ReadAll(rd)
if err != nil {
return err
}

be.data[h] = len(buf)
debug.Log("faked saving %v bytes at %v", len(buf), h)

return nil
}

// Load runs fn with a reader that yields the contents of the file at h at the
// given offset.
func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
be.m.Lock()
defer be.m.Unlock()

if _, ok := be.data[h]; ok {
return errors.New("can't read file saved on dry backend")
}
return be.be.Load(ctx, h, length, offset, fn)
}

// Stat returns information about a file in the backend.
func (be *Backend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
if err := h.Valid(); err != nil {
return restic.FileInfo{}, err
}

be.m.Lock()
defer be.m.Unlock()

if h.Type == restic.ConfigFile {
h.Name = ""
}

debug.Log("stat %v", h)

s, ok := be.data[h]
if !ok {
return be.be.Stat(ctx, h)
}

return restic.FileInfo{Size: int64(s), Name: h.Name}, nil
}

// Remove deletes a file from the backend.
func (be *Backend) Remove(ctx context.Context, h restic.Handle) error {
be.m.Lock()
defer be.m.Unlock()

debug.Log("Remove %v", h)

if _, ok := be.data[h]; !ok {
return errNotFound
}

delete(be.data, h)

return nil
}

// List returns a channel which yields entries from the backend.
func (be *Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
entries := []restic.FileInfo{}
be.m.Lock()
for entry, size := range be.data {
if entry.Type != t {
continue
}
entries = append(entries, restic.FileInfo{
Name: entry.Name,
Size: int64(size),
})
}
be.m.Unlock()

for _, entry := range entries {
if ctx.Err() != nil {
return ctx.Err()
}

err := fn(entry)
if err != nil {
return err
}

if ctx.Err() != nil {
return ctx.Err()
}
}

if ctx.Err() != nil {
return ctx.Err()
}

return be.be.List(ctx, t, fn)
}

// Location returns the location of the backend (RAM).
func (be *Backend) Location() string {
return "DRY:" + be.be.Location()
}

// Delete removes all data in the backend.
func (be *Backend) Delete(ctx context.Context) error {
return errors.New("dry-run doesn't support Delete()")
}

// Close closes the backend.
func (be *Backend) Close() error {
return be.be.Close()
}

0 comments on commit 566118e

Please sign in to comment.