forked from TrueCloudLab/restic
Merge pull request #5110 from MichaelEischer/fix-vss-root-volume-patch
Fix VSS metadata error (v0.17.2)
This commit is contained in:
commit
62222edc4a
8 changed files with 76 additions and 16 deletions
15
changelog/unreleased/issue-5107
Normal file
15
changelog/unreleased/issue-5107
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
Bugfix: Fix metadata error on Windows for backups using VSS
|
||||||
|
|
||||||
|
Since restic 0.17.2, when creating a backup on Windows using `--use-fs-snapshot`,
|
||||||
|
restic would report an error like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: incomplete metadata for C:\: get EA failed while opening file handle for path \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyXX\, with: The process cannot access the file because it is being used by another process.
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed by correctly handling paths that refer to volume
|
||||||
|
shadow copy snapshots.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5107
|
||||||
|
https://github.com/restic/restic/pull/5110
|
||||||
|
https://github.com/restic/restic/pull/5112
|
|
@ -52,14 +52,14 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
||||||
opts := BackupOptions{UseFsSnapshot: useFsSnapshot}
|
opts := BackupOptions{UseFsSnapshot: useFsSnapshot}
|
||||||
|
|
||||||
// first backup
|
// first backup
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
stat1 := dirStats(env.repo)
|
stat1 := dirStats(env.repo)
|
||||||
|
|
||||||
// second backup, implicit incremental
|
// second backup, implicit incremental
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
snapshotIDs := testListSnapshots(t, env.gopts, 2)
|
snapshotIDs := testListSnapshots(t, env.gopts, 2)
|
||||||
|
|
||||||
stat2 := dirStats(env.repo)
|
stat2 := dirStats(env.repo)
|
||||||
|
@ -71,7 +71,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
// third backup, explicit incremental
|
// third backup, explicit incremental
|
||||||
opts.Parent = snapshotIDs[0].String()
|
opts.Parent = snapshotIDs[0].String()
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
snapshotIDs = testListSnapshots(t, env.gopts, 3)
|
snapshotIDs = testListSnapshots(t, env.gopts, 3)
|
||||||
|
|
||||||
stat3 := dirStats(env.repo)
|
stat3 := dirStats(env.repo)
|
||||||
|
@ -84,7 +84,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
testRunRestore(t, env.gopts, restoredir, snapshotID.String()+":"+toPathInSnapshot(filepath.Dir(env.testdata)))
|
||||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
||||||
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
|
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,20 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toPathInSnapshot(path string) string {
|
||||||
|
// use path as is on most platforms, but convert it on windows
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// the path generated by the test is always local so take the shortcut
|
||||||
|
vol := filepath.VolumeName(path)
|
||||||
|
if vol[len(vol)-1] != ':' {
|
||||||
|
panic(fmt.Sprintf("unexpected path: %q", path))
|
||||||
|
}
|
||||||
|
path = vol[:len(vol)-1] + string(filepath.Separator) + path[len(vol)+1:]
|
||||||
|
path = filepath.ToSlash(path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
func TestBackupWithRelativePath(t *testing.T) {
|
func TestBackupWithRelativePath(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
@ -557,7 +571,7 @@ func TestHardLink(t *testing.T) {
|
||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
testRunRestore(t, env.gopts, restoredir, snapshotID.String())
|
||||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
||||||
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
||||||
|
|
||||||
|
|
|
@ -62,11 +62,11 @@ func TestCopy(t *testing.T) {
|
||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
origRestores[restoredir] = struct{}{}
|
origRestores[restoredir] = struct{}{}
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
testRunRestore(t, env.gopts, restoredir, snapshotID.String())
|
||||||
}
|
}
|
||||||
for i, snapshotID := range copiedSnapshotIDs {
|
for i, snapshotID := range copiedSnapshotIDs {
|
||||||
restoredir := filepath.Join(env2.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env2.base, fmt.Sprintf("restore%d", i))
|
||||||
testRunRestore(t, env2.gopts, restoredir, snapshotID)
|
testRunRestore(t, env2.gopts, restoredir, snapshotID.String())
|
||||||
foundMatch := false
|
foundMatch := false
|
||||||
for cmpdir := range origRestores {
|
for cmpdir := range origRestores {
|
||||||
diff := directoriesContentsDiff(restoredir, cmpdir)
|
diff := directoriesContentsDiff(restoredir, cmpdir)
|
||||||
|
|
|
@ -18,17 +18,17 @@ import (
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui/termstatus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID restic.ID) {
|
func testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID string) {
|
||||||
testRunRestoreExcludes(t, opts, dir, snapshotID, nil)
|
testRunRestoreExcludes(t, opts, dir, snapshotID, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, excludes []string) {
|
func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID string, excludes []string) {
|
||||||
opts := RestoreOptions{
|
opts := RestoreOptions{
|
||||||
Target: dir,
|
Target: dir,
|
||||||
}
|
}
|
||||||
opts.Excludes = excludes
|
opts.Excludes = excludes
|
||||||
|
|
||||||
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID.String(), opts, gopts))
|
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID, opts, gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunRestoreAssumeFailure(snapshotID string, opts RestoreOptions, gopts GlobalOptions) error {
|
func testRunRestoreAssumeFailure(snapshotID string, opts RestoreOptions, gopts GlobalOptions) error {
|
||||||
|
@ -198,7 +198,7 @@ func TestRestoreFilter(t *testing.T) {
|
||||||
snapshotID := testListSnapshots(t, env.gopts, 1)[0]
|
snapshotID := testListSnapshots(t, env.gopts, 1)[0]
|
||||||
|
|
||||||
// no restore filter should restore all files
|
// no restore filter should restore all files
|
||||||
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore0"), snapshotID)
|
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore0"), snapshotID.String())
|
||||||
for _, testFile := range testfiles {
|
for _, testFile := range testfiles {
|
||||||
rtest.OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", testFile.name), int64(testFile.size)))
|
rtest.OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", testFile.name), int64(testFile.size)))
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,7 @@ func TestRestoreFilter(t *testing.T) {
|
||||||
|
|
||||||
// restore with excludes
|
// restore with excludes
|
||||||
restoredir := filepath.Join(env.base, "restore-with-excludes")
|
restoredir := filepath.Join(env.base, "restore-with-excludes")
|
||||||
testRunRestoreExcludes(t, env.gopts, restoredir, snapshotID, excludePatterns)
|
testRunRestoreExcludes(t, env.gopts, restoredir, snapshotID.String(), excludePatterns)
|
||||||
testRestoredFileExclusions(t, restoredir)
|
testRestoredFileExclusions(t, restoredir)
|
||||||
|
|
||||||
// Create an exclude file with some patterns
|
// Create an exclude file with some patterns
|
||||||
|
@ -340,7 +340,7 @@ func TestRestoreWithPermissionFailure(t *testing.T) {
|
||||||
|
|
||||||
_ = withRestoreGlobalOptions(func() error {
|
_ = withRestoreGlobalOptions(func() error {
|
||||||
globalOptions.stderr = io.Discard
|
globalOptions.stderr = io.Discard
|
||||||
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0])
|
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0].String())
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ func TestCheckRestoreNoLock(t *testing.T) {
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
|
|
||||||
snapshotIDs := testListSnapshots(t, env.gopts, 4)
|
snapshotIDs := testListSnapshots(t, env.gopts, 4)
|
||||||
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshotIDs[0])
|
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshotIDs[0].String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// a listOnceBackend only allows listing once per filetype
|
// a listOnceBackend only allows listing once per filetype
|
||||||
|
|
|
@ -20,6 +20,15 @@ func fixpath(name string) string {
|
||||||
if strings.HasPrefix(abspath, `\\?\UNC\`) {
|
if strings.HasPrefix(abspath, `\\?\UNC\`) {
|
||||||
return abspath
|
return abspath
|
||||||
}
|
}
|
||||||
|
// Check if \\?\GLOBALROOT exists which marks volume shadow copy snapshots
|
||||||
|
if strings.HasPrefix(abspath, `\\?\GLOBALROOT\`) {
|
||||||
|
if strings.Count(abspath, `\`) == 5 {
|
||||||
|
// Append slash if this just a volume name, e.g. `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyXX`
|
||||||
|
// Without the trailing slash any access to the volume itself will fail.
|
||||||
|
return abspath + string(filepath.Separator)
|
||||||
|
}
|
||||||
|
return abspath
|
||||||
|
}
|
||||||
// Check if \\?\ already exist
|
// Check if \\?\ already exist
|
||||||
if strings.HasPrefix(abspath, `\\?\`) {
|
if strings.HasPrefix(abspath, `\\?\`) {
|
||||||
return abspath
|
return abspath
|
||||||
|
|
|
@ -372,8 +372,11 @@ func (node *Node) fillGenericAttributes(path string, fi os.FileInfo, stat *statT
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(filepath.Clean(path), `\`) {
|
isVolume, err := isVolumePath(path)
|
||||||
// filepath.Clean(path) ends with '\' for Windows root volume paths only
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if isVolume {
|
||||||
// Do not process file attributes, created time and sd for windows root volume paths
|
// Do not process file attributes, created time and sd for windows root volume paths
|
||||||
// Security descriptors are not supported for root volume paths.
|
// Security descriptors are not supported for root volume paths.
|
||||||
// Though file attributes and created time are supported for root volume paths,
|
// Though file attributes and created time are supported for root volume paths,
|
||||||
|
@ -464,6 +467,18 @@ func checkAndStoreEASupport(path string) (isEASupportedVolume bool, err error) {
|
||||||
return isEASupportedVolume, err
|
return isEASupportedVolume, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isVolumePath returns whether a path refers to a volume
|
||||||
|
func isVolumePath(path string) (bool, error) {
|
||||||
|
volName, err := prepareVolumeName(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath := filepath.Clean(path)
|
||||||
|
cleanVolume := filepath.Clean(volName + `\`)
|
||||||
|
return cleanPath == cleanVolume, nil
|
||||||
|
}
|
||||||
|
|
||||||
// prepareVolumeName prepares the volume name for different cases in Windows
|
// prepareVolumeName prepares the volume name for different cases in Windows
|
||||||
func prepareVolumeName(path string) (volumeName string, err error) {
|
func prepareVolumeName(path string) (volumeName string, err error) {
|
||||||
// Check if it's an extended length path
|
// Check if it's an extended length path
|
||||||
|
|
|
@ -450,6 +450,13 @@ func TestPrepareVolumeName(t *testing.T) {
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectedEASupported: false,
|
expectedEASupported: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Volume Shadow Copy root",
|
||||||
|
path: `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1`,
|
||||||
|
expectedVolume: `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1`,
|
||||||
|
expectError: false,
|
||||||
|
expectedEASupported: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Volume Shadow Copy path",
|
name: "Volume Shadow Copy path",
|
||||||
path: `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Users\test`,
|
path: `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Users\test`,
|
||||||
|
|
Loading…
Reference in a new issue