mirror of
https://github.com/restic/restic.git
synced 2026-05-28 04:35:41 -04:00
Allow stripping path components during restore
Restores previously always recreated the full original path under the target directory. For example, restoring /home/work/foo to /home/restores would result in /home/restores/home/work/foo. This change adds support for stripping leading path components during restore, similar to tar’s --strip-components option. Users can now skip the first N directories when restoring, making it more convenient to place files directly at the desired target location.
This commit is contained in:
parent
9e2d60e28c
commit
79b294012a
5 changed files with 169 additions and 4 deletions
10
changelog/unreleased/issue-5639
Normal file
10
changelog/unreleased/issue-5639
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Enhancement: Allow stripping directories when restoring
|
||||
|
||||
Restic restore always restored the full backed up path to a target location.
|
||||
For example restoring `/home/work/foo` to `/home/restores` would create `/home/restores/home/work/foo`.
|
||||
Restic now, much like `tar` permits trimming the structure on restore allowing to skip first N levels of directories using the `--strip-components` option.
|
||||
This way you can now restore `/home/work/foo` to `/home/restores/foo` using `--strip-components=2`.
|
||||
Pathnames with fewer elements will be silently skipped.
|
||||
|
||||
https://github.com/restic/restic/issues/5639
|
||||
https://github.com/restic/restic/pull/5681
|
||||
|
|
@ -73,6 +73,7 @@ type RestoreOptions struct {
|
|||
ExcludeXattrPattern []string
|
||||
IncludeXattrPattern []string
|
||||
OwnershipByName bool
|
||||
StripComponents int
|
||||
}
|
||||
|
||||
func (opts *RestoreOptions) AddFlags(f *pflag.FlagSet) {
|
||||
|
|
@ -90,6 +91,7 @@ func (opts *RestoreOptions) AddFlags(f *pflag.FlagSet) {
|
|||
f.BoolVar(&opts.Verify, "verify", false, "verify restored files content")
|
||||
f.Var(&opts.Overwrite, "overwrite", "overwrite behavior, one of (always|if-changed|if-newer|never)")
|
||||
f.BoolVar(&opts.Delete, "delete", false, "delete files from target directory if they do not exist in snapshot. Use '--dry-run -vv' to check what would be deleted")
|
||||
f.IntVar(&opts.StripComponents, "strip-components", 0, "number of leading path components to strip when restoring")
|
||||
if runtime.GOOS != "windows" {
|
||||
f.BoolVar(&opts.OwnershipByName, "ownership-by-name", false, "restore file ownership by user name and group name (except POSIX ACLs)")
|
||||
}
|
||||
|
|
@ -178,6 +180,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options,
|
|||
Overwrite: opts.Overwrite,
|
||||
Delete: opts.Delete,
|
||||
OwnershipByName: opts.OwnershipByName,
|
||||
StripComponents: opts.StripComponents,
|
||||
})
|
||||
|
||||
totalErrors := 0
|
||||
|
|
|
|||
|
|
@ -75,6 +75,15 @@ func testRunRestoreExcludesFromFile(t testing.TB, gopts global.Options, dir stri
|
|||
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID.String(), opts, gopts))
|
||||
}
|
||||
|
||||
func testRunRestoreStripComponents(t testing.TB, gopts global.Options, dir string, snapshotID string, stripComponents int) {
|
||||
opts := RestoreOptions{
|
||||
Target: dir,
|
||||
StripComponents: stripComponents,
|
||||
}
|
||||
|
||||
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID, opts, gopts))
|
||||
}
|
||||
|
||||
func TestRestoreMustFailWhenUsingBothIncludesAndExcludes(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
|
@ -416,3 +425,127 @@ func TestRestoreDefaultLayout(t *testing.T) {
|
|||
rtest.RemoveAll(t, filepath.Join(env.base, "repo"))
|
||||
rtest.RemoveAll(t, target)
|
||||
}
|
||||
|
||||
func TestRestoreStripComponents(t *testing.T) {
|
||||
testfiles := []struct {
|
||||
path string
|
||||
size uint
|
||||
}{
|
||||
{"subdir1/subdir2/file1.txt", 100},
|
||||
{"subdir1/file2.txt", 150},
|
||||
{"file3.txt", 200},
|
||||
}
|
||||
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
for _, testFile := range testfiles {
|
||||
fullPath := filepath.Join(env.testdata, testFile.path)
|
||||
rtest.OK(t, os.MkdirAll(filepath.Dir(fullPath), 0755))
|
||||
rtest.OK(t, appendRandomData(fullPath, testFile.size))
|
||||
}
|
||||
|
||||
opts := BackupOptions{}
|
||||
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts)
|
||||
testRunCheck(t, env.gopts)
|
||||
|
||||
snapshotID := testListSnapshots(t, env.gopts, 1)[0]
|
||||
|
||||
testCases := []struct {
|
||||
stripComponents int
|
||||
expectedFiles []struct {
|
||||
path string
|
||||
size uint
|
||||
}
|
||||
nonExistentDirs []string
|
||||
}{
|
||||
{
|
||||
stripComponents: 0,
|
||||
expectedFiles: []struct {
|
||||
path string
|
||||
size uint
|
||||
}{
|
||||
{"testdata/subdir1/subdir2/file1.txt", 100},
|
||||
{"testdata/subdir1/file2.txt", 150},
|
||||
{"testdata/file3.txt", 200},
|
||||
},
|
||||
nonExistentDirs: []string{},
|
||||
},
|
||||
{
|
||||
stripComponents: 1,
|
||||
expectedFiles: []struct {
|
||||
path string
|
||||
size uint
|
||||
}{
|
||||
{"subdir1/subdir2/file1.txt", 100},
|
||||
{"subdir1/file2.txt", 150},
|
||||
{"file3.txt", 200},
|
||||
},
|
||||
nonExistentDirs: []string{"testdata"},
|
||||
},
|
||||
{
|
||||
stripComponents: 2,
|
||||
expectedFiles: []struct {
|
||||
path string
|
||||
size uint
|
||||
}{
|
||||
{"subdir2/file1.txt", 100},
|
||||
{"file2.txt", 150},
|
||||
},
|
||||
nonExistentDirs: []string{"testdata", "subdir1"},
|
||||
},
|
||||
{
|
||||
stripComponents: 3,
|
||||
expectedFiles: []struct {
|
||||
path string
|
||||
size uint
|
||||
}{
|
||||
{"file1.txt", 100},
|
||||
},
|
||||
nonExistentDirs: []string{"testdata", "subdir1", "subdir2"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||
testRunRestoreStripComponents(t, env.gopts, restoredir, snapshotID.String(), tc.stripComponents)
|
||||
for _, testFile := range tc.expectedFiles {
|
||||
restoredFilePath := filepath.Join(restoredir, testFile.path)
|
||||
rtest.OK(t, testFileSize(restoredFilePath, int64(testFile.size)))
|
||||
}
|
||||
for _, dir := range tc.nonExistentDirs {
|
||||
_, err := os.Stat(filepath.Join(restoredir, dir))
|
||||
rtest.Assert(t, os.IsNotExist(err), "%s directory should not exist when stripping %d components", dir, tc.stripComponents)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestRestoreStripComponentsMetadata(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
testDir := filepath.Join(env.testdata, "testdir")
|
||||
rtest.OK(t, os.MkdirAll(testDir, 0755))
|
||||
rtest.OK(t, os.Chmod(testDir, 0777))
|
||||
|
||||
testFile := filepath.Join(testDir, "file.txt")
|
||||
rtest.OK(t, appendRandomData(testFile, 100))
|
||||
|
||||
opts := BackupOptions{}
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts)
|
||||
testRunCheck(t, env.gopts)
|
||||
|
||||
snapshotID := testListSnapshots(t, env.gopts, 1)[0]
|
||||
|
||||
restoredir := filepath.Join(env.base, "restore")
|
||||
testRunRestoreStripComponents(t, env.gopts, restoredir, snapshotID.String(), 1)
|
||||
|
||||
restoredTestDir := filepath.Join(restoredir, "testdir")
|
||||
fi, err := os.Stat(restoredTestDir)
|
||||
rtest.OK(t, err)
|
||||
rtest.Assert(t, fi.Mode().Perm() == 0777, "directory permissions should be restored, expected 0777, got %v", fi.Mode().Perm())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,17 @@ disk space. Note that the exact location of the holes can differ from those in
|
|||
the original file, as their location is determined while restoring and is not
|
||||
stored explicitly.
|
||||
|
||||
Much like ``tar`` restic has a ``--strip-components`` option to remove leading path elements
|
||||
when restoring files. For example, to restore everything below the ``work`` directory
|
||||
and place it directly into the target directory, use:
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /srv/restic-repo restore 79766175 --target /tmp/restore-work --strip-components 1 --include /work/foo
|
||||
enter password for repository:
|
||||
restoring <Snapshot of [/home/user/work] at 2015-05-08 21:40:19.884408621 +0200 CEST> to /tmp/restore-work
|
||||
|
||||
This will restore the file ``foo`` to ``/tmp/restore-work/foo``. Note that pathnames with fewer elements than specified will be silently skipped.
|
||||
|
||||
Restoring extended file attributes
|
||||
----------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ type Options struct {
|
|||
Overwrite OverwriteBehavior
|
||||
Delete bool
|
||||
OwnershipByName bool
|
||||
StripComponents int
|
||||
}
|
||||
|
||||
type OverwriteBehavior int
|
||||
|
|
@ -143,7 +144,7 @@ func (res *Restorer) traverseTree(ctx context.Context, target string, treeID res
|
|||
return err
|
||||
}
|
||||
}
|
||||
childFilenames, hasRestored, err := res.traverseTreeInner(ctx, target, location, treeID, visitor)
|
||||
childFilenames, hasRestored, err := res.traverseTreeInner(ctx, target, location, treeID, visitor, res.opts.StripComponents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -154,7 +155,7 @@ func (res *Restorer) traverseTree(ctx context.Context, target string, treeID res
|
|||
return err
|
||||
}
|
||||
|
||||
func (res *Restorer) traverseTreeInner(ctx context.Context, target, location string, treeID restic.ID, visitor treeVisitor) (filenames []string, hasRestored bool, err error) {
|
||||
func (res *Restorer) traverseTreeInner(ctx context.Context, target, location string, treeID restic.ID, visitor treeVisitor, stripComponents int) (filenames []string, hasRestored bool, err error) {
|
||||
debug.Log("%v %v %v", target, location, treeID)
|
||||
tree, err := data.LoadTree(ctx, res.repo, treeID)
|
||||
if err != nil {
|
||||
|
|
@ -215,6 +216,13 @@ func (res *Restorer) traverseTreeInner(ctx context.Context, target, location str
|
|||
selectedForRestore, childMayBeSelected := res.SelectFilter(nodeLocation, node.Type == data.NodeTypeDir)
|
||||
debug.Log("SelectFilter returned %v %v for %q", selectedForRestore, childMayBeSelected, nodeLocation)
|
||||
|
||||
skipNode := stripComponents > 0
|
||||
if skipNode {
|
||||
nodeTarget = target
|
||||
nodeLocation = location
|
||||
selectedForRestore = false
|
||||
}
|
||||
|
||||
if selectedForRestore {
|
||||
hasRestored = true
|
||||
}
|
||||
|
|
@ -237,7 +245,7 @@ func (res *Restorer) traverseTreeInner(ctx context.Context, target, location str
|
|||
var childFilenames []string
|
||||
|
||||
if childMayBeSelected {
|
||||
childFilenames, childHasRestored, err = res.traverseTreeInner(ctx, nodeTarget, nodeLocation, *node.Subtree, visitor)
|
||||
childFilenames, childHasRestored, err = res.traverseTreeInner(ctx, nodeTarget, nodeLocation, *node.Subtree, visitor, stripComponents-1)
|
||||
err = res.sanitizeError(nodeLocation, err)
|
||||
if err != nil {
|
||||
return nil, hasRestored, err
|
||||
|
|
@ -250,7 +258,7 @@ func (res *Restorer) traverseTreeInner(ctx context.Context, target, location str
|
|||
|
||||
// metadata need to be restore when leaving the directory in both cases
|
||||
// selected for restore or any child of any subtree have been restored
|
||||
if (selectedForRestore || childHasRestored) && visitor.leaveDir != nil {
|
||||
if (selectedForRestore || childHasRestored) && !skipNode && visitor.leaveDir != nil {
|
||||
err = res.sanitizeError(nodeLocation, visitor.leaveDir(node, nodeTarget, nodeLocation, childFilenames))
|
||||
if err != nil {
|
||||
return nil, hasRestored, err
|
||||
|
|
|
|||
Loading…
Reference in a new issue