restore: Removed legacy restore implementation
Signed-off-by: Igor Fedorenko <igor@ifedorenko.com>
This commit is contained in:
parent
1869930d95
commit
da57302fca
7 changed files with 33 additions and 72 deletions
|
@ -7,9 +7,5 @@ Implementation uses threads to download and process miltiple remote
|
||||||
files concurrently. To further reduce restore time, each remote
|
files concurrently. To further reduce restore time, each remote
|
||||||
file is downloaded using single repository request.
|
file is downloaded using single repository request.
|
||||||
|
|
||||||
Old restore implementation can be enabled with `--signethreaded` flag.
|
|
||||||
Use `--verify` restore flag to read restored files and verify their
|
|
||||||
content checksum.
|
|
||||||
|
|
||||||
https://github.com/restic/restic/issues/1605
|
https://github.com/restic/restic/issues/1605
|
||||||
https://github.com/restic/restic/pull/1719
|
https://github.com/restic/restic/pull/1719
|
||||||
|
|
|
@ -28,14 +28,13 @@ repository.
|
||||||
|
|
||||||
// RestoreOptions collects all options for the restore command.
|
// RestoreOptions collects all options for the restore command.
|
||||||
type RestoreOptions struct {
|
type RestoreOptions struct {
|
||||||
Exclude []string
|
Exclude []string
|
||||||
Include []string
|
Include []string
|
||||||
Target string
|
Target string
|
||||||
Host string
|
Host string
|
||||||
Paths []string
|
Paths []string
|
||||||
Tags restic.TagLists
|
Tags restic.TagLists
|
||||||
Verify bool
|
Verify bool
|
||||||
SingleThreaded bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var restoreOptions RestoreOptions
|
var restoreOptions RestoreOptions
|
||||||
|
@ -52,7 +51,6 @@ func init() {
|
||||||
flags.Var(&restoreOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"")
|
flags.Var(&restoreOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"")
|
||||||
flags.StringArrayVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
|
flags.StringArrayVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
|
||||||
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
|
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
|
||||||
flags.BoolVar(&restoreOptions.SingleThreaded, "singlethreaded", false, "use single-threaded (legacy) restore implementation")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||||
|
@ -157,7 +155,7 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||||
|
|
||||||
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
||||||
|
|
||||||
err = res.RestoreTo(ctx, opts.Target, opts.SingleThreaded)
|
err = res.RestoreTo(ctx, opts.Target)
|
||||||
if err == nil && opts.Verify {
|
if err == nil && opts.Verify {
|
||||||
Verbosef("verifying files in %s\n", opts.Target)
|
Verbosef("verifying files in %s\n", opts.Target)
|
||||||
var count int
|
var count int
|
||||||
|
|
|
@ -135,7 +135,7 @@ func (node Node) GetExtendedAttribute(a string) []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAt creates the node at the given path but does NOT restore node meta data.
|
// CreateAt creates the node at the given path but does NOT restore node meta data.
|
||||||
func (node *Node) CreateAt(ctx context.Context, path string, repo Repository, idx *HardlinkIndex) error {
|
func (node *Node) CreateAt(ctx context.Context, path string, repo Repository) error {
|
||||||
debug.Log("create node %v at %v", node.Name, path)
|
debug.Log("create node %v at %v", node.Name, path)
|
||||||
|
|
||||||
switch node.Type {
|
switch node.Type {
|
||||||
|
@ -144,7 +144,7 @@ func (node *Node) CreateAt(ctx context.Context, path string, repo Repository, id
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "file":
|
case "file":
|
||||||
if err := node.createFileAt(ctx, path, repo, idx); err != nil {
|
if err := node.createFileAt(ctx, path, repo); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "symlink":
|
case "symlink":
|
||||||
|
@ -259,18 +259,7 @@ func (node Node) createDirAt(path string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node Node) createFileAt(ctx context.Context, path string, repo Repository, idx *HardlinkIndex) error {
|
func (node Node) createFileAt(ctx context.Context, path string, repo Repository) error {
|
||||||
if node.Links > 1 && idx.Has(node.Inode, node.DeviceID) {
|
|
||||||
if err := fs.Remove(path); !os.IsNotExist(err) {
|
|
||||||
return errors.Wrap(err, "RemoveCreateHardlink")
|
|
||||||
}
|
|
||||||
err := fs.Link(idx.GetFilename(node.Inode, node.DeviceID), path)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "CreateHardlink")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
f, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "OpenFile")
|
return errors.Wrap(err, "OpenFile")
|
||||||
|
@ -287,10 +276,6 @@ func (node Node) createFileAt(ctx context.Context, path string, repo Repository,
|
||||||
return errors.Wrap(closeErr, "Close")
|
return errors.Wrap(closeErr, "Close")
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Links > 1 {
|
|
||||||
idx.Add(node.Inode, node.DeviceID, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -177,11 +177,9 @@ func TestNodeRestoreAt(t *testing.T) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
idx := restic.NewHardlinkIndex()
|
|
||||||
|
|
||||||
for _, test := range nodeTests {
|
for _, test := range nodeTests {
|
||||||
nodePath := filepath.Join(tempdir, test.Name)
|
nodePath := filepath.Join(tempdir, test.Name)
|
||||||
rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil, idx))
|
rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil))
|
||||||
rtest.OK(t, test.RestoreMetadata(nodePath))
|
rtest.OK(t, test.RestoreMetadata(nodePath))
|
||||||
|
|
||||||
if test.Type == "symlink" && runtime.GOOS == "windows" {
|
if test.Type == "symlink" && runtime.GOOS == "windows" {
|
||||||
|
|
|
@ -18,8 +18,6 @@ import (
|
||||||
// TODO evaluate memory footprint for larger repositories, say 10M packs/10M files
|
// TODO evaluate memory footprint for larger repositories, say 10M packs/10M files
|
||||||
// TODO consider replacing pack file cache with blob cache
|
// TODO consider replacing pack file cache with blob cache
|
||||||
// TODO avoid decrypting the same blob multiple times
|
// TODO avoid decrypting the same blob multiple times
|
||||||
// TODO remove `restore --singlethreaded` and review/simplify hardlink support
|
|
||||||
// (node.CreateAt shouldn't take HardlinkIndex)
|
|
||||||
// TODO evaluate disabled debug logging overhead for large repositories
|
// TODO evaluate disabled debug logging overhead for large repositories
|
||||||
// TODO consider logging snapshot-relative path to reduce log clutter
|
// TODO consider logging snapshot-relative path to reduce log clutter
|
||||||
|
|
||||||
|
|
|
@ -140,10 +140,10 @@ func (res *Restorer) traverseTree(ctx context.Context, target, location string,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, target, location string, idx *restic.HardlinkIndex) error {
|
func (res *Restorer) restoreNodeTo(ctx context.Context, node *restic.Node, target, location string) error {
|
||||||
debug.Log("restoreNode %v %v %v", node.Name, target, location)
|
debug.Log("restoreNode %v %v %v", node.Name, target, location)
|
||||||
|
|
||||||
err := node.CreateAt(ctx, target, res.repo, idx)
|
err := node.CreateAt(ctx, target, res.repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.Log("node.CreateAt(%s) error %v", target, err)
|
debug.Log("node.CreateAt(%s) error %v", target, err)
|
||||||
}
|
}
|
||||||
|
@ -163,9 +163,20 @@ func (res *Restorer) restoreNodeMetadataTo(node *restic.Node, target, location s
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
return res.restoreNodeMetadataTo(node, target, location)
|
||||||
|
}
|
||||||
|
|
||||||
// RestoreTo creates the directories and files in the snapshot below dst.
|
// RestoreTo creates the directories and files in the snapshot below dst.
|
||||||
// Before an item is created, res.Filter is called.
|
// Before an item is created, res.Filter is called.
|
||||||
func (res *Restorer) RestoreTo(ctx context.Context, dst string, singlethreaded bool) error {
|
func (res *Restorer) RestoreTo(ctx context.Context, dst string) error {
|
||||||
var err error
|
var err error
|
||||||
if !filepath.IsAbs(dst) {
|
if !filepath.IsAbs(dst) {
|
||||||
dst, err = filepath.Abs(dst)
|
dst, err = filepath.Abs(dst)
|
||||||
|
@ -180,31 +191,6 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string, singlethreaded b
|
||||||
noop := func(node *restic.Node, target, location string) error { return nil }
|
noop := func(node *restic.Node, target, location string) error { return nil }
|
||||||
|
|
||||||
idx := restic.NewHardlinkIndex()
|
idx := restic.NewHardlinkIndex()
|
||||||
if singlethreaded {
|
|
||||||
return res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
|
|
||||||
visitNode: func(node *restic.Node, target, location string) error {
|
|
||||||
// create parent dir with default permissions
|
|
||||||
// #leaveDir restores dir metadata after visiting all children
|
|
||||||
err := fs.MkdirAll(filepath.Dir(target), 0700)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.restoreNodeTo(ctx, node, target, location, idx)
|
|
||||||
},
|
|
||||||
|
|
||||||
// Restore directory permissions and timestamp at the end. If we did it earlier
|
|
||||||
// - children restore could fail because of restictive directory permission
|
|
||||||
// - children restore could overwrite the timestamp of the directory they are in
|
|
||||||
leaveDir: restoreNodeMetadata,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
filerestorer := newFileRestorer(res.repo.Backend().Load, res.repo.Key(), filePackTraverser{lookup: res.repo.Index().Lookup})
|
filerestorer := newFileRestorer(res.repo.Backend().Load, res.repo.Key(), filePackTraverser{lookup: res.repo.Index().Lookup})
|
||||||
|
|
||||||
|
@ -258,15 +244,15 @@ func (res *Restorer) RestoreTo(ctx context.Context, dst string, singlethreaded b
|
||||||
return res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{
|
return res.traverseTree(ctx, dst, string(filepath.Separator), *res.sn.Tree, treeVisitor{
|
||||||
enterDir: noop,
|
enterDir: noop,
|
||||||
visitNode: func(node *restic.Node, target, location string) error {
|
visitNode: func(node *restic.Node, target, location string) error {
|
||||||
isHardlink := func() bool {
|
if idx.Has(node.Inode, node.DeviceID) && idx.GetFilename(node.Inode, node.DeviceID) != target {
|
||||||
return idx.Has(node.Inode, node.DeviceID) && idx.GetFilename(node.Inode, node.DeviceID) != target
|
return res.restoreHardlinkAt(node, idx.GetFilename(node.Inode, node.DeviceID), target, location)
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type != "file" || isHardlink() {
|
if node.Type != "file" {
|
||||||
return res.restoreNodeTo(ctx, node, target, location, idx)
|
return res.restoreNodeTo(ctx, node, target, location)
|
||||||
}
|
}
|
||||||
|
|
||||||
return node.RestoreMetadata(target)
|
return res.restoreNodeMetadataTo(node, target, location)
|
||||||
},
|
},
|
||||||
leaveDir: restoreNodeMetadata,
|
leaveDir: restoreNodeMetadata,
|
||||||
})
|
})
|
||||||
|
|
|
@ -340,7 +340,7 @@ func TestRestorer(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err = res.RestoreTo(ctx, tempdir, false)
|
err = res.RestoreTo(ctx, tempdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -446,7 +446,7 @@ func TestRestorerRelative(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err = res.RestoreTo(ctx, "restore", false)
|
err = res.RestoreTo(ctx, "restore")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue