restore: add --overwrite=if-changed to skip files if their mtime&size matches

--overwrite=always still checks the file content
This commit is contained in:
Michael Eischer 2024-05-31 17:34:48 +02:00
parent a66658b4c9
commit 5c3709e17a
2 changed files with 20 additions and 9 deletions

View File

@ -66,7 +66,7 @@ func init() {
initSingleSnapshotFilter(flags, &restoreOptions.SnapshotFilter) initSingleSnapshotFilter(flags, &restoreOptions.SnapshotFilter)
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-newer|never) (default: always)") flags.Var(&restoreOptions.Overwrite, "overwrite", "overwrite behavior, one of (always|if-changed|if-newer|never) (default: always)")
} }
func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,

View File

@ -42,10 +42,13 @@ type OverwriteBehavior int
// Constants for different overwrite behavior // Constants for different overwrite behavior
const ( const (
OverwriteAlways OverwriteBehavior = 0 OverwriteAlways OverwriteBehavior = iota
OverwriteIfNewer OverwriteBehavior = 1 // OverwriteIfChanged is like OverwriteAlways except that it skips restoring the content
OverwriteNever OverwriteBehavior = 2 // of files with matching size&mtime. Metatdata is always restored.
OverwriteInvalid OverwriteBehavior = 3 OverwriteIfChanged
OverwriteIfNewer
OverwriteNever
OverwriteInvalid
) )
// Set implements the method needed for pflag command flag parsing. // Set implements the method needed for pflag command flag parsing.
@ -53,6 +56,8 @@ func (c *OverwriteBehavior) Set(s string) error {
switch s { switch s {
case "always": case "always":
*c = OverwriteAlways *c = OverwriteAlways
case "if-changed":
*c = OverwriteIfChanged
case "if-newer": case "if-newer":
*c = OverwriteIfNewer *c = OverwriteIfNewer
case "never": case "never":
@ -69,6 +74,8 @@ func (c *OverwriteBehavior) String() string {
switch *c { switch *c {
case OverwriteAlways: case OverwriteAlways:
return "always" return "always"
case OverwriteIfChanged:
return "if-changed"
case OverwriteIfNewer: case OverwriteIfNewer:
return "if-newer" return "if-newer"
case OverwriteNever: case OverwriteNever:
@ -387,7 +394,7 @@ func (res *Restorer) withOverwriteCheck(node *restic.Node, target string, isHard
updateMetadataOnly := false updateMetadataOnly := false
if node.Type == "file" && !isHardlink { if node.Type == "file" && !isHardlink {
// if a file fails to verify, then matches is nil which results in restoring from scratch // if a file fails to verify, then matches is nil which results in restoring from scratch
matches, buf, _ = res.verifyFile(target, node, false, buf) matches, buf, _ = res.verifyFile(target, node, false, res.opts.Overwrite == OverwriteIfChanged, buf)
// skip files that are already correct completely // skip files that are already correct completely
updateMetadataOnly = !matches.NeedsRestore() updateMetadataOnly = !matches.NeedsRestore()
} }
@ -396,7 +403,7 @@ func (res *Restorer) withOverwriteCheck(node *restic.Node, target string, isHard
} }
func shouldOverwrite(overwrite OverwriteBehavior, node *restic.Node, destination string) (bool, error) { func shouldOverwrite(overwrite OverwriteBehavior, node *restic.Node, destination string) (bool, error) {
if overwrite == OverwriteAlways { if overwrite == OverwriteAlways || overwrite == OverwriteIfChanged {
return true, nil return true, nil
} }
@ -470,7 +477,7 @@ func (res *Restorer) VerifyFiles(ctx context.Context, dst string) (int, error) {
g.Go(func() (err error) { g.Go(func() (err error) {
var buf []byte var buf []byte
for job := range work { for job := range work {
_, buf, err = res.verifyFile(job.path, job.node, true, buf) _, buf, err = res.verifyFile(job.path, job.node, true, false, buf)
if err != nil { if err != nil {
err = res.Error(job.path, err) err = res.Error(job.path, err)
} }
@ -518,7 +525,7 @@ func (s *fileState) HasMatchingBlob(i int) bool {
// buf and the first return value are scratch space, passed around for reuse. // buf and the first return value are scratch space, passed around for reuse.
// Reusing buffers prevents the verifier goroutines allocating all of RAM and // Reusing buffers prevents the verifier goroutines allocating all of RAM and
// flushing the filesystem cache (at least on Linux). // flushing the filesystem cache (at least on Linux).
func (res *Restorer) verifyFile(target string, node *restic.Node, failFast bool, buf []byte) (*fileState, []byte, error) { func (res *Restorer) verifyFile(target string, node *restic.Node, failFast bool, trustMtime bool, buf []byte) (*fileState, []byte, error) {
f, err := os.OpenFile(target, fs.O_RDONLY|fs.O_NOFOLLOW, 0) f, err := os.OpenFile(target, fs.O_RDONLY|fs.O_NOFOLLOW, 0)
if err != nil { if err != nil {
return nil, buf, err return nil, buf, err
@ -542,6 +549,10 @@ func (res *Restorer) verifyFile(target string, node *restic.Node, failFast bool,
sizeMatches = false sizeMatches = false
} }
if trustMtime && fi.ModTime().Equal(node.ModTime) && sizeMatches {
return &fileState{nil, sizeMatches}, buf, nil
}
matches := make([]bool, len(node.Content)) matches := make([]bool, len(node.Content))
var offset int64 var offset int64
for i, blobID := range node.Content { for i, blobID := range node.Content {