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"
|
|
|
|
|
2018-04-13 14:02:09 +00:00
|
|
|
"github.com/restic/restic/internal/crypto"
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/errors"
|
2016-08-21 15:48:36 +00:00
|
|
|
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/debug"
|
|
|
|
"github.com/restic/restic/internal/fs"
|
2018-05-11 02:56:10 +00:00
|
|
|
"github.com/restic/restic/internal/restic"
|
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.
|
2018-05-11 02:56:10 +00:00
|
|
|
func NewRestorer(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
|
|
|
|
|
2018-05-11 02:56:10 +00:00
|
|
|
r.sn, err = restic.LoadSnapshot(context.TODO(), 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.
|
|
|
|
func (res *Restorer) traverseTree(ctx context.Context, target, location string, treeID restic.ID, visitor treeVisitor) error {
|
2018-01-25 19:49:41 +00:00
|
|
|
debug.Log("%v %v %v", target, location, treeID)
|
2017-06-04 09:16:55 +00:00
|
|
|
tree, err := res.repo.LoadTree(ctx, 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)
|
2018-09-15 00:55:30 +00:00
|
|
|
return 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 {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
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 {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
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)
|
2017-07-07 09:54:10 +00:00
|
|
|
debug.Log("SelectFilter returned %v %v", selectedForRestore, childMayBeSelected)
|
2015-07-08 18:29:27 +00:00
|
|
|
|
2018-04-08 02:43:14 +00:00
|
|
|
sanitizeError := func(err error) error {
|
|
|
|
if err != nil {
|
2018-09-15 00:55:30 +00:00
|
|
|
err = res.Error(nodeLocation, err)
|
2018-04-08 02:43:14 +00:00
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if node.Type == "dir" {
|
2014-09-23 20:39:12 +00:00
|
|
|
if node.Subtree == nil {
|
2016-08-21 15:48:36 +00:00
|
|
|
return errors.Errorf("Dir without subtree in tree %v", treeID.Str())
|
2014-09-23 20:39:12 +00:00
|
|
|
}
|
|
|
|
|
2018-04-08 02:43:14 +00:00
|
|
|
if selectedForRestore {
|
|
|
|
err = sanitizeError(visitor.enterDir(node, nodeTarget, nodeLocation))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if childMayBeSelected {
|
|
|
|
err = sanitizeError(res.traverseTree(ctx, nodeTarget, nodeLocation, *node.Subtree, visitor))
|
2014-09-23 20:39:12 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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.leaveDir(node, nodeTarget, nodeLocation))
|
2018-04-20 11:53:11 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
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 {
|
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
|
2015-05-14 13:58:26 +00:00
|
|
|
}
|
2015-05-14 03:11:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-23 20:39:12 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-08 12:02:30 +00:00
|
|
|
restoreNodeMetadata := func(node *restic.Node, target, location string) error {
|
|
|
|
return res.restoreNodeMetadataTo(node, target, location)
|
2018-07-15 14:02:04 +00:00
|
|
|
}
|
2018-04-08 12:02:30 +00:00
|
|
|
noop := func(node *restic.Node, target, location string) error { return nil }
|
2018-07-15 14:02:04 +00:00
|
|
|
|
2018-05-11 02:56:10 +00:00
|
|
|
idx := restic.NewHardlinkIndex()
|
2018-04-08 12:02:30 +00:00
|
|
|
|
2018-09-15 00:18:37 +00:00
|
|
|
filerestorer := newFileRestorer(dst, res.repo.Backend().Load, res.repo.Key(), filePackTraverser{lookup: res.repo.Index().Lookup})
|
2018-04-08 12:02:30 +00:00
|
|
|
|
|
|
|
// first tree pass: create directories and collect all files to restore
|
|
|
|
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 {
|
|
|
|
// 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 {
|
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
|
|
|
}
|
|
|
|
|
2018-09-15 00:18:37 +00:00
|
|
|
filerestorer.addFile(location, node.Content)
|
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
|
|
|
leaveDir: noop,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-09-15 00:55:30 +00:00
|
|
|
err = filerestorer.restoreFiles(ctx, func(location string, err error) { res.Error(location, err) })
|
2018-04-08 12:02:30 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// second tree pass: restore special files and filesystem metadata
|
|
|
|
return res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{
|
|
|
|
enterDir: noop,
|
|
|
|
visitNode: func(node *restic.Node, target, location string) error {
|
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
|
|
|
},
|
2018-04-08 12:02:30 +00:00
|
|
|
leaveDir: restoreNodeMetadata,
|
2018-04-08 02:43:14 +00:00
|
|
|
})
|
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
|
|
|
|
|
|
|
// VerifyFiles reads all snapshot files and verifies their contents
|
|
|
|
func (res *Restorer) VerifyFiles(ctx context.Context, dst string) (int, error) {
|
|
|
|
// TODO multithreaded?
|
|
|
|
|
|
|
|
count := 0
|
|
|
|
err := res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{
|
|
|
|
enterDir: func(node *restic.Node, target, location string) error { return nil },
|
|
|
|
visitNode: func(node *restic.Node, target, location string) error {
|
|
|
|
if node.Type != "file" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
count++
|
|
|
|
stat, err := os.Stat(target)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if int64(node.Size) != stat.Size() {
|
|
|
|
return errors.Errorf("Invalid file size: expected %d got %d", node.Size, stat.Size())
|
|
|
|
}
|
|
|
|
|
|
|
|
offset := int64(0)
|
|
|
|
for _, blobID := range node.Content {
|
|
|
|
rd, err := os.Open(target)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
blobs, _ := res.repo.Index().Lookup(blobID, restic.DataBlob)
|
|
|
|
length := blobs[0].Length - uint(crypto.Extension)
|
|
|
|
buf := make([]byte, length) // TODO do I want to reuse the buffer somehow?
|
|
|
|
_, err = rd.ReadAt(buf, offset)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !blobID.Equal(restic.Hash(buf)) {
|
|
|
|
return errors.Errorf("Unexpected contents starting at offset %d", offset)
|
|
|
|
}
|
|
|
|
offset += int64(length)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
leaveDir: func(node *restic.Node, target, location string) error { return nil },
|
|
|
|
})
|
|
|
|
|
|
|
|
return count, err
|
|
|
|
}
|