forked from TrueCloudLab/restic
restore: New --verify flag to verify restored files content
Signed-off-by: Igor Fedorenko <igor@ifedorenko.com>
This commit is contained in:
parent
5fa6dc53cb
commit
e206680947
3 changed files with 64 additions and 0 deletions
6
changelog/unreleased/pull-1772
Normal file
6
changelog/unreleased/pull-1772
Normal file
|
@ -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
|
|
@ -34,6 +34,7 @@ type RestoreOptions struct {
|
||||||
Host string
|
Host string
|
||||||
Paths []string
|
Paths []string
|
||||||
Tags restic.TagLists
|
Tags restic.TagLists
|
||||||
|
Verify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var restoreOptions RestoreOptions
|
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.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.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.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 {
|
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)
|
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
||||||
|
|
||||||
err = res.RestoreTo(ctx, 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 {
|
if totalErrors > 0 {
|
||||||
Printf("There were %d errors\n", totalErrors)
|
Printf("There were %d errors\n", totalErrors)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ package restorer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/crypto"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/debug"
|
"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 {
|
func (res *Restorer) Snapshot() *restic.Snapshot {
|
||||||
return res.sn
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue