2018-05-11 02:56:10 +00:00
|
|
|
package restorer
|
2014-09-23 20:39:12 +00:00
|
|
|
|
|
|
|
import (
|
2017-06-04 09:16:55 +00:00
|
|
|
"context"
|
2024-05-31 09:43:42 +00:00
|
|
|
"fmt"
|
2024-05-31 15:06:08 +00:00
|
|
|
"io"
|
2018-04-13 14:02:09 +00:00
|
|
|
"os"
|
2014-09-23 20:39:12 +00:00
|
|
|
"path/filepath"
|
2020-02-20 10:38:44 +00:00
|
|
|
"sync/atomic"
|
2016-08-21 15:48:36 +00:00
|
|
|
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/debug"
|
2020-02-20 10:38:44 +00:00
|
|
|
"github.com/restic/restic/internal/errors"
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/fs"
|
2018-05-11 02:56:10 +00:00
|
|
|
"github.com/restic/restic/internal/restic"
|
2022-10-28 15:44:34 +00:00
|
|
|
restoreui "github.com/restic/restic/internal/ui/restore"
|
2020-02-20 10:38:44 +00:00
|
|
|
|
|
|
|
"golang.org/x/sync/errgroup"
|
2014-09-23 20:39:12 +00:00
|
|
|
)
|
|
|
|
|
2015-05-02 13:23:28 +00:00
|
|
|
// Restorer is used to restore a snapshot to a directory.
|
2014-09-23 20:39:12 +00:00
|
|
|
type Restorer struct {
|
2024-05-31 15:46:59 +00:00
|
|
|
repo restic.Repository
|
|
|
|
sn *restic.Snapshot
|
|
|
|
opts Options
|
2024-05-31 09:43:42 +00:00
|
|
|
|
2024-05-31 15:06:08 +00:00
|
|
|
fileList map[string]bool
|
2022-10-28 15:44:34 +00:00
|
|
|
|
2024-06-29 17:02:57 +00:00
|
|
|
Error func(location string, err error) error
|
|
|
|
Warn func(message string)
|
|
|
|
// SelectFilter determines whether the item is selectedForRestore or whether a childMayBeSelected.
|
|
|
|
// selectedForRestore must not depend on isDir as `removeUnexpectedFiles` always passes false to isDir.
|
2024-06-29 17:23:09 +00:00
|
|
|
SelectFilter func(item string, isDir bool) (selectedForRestore bool, childMayBeSelected bool)
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2024-02-10 21:58:10 +00:00
|
|
|
var restorerAbortOnAllErrors = func(_ string, err error) error { return err }
|
2015-04-30 00:59:06 +00:00
|
|
|
|
2024-05-31 09:42:25 +00:00
|
|
|
type Options struct {
|
2024-05-31 18:57:28 +00:00
|
|
|
DryRun bool
|
2024-05-31 09:43:42 +00:00
|
|
|
Sparse bool
|
|
|
|
Progress *restoreui.Progress
|
|
|
|
Overwrite OverwriteBehavior
|
2024-06-29 16:58:17 +00:00
|
|
|
Delete bool
|
2024-05-31 09:43:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type OverwriteBehavior int
|
|
|
|
|
|
|
|
// Constants for different overwrite behavior
|
|
|
|
const (
|
2024-05-31 15:34:48 +00:00
|
|
|
OverwriteAlways OverwriteBehavior = iota
|
|
|
|
// OverwriteIfChanged is like OverwriteAlways except that it skips restoring the content
|
2024-07-01 22:45:59 +00:00
|
|
|
// of files with matching size&mtime. Metadata is always restored.
|
2024-05-31 15:34:48 +00:00
|
|
|
OverwriteIfChanged
|
|
|
|
OverwriteIfNewer
|
|
|
|
OverwriteNever
|
|
|
|
OverwriteInvalid
|
2024-05-31 09:43:42 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Set implements the method needed for pflag command flag parsing.
|
|
|
|
func (c *OverwriteBehavior) Set(s string) error {
|
|
|
|
switch s {
|
|
|
|
case "always":
|
|
|
|
*c = OverwriteAlways
|
2024-05-31 15:34:48 +00:00
|
|
|
case "if-changed":
|
|
|
|
*c = OverwriteIfChanged
|
2024-05-31 09:43:42 +00:00
|
|
|
case "if-newer":
|
|
|
|
*c = OverwriteIfNewer
|
|
|
|
case "never":
|
|
|
|
*c = OverwriteNever
|
|
|
|
default:
|
|
|
|
*c = OverwriteInvalid
|
|
|
|
return fmt.Errorf("invalid overwrite behavior %q, must be one of (always|if-newer|never)", s)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *OverwriteBehavior) String() string {
|
|
|
|
switch *c {
|
|
|
|
case OverwriteAlways:
|
|
|
|
return "always"
|
2024-05-31 15:34:48 +00:00
|
|
|
case OverwriteIfChanged:
|
|
|
|
return "if-changed"
|
2024-05-31 09:43:42 +00:00
|
|
|
case OverwriteIfNewer:
|
|
|
|
return "if-newer"
|
|
|
|
case OverwriteNever:
|
|
|
|
return "never"
|
|
|
|
default:
|
|
|
|
return "invalid"
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
func (c *OverwriteBehavior) Type() string {
|
|
|
|
return "behavior"
|
2024-05-31 09:42:25 +00:00
|
|
|
}
|
|
|
|
|
2015-04-30 00:59:06 +00:00
|
|
|
// NewRestorer creates a restorer preloaded with the content from the snapshot id.
|
2024-05-31 09:42:25 +00:00
|
|
|
func NewRestorer(repo restic.Repository, sn *restic.Snapshot, opts Options) *Restorer {
|
2015-07-08 20:35:41 +00:00
|
|
|
r := &Restorer{
|
2018-04-08 12:02:30 +00:00
|
|
|
repo: repo,
|
2024-05-31 15:46:59 +00:00
|
|
|
opts: opts,
|
2024-05-31 15:06:08 +00:00
|
|
|
fileList: make(map[string]bool),
|
2018-04-08 12:02:30 +00:00
|
|
|
Error: restorerAbortOnAllErrors,
|
2024-06-29 17:23:09 +00:00
|
|
|
SelectFilter: func(string, bool) (bool, bool) { return true, true },
|
2022-10-03 12:48:14 +00:00
|
|
|
sn: sn,
|
2015-07-08 20:35:41 +00:00
|
|
|
}
|
2014-09-23 20:39:12 +00:00
|
|
|
|
2022-10-03 12:48:14 +00:00
|
|
|
return r
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2018-04-08 02:43:14 +00:00
|
|
|
type treeVisitor struct {
|
|
|
|
enterDir func(node *restic.Node, target, location string) error
|
|
|
|
visitNode func(node *restic.Node, target, location string) error
|
2024-06-29 16:58:17 +00:00
|
|
|
// 'entries' contains all files the snapshot contains for this node. This also includes files
|
|
|
|
// ignored by the SelectFilter.
|
|
|
|
leaveDir func(node *restic.Node, target, location string, entries []string) error
|
2018-04-08 02:43:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// traverseTree traverses a tree from the repo and calls treeVisitor.
|
|
|
|
// target is the path in the file system, location within the snapshot.
|
2024-06-29 16:58:17 +00:00
|
|
|
func (res *Restorer) traverseTree(ctx context.Context, target string, treeID restic.ID, visitor treeVisitor) error {
|
|
|
|
location := string(filepath.Separator)
|
|
|
|
sanitizeError := func(err error) error {
|
|
|
|
switch err {
|
|
|
|
case nil, context.Canceled, context.DeadlineExceeded:
|
|
|
|
// Context errors are permanent.
|
|
|
|
return err
|
|
|
|
default:
|
|
|
|
return res.Error(location, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if visitor.enterDir != nil {
|
|
|
|
err := sanitizeError(visitor.enterDir(nil, target, location))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
childFilenames, hasRestored, err := res.traverseTreeInner(ctx, target, location, treeID, visitor)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if hasRestored && visitor.leaveDir != nil {
|
|
|
|
err = sanitizeError(visitor.leaveDir(nil, target, location, childFilenames))
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (res *Restorer) traverseTreeInner(ctx context.Context, target, location string, treeID restic.ID, visitor treeVisitor) (filenames []string, hasRestored bool, err error) {
|
2018-01-25 19:49:41 +00:00
|
|
|
debug.Log("%v %v %v", target, location, treeID)
|
2022-06-12 12:38:19 +00:00
|
|
|
tree, err := restic.LoadTree(ctx, res.repo, treeID)
|
2014-09-23 20:39:12 +00:00
|
|
|
if err != nil {
|
2018-01-25 19:49:41 +00:00
|
|
|
debug.Log("error loading tree %v: %v", treeID, err)
|
2024-06-29 16:58:17 +00:00
|
|
|
return nil, hasRestored, res.Error(location, err)
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2024-06-29 16:58:17 +00:00
|
|
|
if res.opts.Delete {
|
|
|
|
filenames = make([]string, 0, len(tree.Nodes))
|
|
|
|
}
|
2024-06-29 15:24:47 +00:00
|
|
|
for i, node := range tree.Nodes {
|
|
|
|
// allow GC of tree node
|
|
|
|
tree.Nodes[i] = nil
|
2024-06-29 16:58:17 +00:00
|
|
|
if res.opts.Delete {
|
|
|
|
// just track all files included in the tree node to simplify the control flow.
|
|
|
|
// tracking too many files does not matter except for a slightly elevated memory usage
|
|
|
|
filenames = append(filenames, node.Name)
|
|
|
|
}
|
2017-11-26 14:17:12 +00:00
|
|
|
|
|
|
|
// ensure that the node name does not contain anything that refers to a
|
|
|
|
// top-level directory.
|
|
|
|
nodeName := filepath.Base(filepath.Join(string(filepath.Separator), node.Name))
|
|
|
|
if nodeName != node.Name {
|
|
|
|
debug.Log("node %q has invalid name %q", node.Name, nodeName)
|
2018-09-15 00:55:30 +00:00
|
|
|
err := res.Error(location, errors.Errorf("invalid child node name %s", node.Name))
|
2017-11-26 14:17:12 +00:00
|
|
|
if err != nil {
|
2024-06-29 16:58:17 +00:00
|
|
|
return nil, hasRestored, err
|
2017-11-26 14:17:12 +00:00
|
|
|
}
|
2024-06-29 16:58:17 +00:00
|
|
|
// force disable deletion to prevent unexpected behavior
|
|
|
|
res.opts.Delete = false
|
2017-11-26 14:17:12 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
nodeTarget := filepath.Join(target, nodeName)
|
|
|
|
nodeLocation := filepath.Join(location, nodeName)
|
|
|
|
|
|
|
|
if target == nodeTarget || !fs.HasPathPrefix(target, nodeTarget) {
|
2017-11-26 17:36:48 +00:00
|
|
|
debug.Log("target: %v %v", target, nodeTarget)
|
2017-11-26 14:17:12 +00:00
|
|
|
debug.Log("node %q has invalid target path %q", node.Name, nodeTarget)
|
2018-09-15 00:55:30 +00:00
|
|
|
err := res.Error(nodeLocation, errors.New("node has invalid path"))
|
2017-11-26 14:17:12 +00:00
|
|
|
if err != nil {
|
2024-06-29 16:58:17 +00:00
|
|
|
return nil, hasRestored, err
|
2017-11-26 14:17:12 +00:00
|
|
|
}
|
2024-06-29 16:58:17 +00:00
|
|
|
// force disable deletion to prevent unexpected behavior
|
|
|
|
res.opts.Delete = false
|
2017-11-26 14:17:12 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-04-20 11:53:11 +00:00
|
|
|
// sockets cannot be restored
|
|
|
|
if node.Type == "socket" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-06-29 17:23:09 +00:00
|
|
|
selectedForRestore, childMayBeSelected := res.SelectFilter(nodeLocation, node.Type == "dir")
|
2020-08-29 21:27:20 +00:00
|
|
|
debug.Log("SelectFilter returned %v %v for %q", selectedForRestore, childMayBeSelected, nodeLocation)
|
2015-07-08 18:29:27 +00:00
|
|
|
|
2020-08-29 22:08:38 +00:00
|
|
|
if selectedForRestore {
|
|
|
|
hasRestored = true
|
|
|
|
}
|
|
|
|
|
2018-04-08 02:43:14 +00:00
|
|
|
sanitizeError := func(err error) error {
|
2020-03-16 09:05:59 +00:00
|
|
|
switch err {
|
|
|
|
case nil, context.Canceled, context.DeadlineExceeded:
|
|
|
|
// Context errors are permanent.
|
|
|
|
return err
|
|
|
|
default:
|
|
|
|
return res.Error(nodeLocation, err)
|
2018-04-08 02:43:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if node.Type == "dir" {
|
2014-09-23 20:39:12 +00:00
|
|
|
if node.Subtree == nil {
|
2024-06-29 16:58:17 +00:00
|
|
|
return nil, hasRestored, errors.Errorf("Dir without subtree in tree %v", treeID.Str())
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2020-03-16 14:20:00 +00:00
|
|
|
if selectedForRestore && visitor.enterDir != nil {
|
2018-04-08 02:43:14 +00:00
|
|
|
err = sanitizeError(visitor.enterDir(node, nodeTarget, nodeLocation))
|
|
|
|
if err != nil {
|
2024-06-29 16:58:17 +00:00
|
|
|
return nil, hasRestored, err
|
2018-04-08 02:43:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-29 22:08:38 +00:00
|
|
|
// keep track of restored child status
|
|
|
|
// so metadata of the current directory are restored on leaveDir
|
|
|
|
childHasRestored := false
|
2024-06-29 16:58:17 +00:00
|
|
|
var childFilenames []string
|
2020-08-29 22:08:38 +00:00
|
|
|
|
2018-04-08 02:43:14 +00:00
|
|
|
if childMayBeSelected {
|
2024-06-29 16:58:17 +00:00
|
|
|
childFilenames, childHasRestored, err = res.traverseTreeInner(ctx, nodeTarget, nodeLocation, *node.Subtree, visitor)
|
2020-08-29 22:08:38 +00:00
|
|
|
err = sanitizeError(err)
|
2014-09-23 20:39:12 +00:00
|
|
|
if err != nil {
|
2024-06-29 16:58:17 +00:00
|
|
|
return nil, hasRestored, err
|
2020-08-29 22:08:38 +00:00
|
|
|
}
|
|
|
|
// inform the parent directory to restore parent metadata on leaveDir if needed
|
|
|
|
if childHasRestored {
|
|
|
|
hasRestored = true
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-29 22:08:38 +00:00
|
|
|
// metadata need to be restore when leaving the directory in both cases
|
|
|
|
// selected for restore or any child of any subtree have been restored
|
2020-03-16 14:20:00 +00:00
|
|
|
if (selectedForRestore || childHasRestored) && visitor.leaveDir != nil {
|
2024-06-29 16:58:17 +00:00
|
|
|
err = sanitizeError(visitor.leaveDir(node, nodeTarget, nodeLocation, childFilenames))
|
2018-04-20 11:53:11 +00:00
|
|
|
if err != nil {
|
2024-06-29 16:58:17 +00:00
|
|
|
return nil, hasRestored, err
|
2018-04-20 11:53:11 +00:00
|
|
|
}
|
2018-01-07 14:13:24 +00:00
|
|
|
}
|
restorer: Fix traverseTree
traverseTree() was meant to call enterDir() whenever a directory is
selected for restore, either explicitly or implicitly (=contains a file
which is to be restored). After restoring a file, leaveDir() is called
in reverse order for all intermediate directories so that the metadata
can be restored.
When a directory is selected implicitly, the metadata for it is
restored. This is different from the previous restorer behavior, which
created implicitly selected intermediate directories with permissions
0700 (only user can read/write it).
This commit changes the behavior back to the old one. Only a directory
is explicitly selected for restore, enterDir()/leaveDir() are called for
it. Otherwise, only visitNode() is called, so visitNode() needs to make
sure the parent directory exists. If the directory is explicitly
included, leaveDir() will then restore the metadata correctly.
When we decide to change the behavior (restore metadata for all
intermediate directories, even if selected implicitly), we should do
that in the selection functions, not here.
This finally resolves #1870
2018-07-21 20:39:12 +00:00
|
|
|
|
|
|
|
continue
|
2018-04-08 02:43:14 +00:00
|
|
|
}
|
2018-01-07 14:13:24 +00:00
|
|
|
|
restorer: Fix traverseTree
traverseTree() was meant to call enterDir() whenever a directory is
selected for restore, either explicitly or implicitly (=contains a file
which is to be restored). After restoring a file, leaveDir() is called
in reverse order for all intermediate directories so that the metadata
can be restored.
When a directory is selected implicitly, the metadata for it is
restored. This is different from the previous restorer behavior, which
created implicitly selected intermediate directories with permissions
0700 (only user can read/write it).
This commit changes the behavior back to the old one. Only a directory
is explicitly selected for restore, enterDir()/leaveDir() are called for
it. Otherwise, only visitNode() is called, so visitNode() needs to make
sure the parent directory exists. If the directory is explicitly
included, leaveDir() will then restore the metadata correctly.
When we decide to change the behavior (restore metadata for all
intermediate directories, even if selected implicitly), we should do
that in the selection functions, not here.
This finally resolves #1870
2018-07-21 20:39:12 +00:00
|
|
|
if selectedForRestore {
|
|
|
|
err = sanitizeError(visitor.visitNode(node, nodeTarget, nodeLocation))
|
2018-01-07 14:13:24 +00:00
|
|
|
if err != nil {
|
2024-06-29 16:58:17 +00:00
|
|
|
return nil, hasRestored, err
|
2015-05-14 13:58:26 +00:00
|
|
|
}
|
2015-05-14 03:11:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-29 16:58:17 +00:00
|
|
|
return filenames, hasRestored, nil
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2018-05-11 04:45:14 +00:00
|
|
|
func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, target, location string) error {
|
2024-05-31 18:57:28 +00:00
|
|
|
if !res.opts.DryRun {
|
|
|
|
debug.Log("restoreNode %v %v %v", node.Name, target, location)
|
|
|
|
if err := fs.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
|
|
return errors.Wrap(err, "RemoveNode")
|
|
|
|
}
|
2015-04-30 00:59:06 +00:00
|
|
|
|
2024-05-31 18:57:28 +00:00
|
|
|
err := node.CreateAt(ctx, target, res.repo)
|
|
|
|
if err != nil {
|
|
|
|
debug.Log("node.CreateAt(%s) error %v", target, err)
|
|
|
|
return err
|
|
|
|
}
|
2015-07-06 21:59:28 +00:00
|
|
|
}
|
2023-05-01 10:05:48 +00:00
|
|
|
|
2024-06-29 19:24:45 +00:00
|
|
|
res.opts.Progress.AddProgress(location, restoreui.ActionFileRestored, 0, 0)
|
2023-05-01 10:05:48 +00:00
|
|
|
return res.restoreNodeMetadataTo(node, target, location)
|
2018-04-08 02:43:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location string) error {
|
2024-05-31 18:57:28 +00:00
|
|
|
if res.opts.DryRun {
|
|
|
|
return nil
|
|
|
|
}
|
2018-04-08 02:43:14 +00:00
|
|
|
debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location)
|
2024-02-23 00:52:26 +00:00
|
|
|
err := node.RestoreMetadata(target, res.Warn)
|
2015-04-30 00:59:06 +00:00
|
|
|
if err != nil {
|
2018-04-08 02:43:14 +00:00
|
|
|
debug.Log("node.RestoreMetadata(%s) error %v", target, err)
|
2015-04-30 00:59:06 +00:00
|
|
|
}
|
2018-04-08 02:43:14 +00:00
|
|
|
return err
|
2015-04-30 00:59:06 +00:00
|
|
|
}
|
|
|
|
|
2018-05-11 04:45:14 +00:00
|
|
|
func (res *Restorer) restoreHardlinkAt(node *restic.Node, target, path, location string) error {
|
2024-05-31 18:57:28 +00:00
|
|
|
if !res.opts.DryRun {
|
|
|
|
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 {
|
|
|
|
return errors.WithStack(err)
|
|
|
|
}
|
2018-05-11 04:45:14 +00:00
|
|
|
}
|
2022-10-28 15:44:34 +00:00
|
|
|
|
2024-06-29 19:24:45 +00:00
|
|
|
res.opts.Progress.AddProgress(location, restoreui.ActionFileRestored, 0, 0)
|
2018-09-27 14:35:34 +00:00
|
|
|
// TODO investigate if hardlinks have separate metadata on any supported system
|
|
|
|
return res.restoreNodeMetadataTo(node, path, location)
|
2018-05-11 04:45:14 +00:00
|
|
|
}
|
|
|
|
|
2024-06-07 21:02:46 +00:00
|
|
|
func (res *Restorer) ensureDir(target string) error {
|
2024-05-31 18:57:28 +00:00
|
|
|
if res.opts.DryRun {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-06-07 21:02:46 +00:00
|
|
|
fi, err := fs.Lstat(target)
|
|
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
|
|
return fmt.Errorf("failed to check for directory: %w", err)
|
|
|
|
}
|
|
|
|
if err == nil && !fi.IsDir() {
|
|
|
|
// try to cleanup unexpected file
|
|
|
|
if err := fs.Remove(target); err != nil {
|
|
|
|
return fmt.Errorf("failed to remove stale item: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// create parent dir with default permissions
|
|
|
|
// second pass #leaveDir restores dir metadata after visiting/restoring all children
|
|
|
|
return fs.MkdirAll(target, 0700)
|
|
|
|
}
|
|
|
|
|
2017-03-02 13:52:18 +00:00
|
|
|
// RestoreTo creates the directories and files in the snapshot below dst.
|
2014-09-23 20:39:12 +00:00
|
|
|
// Before an item is created, res.Filter is called.
|
2018-05-11 04:45:14 +00:00
|
|
|
func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
2017-11-26 17:36:48 +00:00
|
|
|
var err error
|
|
|
|
if !filepath.IsAbs(dst) {
|
|
|
|
dst, err = filepath.Abs(dst)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "Abs")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-01 21:24:42 +00:00
|
|
|
idx := NewHardlinkIndex[string]()
|
2024-05-19 10:41:56 +00:00
|
|
|
filerestorer := newFileRestorer(dst, res.repo.LoadBlobsFromPack, res.repo.LookupBlob,
|
2024-05-31 15:46:59 +00:00
|
|
|
res.repo.Connections(), res.opts.Sparse, res.opts.Progress)
|
2021-01-04 18:20:04 +00:00
|
|
|
filerestorer.Error = res.Error
|
2018-04-08 12:02:30 +00:00
|
|
|
|
2020-08-29 21:27:20 +00:00
|
|
|
debug.Log("first pass for %q", dst)
|
|
|
|
|
2024-05-31 15:06:08 +00:00
|
|
|
var buf []byte
|
|
|
|
|
2018-04-08 12:02:30 +00:00
|
|
|
// first tree pass: create directories and collect all files to restore
|
2024-06-29 16:58:17 +00:00
|
|
|
err = res.traverseTree(ctx, dst, *res.sn.Tree, treeVisitor{
|
2024-02-10 21:58:10 +00:00
|
|
|
enterDir: func(_ *restic.Node, target, location string) error {
|
2020-08-29 21:27:20 +00:00
|
|
|
debug.Log("first pass, enterDir: mkdir %q, leaveDir should restore metadata", location)
|
2024-06-29 16:58:17 +00:00
|
|
|
if location != "/" {
|
|
|
|
res.opts.Progress.AddFile(0)
|
|
|
|
}
|
2024-06-07 21:02:46 +00:00
|
|
|
return res.ensureDir(target)
|
2018-04-08 02:43:14 +00:00
|
|
|
},
|
2018-04-08 12:02:30 +00:00
|
|
|
|
2018-04-08 02:43:14 +00:00
|
|
|
visitNode: func(node *restic.Node, target, location string) error {
|
2020-08-29 21:27:20 +00:00
|
|
|
debug.Log("first pass, visitNode: mkdir %q, leaveDir on second pass should restore metadata", location)
|
2024-06-07 21:02:46 +00:00
|
|
|
if err := res.ensureDir(filepath.Dir(target)); err != nil {
|
restorer: Fix traverseTree
traverseTree() was meant to call enterDir() whenever a directory is
selected for restore, either explicitly or implicitly (=contains a file
which is to be restored). After restoring a file, leaveDir() is called
in reverse order for all intermediate directories so that the metadata
can be restored.
When a directory is selected implicitly, the metadata for it is
restored. This is different from the previous restorer behavior, which
created implicitly selected intermediate directories with permissions
0700 (only user can read/write it).
This commit changes the behavior back to the old one. Only a directory
is explicitly selected for restore, enterDir()/leaveDir() are called for
it. Otherwise, only visitNode() is called, so visitNode() needs to make
sure the parent directory exists. If the directory is explicitly
included, leaveDir() will then restore the metadata correctly.
When we decide to change the behavior (restore metadata for all
intermediate directories, even if selected implicitly), we should do
that in the selection functions, not here.
This finally resolves #1870
2018-07-21 20:39:12 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-04-08 12:02:30 +00:00
|
|
|
if node.Type != "file" {
|
2024-05-31 15:46:59 +00:00
|
|
|
res.opts.Progress.AddFile(0)
|
2018-04-08 12:02:30 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if node.Links > 1 {
|
|
|
|
if idx.Has(node.Inode, node.DeviceID) {
|
2024-05-31 09:07:53 +00:00
|
|
|
// a hardlinked file does not increase the restore size
|
2024-05-31 15:46:59 +00:00
|
|
|
res.opts.Progress.AddFile(0)
|
2018-04-08 12:02:30 +00:00
|
|
|
return nil
|
|
|
|
}
|
2018-09-15 00:18:37 +00:00
|
|
|
idx.Add(node.Inode, node.DeviceID, location)
|
2018-04-08 12:02:30 +00:00
|
|
|
}
|
|
|
|
|
2024-05-31 18:38:51 +00:00
|
|
|
buf, err = res.withOverwriteCheck(node, target, location, false, buf, func(updateMetadataOnly bool, matches *fileState) error {
|
2024-05-31 15:06:08 +00:00
|
|
|
if updateMetadataOnly {
|
2024-05-31 18:38:51 +00:00
|
|
|
res.opts.Progress.AddSkippedFile(location, node.Size)
|
2024-05-31 15:06:08 +00:00
|
|
|
} else {
|
|
|
|
res.opts.Progress.AddFile(node.Size)
|
2024-05-31 18:57:28 +00:00
|
|
|
if !res.opts.DryRun {
|
|
|
|
filerestorer.addFile(location, node.Content, int64(node.Size), matches)
|
|
|
|
} else {
|
2024-06-29 19:24:45 +00:00
|
|
|
action := restoreui.ActionFileUpdated
|
|
|
|
if matches == nil {
|
|
|
|
action = restoreui.ActionFileRestored
|
|
|
|
}
|
2024-05-31 18:57:28 +00:00
|
|
|
// immediately mark as completed
|
2024-06-29 19:24:45 +00:00
|
|
|
res.opts.Progress.AddProgress(location, action, node.Size, node.Size)
|
2024-05-31 18:57:28 +00:00
|
|
|
}
|
2024-05-31 15:06:08 +00:00
|
|
|
}
|
|
|
|
res.trackFile(location, updateMetadataOnly)
|
2024-05-31 09:43:42 +00:00
|
|
|
return nil
|
|
|
|
})
|
2024-05-31 15:06:08 +00:00
|
|
|
return err
|
2018-04-08 02:43:14 +00:00
|
|
|
},
|
2018-04-08 12:02:30 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-05-31 18:57:28 +00:00
|
|
|
if !res.opts.DryRun {
|
|
|
|
err = filerestorer.restoreFiles(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-04-08 12:02:30 +00:00
|
|
|
}
|
|
|
|
|
2020-08-29 21:27:20 +00:00
|
|
|
debug.Log("second pass for %q", dst)
|
|
|
|
|
2018-04-08 12:02:30 +00:00
|
|
|
// second tree pass: restore special files and filesystem metadata
|
2024-06-29 16:58:17 +00:00
|
|
|
err = res.traverseTree(ctx, dst, *res.sn.Tree, treeVisitor{
|
2018-04-08 12:02:30 +00:00
|
|
|
visitNode: func(node *restic.Node, target, location string) error {
|
2020-08-29 21:27:20 +00:00
|
|
|
debug.Log("second pass, visitNode: restore node %q", location)
|
2018-05-11 04:45:14 +00:00
|
|
|
if node.Type != "file" {
|
2024-05-31 18:38:51 +00:00
|
|
|
_, err := res.withOverwriteCheck(node, target, location, false, nil, func(_ bool, _ *fileState) error {
|
2024-05-31 09:43:42 +00:00
|
|
|
return res.restoreNodeTo(ctx, node, target, location)
|
|
|
|
})
|
2024-05-31 15:06:08 +00:00
|
|
|
return err
|
2018-04-08 12:02:30 +00:00
|
|
|
}
|
|
|
|
|
2023-10-01 21:24:42 +00:00
|
|
|
if idx.Has(node.Inode, node.DeviceID) && idx.Value(node.Inode, node.DeviceID) != location {
|
2024-05-31 18:38:51 +00:00
|
|
|
_, err := res.withOverwriteCheck(node, target, location, true, nil, func(_ bool, _ *fileState) error {
|
2024-05-31 09:43:42 +00:00
|
|
|
return res.restoreHardlinkAt(node, filerestorer.targetPath(idx.Value(node.Inode, node.DeviceID)), target, location)
|
|
|
|
})
|
2024-05-31 15:06:08 +00:00
|
|
|
return err
|
2018-09-27 12:59:33 +00:00
|
|
|
}
|
|
|
|
|
2024-05-31 15:06:08 +00:00
|
|
|
if _, ok := res.hasRestoredFile(location); ok {
|
2024-05-31 09:43:42 +00:00
|
|
|
return res.restoreNodeMetadataTo(node, target, location)
|
|
|
|
}
|
|
|
|
// don't touch skipped files
|
|
|
|
return nil
|
2018-04-08 02:43:14 +00:00
|
|
|
},
|
2024-06-29 16:58:17 +00:00
|
|
|
leaveDir: func(node *restic.Node, target, location string, expectedFilenames []string) error {
|
2024-06-29 17:02:57 +00:00
|
|
|
if res.opts.Delete {
|
|
|
|
if err := res.removeUnexpectedFiles(target, location, expectedFilenames); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-29 16:58:17 +00:00
|
|
|
if node == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-05-01 10:05:48 +00:00
|
|
|
err := res.restoreNodeMetadataTo(node, target, location)
|
2024-05-31 09:07:53 +00:00
|
|
|
if err == nil {
|
2024-06-29 19:24:45 +00:00
|
|
|
res.opts.Progress.AddProgress(location, restoreui.ActionDirRestored, 0, 0)
|
2023-05-01 10:05:48 +00:00
|
|
|
}
|
|
|
|
return err
|
|
|
|
},
|
2018-04-08 02:43:14 +00:00
|
|
|
})
|
2020-08-29 22:08:38 +00:00
|
|
|
return err
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2024-06-29 17:02:57 +00:00
|
|
|
func (res *Restorer) removeUnexpectedFiles(target, location string, expectedFilenames []string) error {
|
|
|
|
if !res.opts.Delete {
|
|
|
|
panic("internal error")
|
|
|
|
}
|
|
|
|
|
|
|
|
entries, err := fs.Readdirnames(fs.Local{}, target, fs.O_NOFOLLOW)
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
return nil
|
|
|
|
} else if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
keep := map[string]struct{}{}
|
|
|
|
for _, name := range expectedFilenames {
|
|
|
|
keep[name] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, entry := range entries {
|
|
|
|
if _, ok := keep[entry]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
nodeTarget := filepath.Join(target, entry)
|
|
|
|
nodeLocation := filepath.Join(location, entry)
|
|
|
|
|
|
|
|
if target == nodeTarget || !fs.HasPathPrefix(target, nodeTarget) {
|
|
|
|
return fmt.Errorf("skipping deletion due to invalid filename: %v", entry)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO pass a proper value to the isDir parameter once this becomes relevant for the filters
|
2024-06-29 17:23:09 +00:00
|
|
|
selectedForRestore, _ := res.SelectFilter(nodeLocation, false)
|
2024-06-29 17:02:57 +00:00
|
|
|
// only delete files that were selected for restore
|
|
|
|
if selectedForRestore {
|
|
|
|
if !res.opts.DryRun {
|
|
|
|
if err := fs.RemoveAll(nodeTarget); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-05-31 15:06:08 +00:00
|
|
|
func (res *Restorer) trackFile(location string, metadataOnly bool) {
|
|
|
|
res.fileList[location] = metadataOnly
|
2024-05-31 09:43:42 +00:00
|
|
|
}
|
|
|
|
|
2024-05-31 15:06:08 +00:00
|
|
|
func (res *Restorer) hasRestoredFile(location string) (metadataOnly bool, ok bool) {
|
|
|
|
metadataOnly, ok = res.fileList[location]
|
|
|
|
return metadataOnly, ok
|
2024-05-31 09:43:42 +00:00
|
|
|
}
|
|
|
|
|
2024-05-31 18:38:51 +00:00
|
|
|
func (res *Restorer) withOverwriteCheck(node *restic.Node, target, location string, isHardlink bool, buf []byte, cb func(updateMetadataOnly bool, matches *fileState) error) ([]byte, error) {
|
2024-05-31 15:46:59 +00:00
|
|
|
overwrite, err := shouldOverwrite(res.opts.Overwrite, node, target)
|
2024-05-31 09:43:42 +00:00
|
|
|
if err != nil {
|
2024-05-31 15:06:08 +00:00
|
|
|
return buf, err
|
2024-05-31 09:43:42 +00:00
|
|
|
} else if !overwrite {
|
|
|
|
size := node.Size
|
|
|
|
if isHardlink {
|
|
|
|
size = 0
|
|
|
|
}
|
2024-05-31 18:38:51 +00:00
|
|
|
res.opts.Progress.AddSkippedFile(location, size)
|
2024-05-31 15:06:08 +00:00
|
|
|
return buf, nil
|
2024-05-31 09:43:42 +00:00
|
|
|
}
|
2024-05-31 15:06:08 +00:00
|
|
|
|
|
|
|
var matches *fileState
|
|
|
|
updateMetadataOnly := false
|
|
|
|
if node.Type == "file" && !isHardlink {
|
|
|
|
// if a file fails to verify, then matches is nil which results in restoring from scratch
|
2024-05-31 15:34:48 +00:00
|
|
|
matches, buf, _ = res.verifyFile(target, node, false, res.opts.Overwrite == OverwriteIfChanged, buf)
|
2024-05-31 15:06:08 +00:00
|
|
|
// skip files that are already correct completely
|
|
|
|
updateMetadataOnly = !matches.NeedsRestore()
|
|
|
|
}
|
|
|
|
|
|
|
|
return buf, cb(updateMetadataOnly, matches)
|
2024-05-31 09:43:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func shouldOverwrite(overwrite OverwriteBehavior, node *restic.Node, destination string) (bool, error) {
|
2024-05-31 15:34:48 +00:00
|
|
|
if overwrite == OverwriteAlways || overwrite == OverwriteIfChanged {
|
2024-05-31 09:43:42 +00:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
fi, err := fs.Lstat(destination)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if overwrite == OverwriteIfNewer {
|
|
|
|
// return if node is newer
|
|
|
|
return node.ModTime.After(fi.ModTime()), nil
|
|
|
|
} else if overwrite == OverwriteNever {
|
|
|
|
// file exists
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
panic("unknown overwrite behavior")
|
|
|
|
}
|
|
|
|
|
2015-05-02 13:23:28 +00:00
|
|
|
// Snapshot returns the snapshot this restorer is configured to use.
|
2018-05-11 02:56:10 +00:00
|
|
|
func (res *Restorer) Snapshot() *restic.Snapshot {
|
2014-09-23 20:39:12 +00:00
|
|
|
return res.sn
|
|
|
|
}
|
2018-04-13 14:02:09 +00:00
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
// Number of workers in VerifyFiles.
|
|
|
|
const nVerifyWorkers = 8
|
|
|
|
|
2020-02-20 10:56:33 +00:00
|
|
|
// VerifyFiles checks whether all regular files in the snapshot res.sn
|
|
|
|
// have been successfully written to dst. It stops when it encounters an
|
2020-02-21 08:51:43 +00:00
|
|
|
// error. It returns that error and the number of files it has successfully
|
|
|
|
// verified.
|
2018-04-13 14:02:09 +00:00
|
|
|
func (res *Restorer) VerifyFiles(ctx context.Context, dst string) (int, error) {
|
2020-02-20 10:38:44 +00:00
|
|
|
type mustCheck struct {
|
|
|
|
node *restic.Node
|
|
|
|
path string
|
|
|
|
}
|
|
|
|
|
2020-02-20 21:43:56 +00:00
|
|
|
var (
|
2020-02-20 10:38:44 +00:00
|
|
|
nchecked uint64
|
|
|
|
work = make(chan mustCheck, 2*nVerifyWorkers)
|
2020-02-20 21:43:56 +00:00
|
|
|
)
|
2018-04-13 14:02:09 +00:00
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
g, ctx := errgroup.WithContext(ctx)
|
2018-04-13 14:02:09 +00:00
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
// Traverse tree and send jobs to work.
|
|
|
|
g.Go(func() error {
|
|
|
|
defer close(work)
|
2018-04-13 14:02:09 +00:00
|
|
|
|
2024-06-29 16:58:17 +00:00
|
|
|
err := res.traverseTree(ctx, dst, *res.sn.Tree, treeVisitor{
|
2024-05-31 09:43:42 +00:00
|
|
|
visitNode: func(node *restic.Node, target, location string) error {
|
2024-05-31 15:06:08 +00:00
|
|
|
if node.Type != "file" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if metadataOnly, ok := res.hasRestoredFile(location); !ok || metadataOnly {
|
2020-02-20 10:38:44 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
case work <- mustCheck{node, target}:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return err
|
|
|
|
})
|
2018-10-14 19:00:14 +00:00
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
for i := 0; i < nVerifyWorkers; i++ {
|
|
|
|
g.Go(func() (err error) {
|
|
|
|
var buf []byte
|
|
|
|
for job := range work {
|
2024-05-31 15:34:48 +00:00
|
|
|
_, buf, err = res.verifyFile(job.path, job.node, true, false, buf)
|
2018-04-13 14:02:09 +00:00
|
|
|
if err != nil {
|
2021-09-19 11:21:57 +00:00
|
|
|
err = res.Error(job.path, err)
|
|
|
|
}
|
|
|
|
if err != nil || ctx.Err() != nil {
|
2020-02-20 10:38:44 +00:00
|
|
|
break
|
2018-04-13 14:02:09 +00:00
|
|
|
}
|
2020-02-21 08:51:43 +00:00
|
|
|
atomic.AddUint64(&nchecked, 1)
|
2018-04-13 14:02:09 +00:00
|
|
|
}
|
2020-03-16 09:05:59 +00:00
|
|
|
return err
|
2020-02-20 10:38:44 +00:00
|
|
|
})
|
|
|
|
}
|
2018-04-13 14:02:09 +00:00
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
return int(nchecked), g.Wait()
|
|
|
|
}
|
|
|
|
|
2024-05-31 15:06:08 +00:00
|
|
|
type fileState struct {
|
|
|
|
blobMatches []bool
|
|
|
|
sizeMatches bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *fileState) NeedsRestore() bool {
|
|
|
|
if s == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if !s.sizeMatches {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
for _, match := range s.blobMatches {
|
|
|
|
if !match {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *fileState) HasMatchingBlob(i int) bool {
|
|
|
|
if s == nil || s.blobMatches == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return i < len(s.blobMatches) && s.blobMatches[i]
|
|
|
|
}
|
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
// Verify that the file target has the contents of node.
|
2020-03-19 11:56:11 +00:00
|
|
|
//
|
2020-02-20 10:38:44 +00:00
|
|
|
// buf and the first return value are scratch space, passed around for reuse.
|
2020-03-19 11:56:11 +00:00
|
|
|
// Reusing buffers prevents the verifier goroutines allocating all of RAM and
|
|
|
|
// flushing the filesystem cache (at least on Linux).
|
2024-05-31 15:34:48 +00:00
|
|
|
func (res *Restorer) verifyFile(target string, node *restic.Node, failFast bool, trustMtime bool, buf []byte) (*fileState, []byte, error) {
|
2024-06-13 20:40:35 +00:00
|
|
|
f, err := fs.OpenFile(target, fs.O_RDONLY|fs.O_NOFOLLOW, 0)
|
2020-02-20 10:38:44 +00:00
|
|
|
if err != nil {
|
2024-05-31 15:06:08 +00:00
|
|
|
return nil, buf, err
|
2020-02-20 10:38:44 +00:00
|
|
|
}
|
2021-09-19 12:14:35 +00:00
|
|
|
defer func() {
|
|
|
|
_ = f.Close()
|
|
|
|
}()
|
2020-02-20 10:38:44 +00:00
|
|
|
|
|
|
|
fi, err := f.Stat()
|
2024-05-31 15:06:08 +00:00
|
|
|
sizeMatches := true
|
2020-02-20 10:38:44 +00:00
|
|
|
switch {
|
|
|
|
case err != nil:
|
2024-05-31 15:06:08 +00:00
|
|
|
return nil, buf, err
|
2024-05-22 15:36:52 +00:00
|
|
|
case !fi.Mode().IsRegular():
|
2024-05-31 15:06:08 +00:00
|
|
|
return nil, buf, errors.Errorf("Expected %s to be a regular file", target)
|
2020-02-20 10:38:44 +00:00
|
|
|
case int64(node.Size) != fi.Size():
|
2024-05-31 15:06:08 +00:00
|
|
|
if failFast {
|
|
|
|
return nil, buf, errors.Errorf("Invalid file size for %s: expected %d, got %d",
|
|
|
|
target, node.Size, fi.Size())
|
|
|
|
}
|
|
|
|
sizeMatches = false
|
2020-02-20 10:38:44 +00:00
|
|
|
}
|
|
|
|
|
2024-05-31 15:34:48 +00:00
|
|
|
if trustMtime && fi.ModTime().Equal(node.ModTime) && sizeMatches {
|
|
|
|
return &fileState{nil, sizeMatches}, buf, nil
|
|
|
|
}
|
|
|
|
|
2024-05-31 15:06:08 +00:00
|
|
|
matches := make([]bool, len(node.Content))
|
2020-02-20 10:38:44 +00:00
|
|
|
var offset int64
|
2024-05-31 15:06:08 +00:00
|
|
|
for i, blobID := range node.Content {
|
2024-05-19 12:54:50 +00:00
|
|
|
length, found := res.repo.LookupBlobSize(restic.DataBlob, blobID)
|
2020-02-20 10:38:44 +00:00
|
|
|
if !found {
|
2024-05-31 15:06:08 +00:00
|
|
|
return nil, buf, errors.Errorf("Unable to fetch blob %s", blobID)
|
2020-02-20 10:38:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if length > uint(cap(buf)) {
|
2020-03-16 09:24:24 +00:00
|
|
|
buf = make([]byte, 2*length)
|
2020-02-20 10:38:44 +00:00
|
|
|
}
|
|
|
|
buf = buf[:length]
|
|
|
|
|
|
|
|
_, err = f.ReadAt(buf, offset)
|
2024-05-31 15:06:08 +00:00
|
|
|
if err == io.EOF && !failFast {
|
|
|
|
sizeMatches = false
|
|
|
|
break
|
|
|
|
}
|
2020-02-20 10:38:44 +00:00
|
|
|
if err != nil {
|
2024-05-31 15:06:08 +00:00
|
|
|
return nil, buf, err
|
2020-02-20 10:38:44 +00:00
|
|
|
}
|
2024-05-31 15:06:08 +00:00
|
|
|
matches[i] = blobID.Equal(restic.Hash(buf))
|
|
|
|
if failFast && !matches[i] {
|
|
|
|
return nil, buf, errors.Errorf(
|
2020-02-20 10:38:44 +00:00
|
|
|
"Unexpected content in %s, starting at offset %d",
|
|
|
|
target, offset)
|
|
|
|
}
|
|
|
|
offset += int64(length)
|
|
|
|
}
|
2018-04-13 14:02:09 +00:00
|
|
|
|
2024-05-31 15:06:08 +00:00
|
|
|
return &fileState{matches, sizeMatches}, buf, nil
|
2018-04-13 14:02:09 +00:00
|
|
|
}
|