Refactor: moved restorer to separate package

Signed-off-by: Igor Fedorenko <igor@ifedorenko.com>
This commit is contained in:
Igor Fedorenko
2018-05-10 22:56:10 -04:00
parent e4c0d77bdd
commit 26be094f28
3 changed files with 20 additions and 17 deletions

View File

@@ -0,0 +1,174 @@
package restorer
import (
"context"
"os"
"path/filepath"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/restic"
)
// Restorer is used to restore a snapshot to a directory.
type Restorer struct {
repo restic.Repository
sn *restic.Snapshot
Error func(dir string, node *restic.Node, err error) error
SelectFilter func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool)
}
var restorerAbortOnAllErrors = func(str string, node *restic.Node, err error) error { return err }
// NewRestorer creates a restorer preloaded with the content from the snapshot id.
func NewRestorer(repo restic.Repository, id restic.ID) (*Restorer, error) {
r := &Restorer{
repo: repo, Error: restorerAbortOnAllErrors,
SelectFilter: func(string, string, *restic.Node) (bool, bool) { return true, true },
}
var err error
r.sn, err = restic.LoadSnapshot(context.TODO(), repo, id)
if err != nil {
return nil, err
}
return r, nil
}
// restoreTo restores a tree from the repo to a destination. target is the path in
// the file system, location within the snapshot.
func (res *Restorer) restoreTo(ctx context.Context, target, location string, treeID restic.ID, idx *restic.HardlinkIndex) error {
debug.Log("%v %v %v", target, location, treeID)
tree, err := res.repo.LoadTree(ctx, treeID)
if err != nil {
debug.Log("error loading tree %v: %v", treeID, err)
return res.Error(location, nil, err)
}
for _, node := range tree.Nodes {
// 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)
err := res.Error(location, node, errors.New("node has invalid name"))
if err != nil {
return err
}
continue
}
nodeTarget := filepath.Join(target, nodeName)
nodeLocation := filepath.Join(location, nodeName)
if target == nodeTarget || !fs.HasPathPrefix(target, nodeTarget) {
debug.Log("target: %v %v", target, nodeTarget)
debug.Log("node %q has invalid target path %q", node.Name, nodeTarget)
err := res.Error(nodeLocation, node, errors.New("node has invalid path"))
if err != nil {
return err
}
continue
}
// sockets cannot be restored
if node.Type == "socket" {
continue
}
selectedForRestore, childMayBeSelected := res.SelectFilter(nodeLocation, nodeTarget, node)
debug.Log("SelectFilter returned %v %v", selectedForRestore, childMayBeSelected)
if node.Type == "dir" && childMayBeSelected {
if node.Subtree == nil {
return errors.Errorf("Dir without subtree in tree %v", treeID.Str())
}
err = res.restoreTo(ctx, nodeTarget, nodeLocation, *node.Subtree, idx)
if err != nil {
err = res.Error(nodeLocation, node, err)
if err != nil {
return err
}
}
}
if selectedForRestore {
err = res.restoreNodeTo(ctx, node, nodeTarget, nodeLocation, idx)
if err != nil {
err = res.Error(nodeLocation, node, errors.Wrap(err, "restoreNodeTo"))
if err != nil {
return err
}
}
// Restore directory timestamp at the end. If we would do it earlier, restoring files within
// the directory would overwrite the timestamp of the directory they are in.
err = node.RestoreTimestamps(nodeTarget)
if err != nil {
err = res.Error(nodeLocation, node, errors.Wrap(err, "RestoreTimestamps"))
if err != nil {
return err
}
}
}
}
return nil
}
func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, target, location string, idx *restic.HardlinkIndex) error {
debug.Log("%v %v %v", node.Name, target, location)
err := node.CreateAt(ctx, target, res.repo, idx)
if err != nil {
debug.Log("node.CreateAt(%s) error %v", target, err)
}
// Did it fail because of ENOENT?
if err != nil && os.IsNotExist(errors.Cause(err)) {
debug.Log("create intermediate paths")
// Create parent directories and retry
err = fs.MkdirAll(filepath.Dir(target), 0700)
if err == nil || os.IsExist(errors.Cause(err)) {
err = node.CreateAt(ctx, target, res.repo, idx)
}
}
if err != nil {
debug.Log("error %v", err)
err = res.Error(location, node, err)
if err != nil {
return err
}
}
return nil
}
// RestoreTo creates the directories and files in the snapshot below dst.
// Before an item is created, res.Filter is called.
func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
var err error
if !filepath.IsAbs(dst) {
dst, err = filepath.Abs(dst)
if err != nil {
return errors.Wrap(err, "Abs")
}
}
idx := restic.NewHardlinkIndex()
return res.restoreTo(ctx, dst, string(filepath.Separator), *res.sn.Tree, idx)
}
// Snapshot returns the snapshot this restorer is configured to use.
func (res *Restorer) Snapshot() *restic.Snapshot {
return res.sn
}

View File

@@ -0,0 +1,425 @@
package restorer_test
import (
"bytes"
"context"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/restorer"
rtest "github.com/restic/restic/internal/test"
)
type Node interface{}
type Snapshot struct {
Nodes map[string]Node
treeID restic.ID
}
type File struct {
Data string
}
type Dir struct {
Nodes map[string]Node
Mode os.FileMode
}
func saveFile(t testing.TB, repo restic.Repository, node File) restic.ID {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
id, err := repo.SaveBlob(ctx, restic.DataBlob, []byte(node.Data), restic.ID{})
if err != nil {
t.Fatal(err)
}
return id
}
func saveDir(t testing.TB, repo restic.Repository, nodes map[string]Node) restic.ID {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tree := &restic.Tree{}
for name, n := range nodes {
var id restic.ID
switch node := n.(type) {
case File:
id = saveFile(t, repo, node)
tree.Insert(&restic.Node{
Type: "file",
Mode: 0644,
Name: name,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Content: []restic.ID{id},
})
case Dir:
id = saveDir(t, repo, node.Nodes)
mode := node.Mode
if mode == 0 {
mode = 0755
}
tree.Insert(&restic.Node{
Type: "dir",
Mode: mode,
Name: name,
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
Subtree: &id,
})
default:
t.Fatalf("unknown node type %T", node)
}
}
id, err := repo.SaveTree(ctx, tree)
if err != nil {
t.Fatal(err)
}
return id
}
func saveSnapshot(t testing.TB, repo restic.Repository, snapshot Snapshot) (restic.Repository, restic.ID) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
treeID := saveDir(t, repo, snapshot.Nodes)
err := repo.Flush(ctx)
if err != nil {
t.Fatal(err)
}
err = repo.SaveIndex(ctx)
if err != nil {
t.Fatal(err)
}
sn, err := restic.NewSnapshot([]string{"test"}, nil, "", time.Now())
if err != nil {
t.Fatal(err)
}
sn.Tree = &treeID
id, err := repo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
if err != nil {
t.Fatal(err)
}
return repo, id
}
// toSlash converts the OS specific path dir to a slash-separated path.
func toSlash(dir string) string {
data := strings.Split(dir, string(filepath.Separator))
return strings.Join(data, "/")
}
func TestRestorer(t *testing.T) {
var tests = []struct {
Snapshot
Files map[string]string
ErrorsMust map[string]string
ErrorsMay map[string]string
}{
// valid test cases
{
Snapshot: Snapshot{
Nodes: map[string]Node{
"foo": File{"content: foo\n"},
"dirtest": Dir{
Nodes: map[string]Node{
"file": File{"content: file\n"},
},
},
},
},
Files: map[string]string{
"foo": "content: foo\n",
"dirtest/file": "content: file\n",
},
},
{
Snapshot: Snapshot{
Nodes: map[string]Node{
"top": File{"toplevel file"},
"dir": Dir{
Nodes: map[string]Node{
"file": File{"file in dir"},
"subdir": Dir{
Nodes: map[string]Node{
"file": File{"file in subdir"},
},
},
},
},
},
},
Files: map[string]string{
"top": "toplevel file",
"dir/file": "file in dir",
"dir/subdir/file": "file in subdir",
},
},
{
Snapshot: Snapshot{
Nodes: map[string]Node{
"dir": Dir{
Mode: 0444,
},
"file": File{"top-level file"},
},
},
Files: map[string]string{
"file": "top-level file",
},
},
{
Snapshot: Snapshot{
Nodes: map[string]Node{
"dir": Dir{
Mode: 0555,
Nodes: map[string]Node{
"file": File{"file in dir"},
},
},
},
},
Files: map[string]string{
"dir/file": "file in dir",
},
},
// test cases with invalid/constructed names
{
Snapshot: Snapshot{
Nodes: map[string]Node{
`..\test`: File{"foo\n"},
`..\..\foo\..\bar\..\xx\test2`: File{"test2\n"},
},
},
ErrorsMay: map[string]string{
`/#..\test`: "node has invalid name",
`/#..\..\foo\..\bar\..\xx\test2`: "node has invalid name",
},
},
{
Snapshot: Snapshot{
Nodes: map[string]Node{
`../test`: File{"foo\n"},
`../../foo/../bar/../xx/test2`: File{"test2\n"},
},
},
ErrorsMay: map[string]string{
`/#../test`: "node has invalid name",
`/#../../foo/../bar/../xx/test2`: "node has invalid name",
},
},
{
Snapshot: Snapshot{
Nodes: map[string]Node{
"top": File{"toplevel file"},
"x": Dir{
Nodes: map[string]Node{
"file1": File{"file1"},
"..": Dir{
Nodes: map[string]Node{
"file2": File{"file2"},
"..": Dir{
Nodes: map[string]Node{
"file2": File{"file2"},
},
},
},
},
},
},
},
},
Files: map[string]string{
"top": "toplevel file",
},
ErrorsMust: map[string]string{
`/x#..`: "node has invalid name",
},
},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
repo, cleanup := repository.TestRepository(t)
defer cleanup()
_, id := saveSnapshot(t, repo, test.Snapshot)
t.Logf("snapshot saved as %v", id.Str())
res, err := restorer.NewRestorer(repo, id)
if err != nil {
t.Fatal(err)
}
tempdir, cleanup := rtest.TempDir(t)
defer cleanup()
res.SelectFilter = func(item, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) {
t.Logf("restore %v to %v", item, dstpath)
if !fs.HasPathPrefix(tempdir, dstpath) {
t.Errorf("would restore %v to %v, which is not within the target dir %v",
item, dstpath, tempdir)
return false, false
}
return true, true
}
errors := make(map[string]string)
res.Error = func(dir string, node *restic.Node, err error) error {
t.Logf("restore returned error for %q in dir %v: %v", node.Name, dir, err)
dir = toSlash(dir)
errors[dir+"#"+node.Name] = err.Error()
return nil
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = res.RestoreTo(ctx, tempdir)
if err != nil {
t.Fatal(err)
}
for filename, errorMessage := range test.ErrorsMust {
msg, ok := errors[filename]
if !ok {
t.Errorf("expected error for %v, found none", filename)
continue
}
if msg != "" && msg != errorMessage {
t.Errorf("wrong error message for %v: got %q, want %q",
filename, msg, errorMessage)
}
delete(errors, filename)
}
for filename, errorMessage := range test.ErrorsMay {
msg, ok := errors[filename]
if !ok {
continue
}
if msg != "" && msg != errorMessage {
t.Errorf("wrong error message for %v: got %q, want %q",
filename, msg, errorMessage)
}
delete(errors, filename)
}
for filename, err := range errors {
t.Errorf("unexpected error for %v found: %v", filename, err)
}
for filename, content := range test.Files {
data, err := ioutil.ReadFile(filepath.Join(tempdir, filepath.FromSlash(filename)))
if err != nil {
t.Errorf("unable to read file %v: %v", filename, err)
continue
}
if !bytes.Equal(data, []byte(content)) {
t.Errorf("file %v has wrong content: want %q, got %q", filename, content, data)
}
}
})
}
}
func TestRestorerRelative(t *testing.T) {
var tests = []struct {
Snapshot
Files map[string]string
}{
{
Snapshot: Snapshot{
Nodes: map[string]Node{
"foo": File{"content: foo\n"},
"dirtest": Dir{
Nodes: map[string]Node{
"file": File{"content: file\n"},
},
},
},
},
Files: map[string]string{
"foo": "content: foo\n",
"dirtest/file": "content: file\n",
},
},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
repo, cleanup := repository.TestRepository(t)
defer cleanup()
_, id := saveSnapshot(t, repo, test.Snapshot)
t.Logf("snapshot saved as %v", id.Str())
res, err := restorer.NewRestorer(repo, id)
if err != nil {
t.Fatal(err)
}
tempdir, cleanup := rtest.TempDir(t)
defer cleanup()
cleanup = fs.TestChdir(t, tempdir)
defer cleanup()
errors := make(map[string]string)
res.Error = func(dir string, node *restic.Node, err error) error {
t.Logf("restore returned error for %q in dir %v: %v", node.Name, dir, err)
dir = toSlash(dir)
errors[dir+"#"+node.Name] = err.Error()
return nil
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = res.RestoreTo(ctx, "restore")
if err != nil {
t.Fatal(err)
}
for filename, err := range errors {
t.Errorf("unexpected error for %v found: %v", filename, err)
}
for filename, content := range test.Files {
data, err := ioutil.ReadFile(filepath.Join(tempdir, "restore", filepath.FromSlash(filename)))
if err != nil {
t.Errorf("unable to read file %v: %v", filename, err)
continue
}
if !bytes.Equal(data, []byte(content)) {
t.Errorf("file %v has wrong content: want %q, got %q", filename, content, data)
}
}
})
}
}