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"
|
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"
|
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 {
|
2018-05-11 02:56:10 +00:00
|
|
|
repo restic.Repository
|
|
|
|
sn *restic.Snapshot
|
2014-09-23 20:39:12 +00:00
|
|
|
|
2018-09-15 00:55:30 +00:00
|
|
|
Error func(location string, err error) error
|
2018-05-11 02:56:10 +00:00
|
|
|
SelectFilter func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool)
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2018-09-15 00:55:30 +00:00
|
|
|
var restorerAbortOnAllErrors = func(location string, err error) error { return err }
|
2015-04-30 00:59:06 +00:00
|
|
|
|
|
|
|
// NewRestorer creates a restorer preloaded with the content from the snapshot id.
|
2020-04-10 09:36:14 +00:00
|
|
|
func NewRestorer(ctx context.Context, repo restic.Repository, id restic.ID) (*Restorer, error) {
|
2015-07-08 20:35:41 +00:00
|
|
|
r := &Restorer{
|
2018-04-08 12:02:30 +00:00
|
|
|
repo: repo,
|
|
|
|
Error: restorerAbortOnAllErrors,
|
2018-05-11 02:56:10 +00:00
|
|
|
SelectFilter: func(string, string, *restic.Node) (bool, bool) { return true, true },
|
2015-07-08 20:35:41 +00:00
|
|
|
}
|
2014-09-23 20:39:12 +00:00
|
|
|
|
|
|
|
var err error
|
|
|
|
|
2020-04-10 09:36:14 +00:00
|
|
|
r.sn, err = restic.LoadSnapshot(ctx, repo, id)
|
2014-09-23 20:39:12 +00:00
|
|
|
if err != nil {
|
2016-08-21 15:24:13 +00:00
|
|
|
return nil, err
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
leaveDir func(node *restic.Node, target, location string) error
|
|
|
|
}
|
|
|
|
|
|
|
|
// traverseTree traverses a tree from the repo and calls treeVisitor.
|
|
|
|
// target is the path in the file system, location within the snapshot.
|
2020-08-29 22:08:38 +00:00
|
|
|
func (res *Restorer) traverseTree(ctx context.Context, target, location string, treeID restic.ID, visitor treeVisitor) (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)
|
2020-08-29 22:08:38 +00:00
|
|
|
return hasRestored, res.Error(location, err)
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2015-01-10 22:40:10 +00:00
|
|
|
for _, node := range tree.Nodes {
|
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 {
|
2020-08-29 22:08:38 +00:00
|
|
|
return hasRestored, err
|
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 {
|
2020-08-29 22:08:38 +00:00
|
|
|
return hasRestored, err
|
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
|
|
|
|
}
|
|
|
|
|
2017-11-26 14:17:12 +00:00
|
|
|
selectedForRestore, childMayBeSelected := res.SelectFilter(nodeLocation, nodeTarget, node)
|
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 {
|
2020-08-29 22:08:38 +00:00
|
|
|
return 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 {
|
2020-08-29 22:08:38 +00:00
|
|
|
return 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
|
|
|
|
|
2018-04-08 02:43:14 +00:00
|
|
|
if childMayBeSelected {
|
2020-08-29 22:08:38 +00:00
|
|
|
childHasRestored, err = res.traverseTree(ctx, nodeTarget, nodeLocation, *node.Subtree, visitor)
|
|
|
|
err = sanitizeError(err)
|
2014-09-23 20:39:12 +00:00
|
|
|
if err != nil {
|
2020-08-29 22:08:38 +00:00
|
|
|
return hasRestored, err
|
|
|
|
}
|
|
|
|
// 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 {
|
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
|
|
|
err = sanitizeError(visitor.leaveDir(node, nodeTarget, nodeLocation))
|
2018-04-20 11:53:11 +00:00
|
|
|
if err != nil {
|
2020-08-29 22:08:38 +00:00
|
|
|
return 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 {
|
2020-08-29 22:08:38 +00:00
|
|
|
return hasRestored, err
|
2015-05-14 13:58:26 +00:00
|
|
|
}
|
2015-05-14 03:11:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-29 22:08:38 +00:00
|
|
|
return 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 {
|
2018-04-08 02:43:14 +00:00
|
|
|
debug.Log("restoreNode %v %v %v", node.Name, target, location)
|
2015-04-30 00:59:06 +00:00
|
|
|
|
2018-05-11 04:45:14 +00:00
|
|
|
err := node.CreateAt(ctx, target, res.repo)
|
2015-07-06 21:59:28 +00:00
|
|
|
if err != nil {
|
2017-11-26 14:17:12 +00:00
|
|
|
debug.Log("node.CreateAt(%s) error %v", target, err)
|
2015-07-06 21:59:28 +00:00
|
|
|
}
|
2018-04-08 02:43:14 +00:00
|
|
|
if err == nil {
|
|
|
|
err = res.restoreNodeMetadataTo(node, target, location)
|
2015-04-30 00:59:06 +00:00
|
|
|
}
|
|
|
|
|
2018-04-08 02:43:14 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location string) error {
|
|
|
|
debug.Log("restoreNodeMetadata %v %v %v", node.Name, target, location)
|
|
|
|
err := node.RestoreMetadata(target)
|
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 {
|
|
|
|
if err := fs.Remove(path); !os.IsNotExist(err) {
|
|
|
|
return errors.Wrap(err, "RemoveCreateHardlink")
|
|
|
|
}
|
|
|
|
err := fs.Link(target, path)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "CreateHardlink")
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
2018-09-27 12:59:33 +00:00
|
|
|
func (res *Restorer) restoreEmptyFileAt(node *restic.Node, target, location string) error {
|
|
|
|
wr, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = wr.Close()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return res.restoreNodeMetadataTo(node, target, location)
|
|
|
|
}
|
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-17 11:45:42 +00:00
|
|
|
idx := NewHardlinkIndex()
|
2021-08-07 22:38:17 +00:00
|
|
|
filerestorer := newFileRestorer(dst, res.repo.Backend().Load, res.repo.Key(), res.repo.Index().Lookup, res.repo.Connections())
|
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)
|
|
|
|
|
2018-04-08 12:02:30 +00:00
|
|
|
// first tree pass: create directories and collect all files to restore
|
2020-08-29 22:08:38 +00:00
|
|
|
_, err = res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{
|
2018-04-08 02:43:14 +00:00
|
|
|
enterDir: func(node *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)
|
2018-04-08 02:43:14 +00:00
|
|
|
// create dir with default permissions
|
|
|
|
// #leaveDir restores dir metadata after visiting all children
|
|
|
|
return fs.MkdirAll(target, 0700)
|
|
|
|
},
|
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)
|
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
|
|
|
// create parent dir with default permissions
|
2018-04-08 12:02:30 +00:00
|
|
|
// second pass #leaveDir restores dir metadata after visiting/restoring all children
|
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
|
|
|
err := fs.MkdirAll(filepath.Dir(target), 0700)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-04-08 12:02:30 +00:00
|
|
|
if node.Type != "file" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-09-27 12:59:33 +00:00
|
|
|
if node.Size == 0 {
|
|
|
|
return nil // deal with empty files later
|
|
|
|
}
|
|
|
|
|
2018-04-08 12:02:30 +00:00
|
|
|
if node.Links > 1 {
|
|
|
|
if idx.Has(node.Inode, node.DeviceID) {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2020-08-15 15:45:05 +00:00
|
|
|
filerestorer.addFile(location, node.Content, int64(node.Size))
|
2018-04-08 12:02:30 +00:00
|
|
|
|
|
|
|
return nil
|
2018-04-08 02:43:14 +00:00
|
|
|
},
|
2018-04-08 12:02:30 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-11-27 12:22:38 +00:00
|
|
|
err = filerestorer.restoreFiles(ctx)
|
2018-04-08 12:02:30 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
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
|
2020-08-29 22:08:38 +00:00
|
|
|
_, err = res.traverseTree(ctx, dst, string(filepath.Separator), *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" {
|
|
|
|
return res.restoreNodeTo(ctx, node, target, location)
|
2018-04-08 12:02:30 +00:00
|
|
|
}
|
|
|
|
|
2018-09-27 12:59:33 +00:00
|
|
|
// create empty files, but not hardlinks to empty files
|
|
|
|
if node.Size == 0 && (node.Links < 2 || !idx.Has(node.Inode, node.DeviceID)) {
|
|
|
|
if node.Links > 1 {
|
|
|
|
idx.Add(node.Inode, node.DeviceID, location)
|
|
|
|
}
|
|
|
|
return res.restoreEmptyFileAt(node, target, location)
|
|
|
|
}
|
|
|
|
|
|
|
|
if idx.Has(node.Inode, node.DeviceID) && idx.GetFilename(node.Inode, node.DeviceID) != location {
|
|
|
|
return res.restoreHardlinkAt(node, filerestorer.targetPath(idx.GetFilename(node.Inode, node.DeviceID)), target, location)
|
|
|
|
}
|
|
|
|
|
2018-05-11 04:45:14 +00:00
|
|
|
return res.restoreNodeMetadataTo(node, target, location)
|
2018-04-08 02:43:14 +00:00
|
|
|
},
|
2020-03-16 14:20:00 +00:00
|
|
|
leaveDir: res.restoreNodeMetadataTo,
|
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
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
_, err := res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{
|
|
|
|
visitNode: func(node *restic.Node, target, location string) error {
|
|
|
|
if node.Type != "file" {
|
|
|
|
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 {
|
|
|
|
buf, err = res.verifyFile(job.path, job.node, 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()
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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).
|
2020-02-20 10:38:44 +00:00
|
|
|
func (res *Restorer) verifyFile(target string, node *restic.Node, buf []byte) ([]byte, error) {
|
|
|
|
f, err := os.Open(target)
|
|
|
|
if err != nil {
|
|
|
|
return buf, err
|
|
|
|
}
|
2021-09-19 12:14:35 +00:00
|
|
|
defer func() {
|
|
|
|
_ = f.Close()
|
|
|
|
}()
|
2020-02-20 10:38:44 +00:00
|
|
|
|
|
|
|
fi, err := f.Stat()
|
|
|
|
switch {
|
|
|
|
case err != nil:
|
2020-03-19 11:56:11 +00:00
|
|
|
return buf, err
|
2020-02-20 10:38:44 +00:00
|
|
|
case int64(node.Size) != fi.Size():
|
|
|
|
return buf, errors.Errorf("Invalid file size for %s: expected %d, got %d",
|
|
|
|
target, node.Size, fi.Size())
|
|
|
|
}
|
|
|
|
|
|
|
|
var offset int64
|
|
|
|
for _, blobID := range node.Content {
|
|
|
|
length, found := res.repo.LookupBlobSize(blobID, restic.DataBlob)
|
|
|
|
if !found {
|
|
|
|
return buf, errors.Errorf("Unable to fetch blob %s", blobID)
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
if err != nil {
|
|
|
|
return buf, err
|
|
|
|
}
|
|
|
|
if !blobID.Equal(restic.Hash(buf)) {
|
|
|
|
return buf, errors.Errorf(
|
|
|
|
"Unexpected content in %s, starting at offset %d",
|
|
|
|
target, offset)
|
|
|
|
}
|
|
|
|
offset += int64(length)
|
|
|
|
}
|
2018-04-13 14:02:09 +00:00
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
return buf, nil
|
2018-04-13 14:02:09 +00:00
|
|
|
}
|