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"
|
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 09:43:42 +00:00
|
|
|
repo restic.Repository
|
|
|
|
sn *restic.Snapshot
|
|
|
|
sparse bool
|
|
|
|
progress *restoreui.Progress
|
|
|
|
overwrite OverwriteBehavior
|
|
|
|
|
|
|
|
fileList map[string]struct{}
|
2022-10-28 15:44:34 +00:00
|
|
|
|
2018-09-15 00:55:30 +00:00
|
|
|
Error func(location string, err error) error
|
2024-02-23 00:52:26 +00:00
|
|
|
Warn func(message string)
|
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
|
|
|
}
|
|
|
|
|
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 09:43:42 +00:00
|
|
|
Sparse bool
|
|
|
|
Progress *restoreui.Progress
|
|
|
|
Overwrite OverwriteBehavior
|
|
|
|
}
|
|
|
|
|
|
|
|
type OverwriteBehavior int
|
|
|
|
|
|
|
|
// Constants for different overwrite behavior
|
|
|
|
const (
|
|
|
|
OverwriteAlways OverwriteBehavior = 0
|
|
|
|
OverwriteIfNewer OverwriteBehavior = 1
|
|
|
|
OverwriteNever OverwriteBehavior = 2
|
|
|
|
OverwriteInvalid OverwriteBehavior = 3
|
|
|
|
)
|
|
|
|
|
|
|
|
// Set implements the method needed for pflag command flag parsing.
|
|
|
|
func (c *OverwriteBehavior) Set(s string) error {
|
|
|
|
switch s {
|
|
|
|
case "always":
|
|
|
|
*c = OverwriteAlways
|
|
|
|
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"
|
|
|
|
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 09:42:25 +00:00
|
|
|
sparse: opts.Sparse,
|
|
|
|
progress: opts.Progress,
|
2024-05-31 09:43:42 +00:00
|
|
|
overwrite: opts.Overwrite,
|
|
|
|
fileList: make(map[string]struct{}),
|
2018-04-08 12:02:30 +00:00
|
|
|
Error: restorerAbortOnAllErrors,
|
2018-05-11 02:56:10 +00:00
|
|
|
SelectFilter: func(string, string, *restic.Node) (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
|
|
|
|
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)
|
2023-05-01 10:05:48 +00:00
|
|
|
return err
|
2015-07-06 21:59:28 +00:00
|
|
|
}
|
2023-05-01 10:05:48 +00:00
|
|
|
|
2024-05-31 09:07:53 +00:00
|
|
|
res.progress.AddProgress(location, 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 {
|
|
|
|
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 {
|
|
|
|
if err := fs.Remove(path); !os.IsNotExist(err) {
|
|
|
|
return errors.Wrap(err, "RemoveCreateHardlink")
|
|
|
|
}
|
|
|
|
err := fs.Link(target, path)
|
|
|
|
if err != nil {
|
2022-10-16 09:32:38 +00:00
|
|
|
return errors.WithStack(err)
|
2018-05-11 04:45:14 +00:00
|
|
|
}
|
2022-10-28 15:44:34 +00:00
|
|
|
|
2024-05-31 09:07:53 +00:00
|
|
|
res.progress.AddProgress(location, 0, 0)
|
2022-10-28 15:44:34 +00:00
|
|
|
|
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
|
|
|
}
|
|
|
|
|
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,
|
2022-10-28 15:44:34 +00:00
|
|
|
res.repo.Connections(), res.sparse, res.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)
|
|
|
|
|
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{
|
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-05-31 09:07:53 +00:00
|
|
|
res.progress.AddFile(0)
|
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" {
|
2024-05-31 09:07:53 +00:00
|
|
|
res.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
|
|
|
|
res.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 16:30:51 +00:00
|
|
|
return res.withOverwriteCheck(node, target, false, func() error {
|
2024-05-31 09:43:42 +00:00
|
|
|
res.progress.AddFile(node.Size)
|
|
|
|
filerestorer.addFile(location, node.Content, int64(node.Size))
|
|
|
|
res.trackFile(location)
|
|
|
|
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" {
|
2024-05-31 16:30:51 +00:00
|
|
|
return res.withOverwriteCheck(node, target, false, func() error {
|
2024-05-31 09:43:42 +00:00
|
|
|
return res.restoreNodeTo(ctx, node, target, location)
|
|
|
|
})
|
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 16:30:51 +00:00
|
|
|
return res.withOverwriteCheck(node, target, true, func() error {
|
2024-05-31 09:43:42 +00:00
|
|
|
return res.restoreHardlinkAt(node, filerestorer.targetPath(idx.Value(node.Inode, node.DeviceID)), target, location)
|
|
|
|
})
|
2018-09-27 12:59:33 +00:00
|
|
|
}
|
|
|
|
|
2024-05-31 09:43:42 +00:00
|
|
|
if res.hasRestoredFile(location) {
|
|
|
|
return res.restoreNodeMetadataTo(node, target, location)
|
|
|
|
}
|
|
|
|
// don't touch skipped files
|
|
|
|
return nil
|
2018-04-08 02:43:14 +00:00
|
|
|
},
|
2023-05-01 10:05:48 +00:00
|
|
|
leaveDir: func(node *restic.Node, target, location string) error {
|
|
|
|
err := res.restoreNodeMetadataTo(node, target, location)
|
2024-05-31 09:07:53 +00:00
|
|
|
if err == nil {
|
2023-05-01 10:05:48 +00:00
|
|
|
res.progress.AddProgress(location, 0, 0)
|
|
|
|
}
|
|
|
|
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-05-31 09:43:42 +00:00
|
|
|
func (res *Restorer) trackFile(location string) {
|
|
|
|
res.fileList[location] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (res *Restorer) hasRestoredFile(location string) bool {
|
|
|
|
_, ok := res.fileList[location]
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
2024-05-31 16:30:51 +00:00
|
|
|
func (res *Restorer) withOverwriteCheck(node *restic.Node, target string, isHardlink bool, cb func() error) error {
|
2024-05-31 09:43:42 +00:00
|
|
|
overwrite, err := shouldOverwrite(res.overwrite, node, target)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
} else if !overwrite {
|
|
|
|
size := node.Size
|
|
|
|
if isHardlink {
|
|
|
|
size = 0
|
|
|
|
}
|
2024-05-31 12:12:06 +00:00
|
|
|
res.progress.AddSkippedFile(size)
|
2024-05-31 09:43:42 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return cb()
|
|
|
|
}
|
|
|
|
|
|
|
|
func shouldOverwrite(overwrite OverwriteBehavior, node *restic.Node, destination string) (bool, error) {
|
|
|
|
if overwrite == OverwriteAlways {
|
|
|
|
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
|
|
|
|
2020-02-20 10:38:44 +00:00
|
|
|
_, err := res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{
|
2024-05-31 09:43:42 +00:00
|
|
|
visitNode: func(node *restic.Node, target, location string) error {
|
|
|
|
if node.Type != "file" || !res.hasRestoredFile(location) {
|
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 {
|
|
|
|
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) {
|
2024-05-22 15:36:52 +00:00
|
|
|
f, err := os.OpenFile(target, fs.O_RDONLY|fs.O_NOFOLLOW, 0)
|
2020-02-20 10:38:44 +00:00
|
|
|
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
|
2024-05-22 15:36:52 +00:00
|
|
|
case !fi.Mode().IsRegular():
|
|
|
|
return 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():
|
|
|
|
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 {
|
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 {
|
|
|
|
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
|
|
|
}
|