backup: support specifying volume instead of path on Windows

"C:" (volume name) versus "C:\" (path)
This commit is contained in:
Michael Eischer 2024-08-30 11:25:51 +02:00
parent a0f2dfbc19
commit 4fcedb4bae
3 changed files with 85 additions and 1 deletions

View file

@ -0,0 +1,19 @@
Bugfix: Correctly handle passing volume name to `backup` command
On Windows, when the specified backup target only included the volume
name without a trailing slash, for example, `C:`, then restoring the
resulting snapshot would result in an error. Note that using `C:\`
as backup target worked correctly.
Specifying volume names now works correctly.
To restore snapshots created before this bugfix, use the `<snapshot>:<subpath>`
syntax. For a snapshot with ID `12345678` and a backup of `C:`, the following
command can be used:
```
restic restore 12345678:/C/C:./ --target output/folder
```
https://github.com/restic/restic/issues/2004
https://github.com/restic/restic/pull/5028

View file

@ -715,7 +715,12 @@ func resolveRelativeTargets(filesys fs.FS, targets []string) ([]string, error) {
debug.Log("targets before resolving: %v", targets) debug.Log("targets before resolving: %v", targets)
result := make([]string, 0, len(targets)) result := make([]string, 0, len(targets))
for _, target := range targets { for _, target := range targets {
target = filesys.Clean(target) if target != "" && filesys.VolumeName(target) == target {
// special case to allow users to also specify a volume name "C:" instead of a path "C:\"
target = target + filesys.Separator()
} else {
target = filesys.Clean(target)
}
pc, _ := pathComponents(filesys, target, false) pc, _ := pathComponents(filesys, target, false)
if len(pc) > 0 { if len(pc) > 0 {
result = append(result, target) result = append(result, target)

View file

@ -1448,6 +1448,66 @@ func TestArchiverSnapshot(t *testing.T) {
} }
} }
func TestResolveRelativeTargetsSpecial(t *testing.T) {
var tests = []struct {
name string
targets []string
expected []string
win bool
}{
{
name: "basic relative path",
targets: []string{filepath.FromSlash("some/path")},
expected: []string{filepath.FromSlash("some/path")},
},
{
name: "partial relative path",
targets: []string{filepath.FromSlash("../some/path")},
expected: []string{filepath.FromSlash("../some/path")},
},
{
name: "basic absolute path",
targets: []string{filepath.FromSlash("/some/path")},
expected: []string{filepath.FromSlash("/some/path")},
},
{
name: "volume name",
targets: []string{"C:"},
expected: []string{"C:\\"},
win: true,
},
{
name: "volume root path",
targets: []string{"C:\\"},
expected: []string{"C:\\"},
win: true,
},
{
name: "UNC path",
targets: []string{"\\\\server\\volume"},
expected: []string{"\\\\server\\volume\\"},
win: true,
},
{
name: "UNC path with trailing slash",
targets: []string{"\\\\server\\volume\\"},
expected: []string{"\\\\server\\volume\\"},
win: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.win && runtime.GOOS != "windows" {
t.Skip("skip test on unix")
}
targets, err := resolveRelativeTargets(&fs.Local{}, test.targets)
rtest.OK(t, err)
rtest.Equals(t, test.expected, targets)
})
}
}
func TestArchiverSnapshotSelect(t *testing.T) { func TestArchiverSnapshotSelect(t *testing.T) {
var tests = []struct { var tests = []struct {
name string name string