diff --git a/changelog/unreleased/pull-1772 b/changelog/unreleased/pull-1772 new file mode 100644 index 000000000..912092455 --- /dev/null +++ b/changelog/unreleased/pull-1772 @@ -0,0 +1,6 @@ +Enhancement: Add restore --verify to verify restored file content + +Restore will print error message if restored file content does not match +expected SHA256 checksum + +https://github.com/restic/restic/pull/1772 diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 846eb74b2..4bf59c06f 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -34,6 +34,7 @@ type RestoreOptions struct { Host string Paths []string Tags restic.TagLists + Verify bool } var restoreOptions RestoreOptions @@ -49,6 +50,7 @@ func init() { flags.StringVarP(&restoreOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`) flags.Var(&restoreOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"") flags.StringArrayVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"") + flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content") } func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { @@ -154,6 +156,12 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error { Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target) err = res.RestoreTo(ctx, opts.Target) + if err == nil && opts.Verify { + Verbosef("verifying files in %s\n", opts.Target) + var count int + count, err = res.VerifyFiles(ctx, opts.Target) + Verbosef("finished verifying %d files in %s\n", count, opts.Target) + } if totalErrors > 0 { Printf("There were %d errors\n", totalErrors) } diff --git a/internal/restorer/restorer.go b/internal/restorer/restorer.go index 7d48fa9ec..641b05877 100644 --- a/internal/restorer/restorer.go +++ b/internal/restorer/restorer.go @@ -2,8 +2,10 @@ package restorer import ( "context" + "os" "path/filepath" + "github.com/restic/restic/internal/crypto" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/debug" @@ -218,3 +220,51 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string) error { func (res *Restorer) Snapshot() *restic.Snapshot { return res.sn } + +// VerifyFiles reads all snapshot files and verifies their contents +func (res *Restorer) VerifyFiles(ctx context.Context, dst string) (int, error) { + // TODO multithreaded? + + count := 0 + err := res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{ + enterDir: func(node *restic.Node, target, location string) error { return nil }, + visitNode: func(node *restic.Node, target, location string) error { + if node.Type != "file" { + return nil + } + + count++ + stat, err := os.Stat(target) + if err != nil { + return err + } + if int64(node.Size) != stat.Size() { + return errors.Errorf("Invalid file size: expected %d got %d", node.Size, stat.Size()) + } + + offset := int64(0) + for _, blobID := range node.Content { + rd, err := os.Open(target) + if err != nil { + return err + } + blobs, _ := res.repo.Index().Lookup(blobID, restic.DataBlob) + length := blobs[0].Length - uint(crypto.Extension) + buf := make([]byte, length) // TODO do I want to reuse the buffer somehow? + _, err = rd.ReadAt(buf, offset) + if err != nil { + return err + } + if !blobID.Equal(restic.Hash(buf)) { + return errors.Errorf("Unexpected contents starting at offset %d", offset) + } + offset += int64(length) + } + + return nil + }, + leaveDir: func(node *restic.Node, target, location string) error { return nil }, + }) + + return count, err +}