48dbefc37e
The actual implementation still relies on file paths, but with the abstraction layer in place, an FS implementation can ensure atomic file accesses in the future.
341 lines
8.3 KiB
Go
341 lines
8.3 KiB
Go
//go:build windows
|
|
// +build windows
|
|
|
|
package fs
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
ole "github.com/go-ole/go-ole"
|
|
"github.com/restic/restic/internal/options"
|
|
rtest "github.com/restic/restic/internal/test"
|
|
)
|
|
|
|
func matchStrings(ptrs []string, strs []string) bool {
|
|
if len(ptrs) != len(strs) {
|
|
return false
|
|
}
|
|
|
|
for i, p := range ptrs {
|
|
if p == "" {
|
|
return false
|
|
}
|
|
matched, err := regexp.MatchString(p, strs[i])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if !matched {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func matchMap(strs []string, m map[string]struct{}) bool {
|
|
if len(strs) != len(m) {
|
|
return false
|
|
}
|
|
|
|
for _, s := range strs {
|
|
if _, ok := m[s]; !ok {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func TestVSSConfig(t *testing.T) {
|
|
type config struct {
|
|
excludeAllMountPoints bool
|
|
timeout time.Duration
|
|
provider string
|
|
}
|
|
setTests := []struct {
|
|
input options.Options
|
|
output config
|
|
}{
|
|
{
|
|
options.Options{
|
|
"vss.timeout": "6h38m42s",
|
|
"vss.provider": "Ms",
|
|
},
|
|
config{
|
|
timeout: 23922000000000,
|
|
provider: "Ms",
|
|
},
|
|
},
|
|
{
|
|
options.Options{
|
|
"vss.exclude-all-mount-points": "t",
|
|
"vss.provider": "{b5946137-7b9f-4925-af80-51abd60b20d5}",
|
|
},
|
|
config{
|
|
excludeAllMountPoints: true,
|
|
timeout: 120000000000,
|
|
provider: "{b5946137-7b9f-4925-af80-51abd60b20d5}",
|
|
},
|
|
},
|
|
{
|
|
options.Options{
|
|
"vss.exclude-all-mount-points": "0",
|
|
"vss.exclude-volumes": "",
|
|
"vss.timeout": "120s",
|
|
"vss.provider": "Microsoft Software Shadow Copy provider 1.0",
|
|
},
|
|
config{
|
|
timeout: 120000000000,
|
|
provider: "Microsoft Software Shadow Copy provider 1.0",
|
|
},
|
|
},
|
|
}
|
|
for i, test := range setTests {
|
|
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
|
cfg, err := ParseVSSConfig(test.input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
errorHandler := func(item string, err error) {
|
|
t.Fatalf("unexpected error (%v)", err)
|
|
}
|
|
messageHandler := func(msg string, args ...interface{}) {
|
|
t.Fatalf("unexpected message (%s)", fmt.Sprintf(msg, args))
|
|
}
|
|
|
|
dst := NewLocalVss(errorHandler, messageHandler, cfg)
|
|
|
|
if dst.excludeAllMountPoints != test.output.excludeAllMountPoints ||
|
|
dst.excludeVolumes != nil || dst.timeout != test.output.timeout ||
|
|
dst.provider != test.output.provider {
|
|
t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, dst)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseMountPoints(t *testing.T) {
|
|
volumeMatch := regexp.MustCompile(`^\\\\\?\\Volume\{[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\}\\$`)
|
|
|
|
// It's not a good idea to test functions based on getVolumeNameForVolumeMountPoint by calling
|
|
// getVolumeNameForVolumeMountPoint itself, but we have restricted test environment:
|
|
// cannot manage volumes and can only be sure that the mount point C:\ exists
|
|
sysVolume, err := getVolumeNameForVolumeMountPoint("C:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// We don't know a valid volume GUID path for c:\, but we'll at least check its format
|
|
if !volumeMatch.MatchString(sysVolume) {
|
|
t.Fatalf("invalid volume GUID path: %s", sysVolume)
|
|
}
|
|
// Changing the case and removing trailing backslash allows tests
|
|
// the equality of different ways of writing a volume name
|
|
sysVolumeMutated := strings.ToUpper(sysVolume[:len(sysVolume)-1])
|
|
sysVolumeMatch := strings.ToLower(sysVolume)
|
|
|
|
type check struct {
|
|
volume string
|
|
result bool
|
|
}
|
|
setTests := []struct {
|
|
input options.Options
|
|
output []string
|
|
checks []check
|
|
errors []string
|
|
}{
|
|
{
|
|
options.Options{
|
|
"vss.exclude-volumes": `c:;c:\;` + sysVolume + `;` + sysVolumeMutated,
|
|
},
|
|
[]string{
|
|
sysVolumeMatch,
|
|
},
|
|
[]check{
|
|
{`c:\`, false},
|
|
{`c:`, false},
|
|
{sysVolume, false},
|
|
{sysVolumeMutated, false},
|
|
},
|
|
[]string{},
|
|
},
|
|
{
|
|
options.Options{
|
|
"vss.exclude-volumes": `z:\nonexistent;c:;c:\windows\;\\?\Volume{39b9cac2-bcdb-4d51-97c8-0d0677d607fb}\`,
|
|
},
|
|
[]string{
|
|
sysVolumeMatch,
|
|
},
|
|
[]check{
|
|
{`c:\windows\`, true},
|
|
{`\\?\Volume{39b9cac2-bcdb-4d51-97c8-0d0677d607fb}\`, true},
|
|
{`c:`, false},
|
|
{``, true},
|
|
},
|
|
[]string{
|
|
`failed to parse vss\.exclude-volumes \[z:\\nonexistent\]:.*`,
|
|
`failed to parse vss\.exclude-volumes \[c:\\windows\\\]:.*`,
|
|
`failed to parse vss\.exclude-volumes \[\\\\\?\\Volume\{39b9cac2-bcdb-4d51-97c8-0d0677d607fb\}\\\]:.*`,
|
|
`failed to get volume from mount point \[c:\\windows\\\]:.*`,
|
|
`failed to get volume from mount point \[\\\\\?\\Volume\{39b9cac2-bcdb-4d51-97c8-0d0677d607fb\}\\\]:.*`,
|
|
`failed to get volume from mount point \[\]:.*`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, test := range setTests {
|
|
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
|
cfg, err := ParseVSSConfig(test.input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var log []string
|
|
errorHandler := func(item string, err error) {
|
|
log = append(log, strings.TrimSpace(err.Error()))
|
|
}
|
|
messageHandler := func(msg string, args ...interface{}) {
|
|
t.Fatalf("unexpected message (%s)", fmt.Sprintf(msg, args))
|
|
}
|
|
|
|
dst := NewLocalVss(errorHandler, messageHandler, cfg)
|
|
|
|
if !matchMap(test.output, dst.excludeVolumes) {
|
|
t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v",
|
|
test.output, dst.excludeVolumes)
|
|
}
|
|
|
|
for _, c := range test.checks {
|
|
if dst.isMountPointIncluded(c.volume) != c.result {
|
|
t.Fatalf(`wrong check: isMountPointIncluded("%s") != %v`, c.volume, c.result)
|
|
}
|
|
}
|
|
|
|
if !matchStrings(test.errors, log) {
|
|
t.Fatalf("wrong log, want:\n %#v\ngot:\n %#v", test.errors, log)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseProvider(t *testing.T) {
|
|
msProvider := ole.NewGUID("{b5946137-7b9f-4925-af80-51abd60b20d5}")
|
|
setTests := []struct {
|
|
provider string
|
|
id *ole.GUID
|
|
result string
|
|
}{
|
|
{
|
|
"",
|
|
ole.IID_NULL,
|
|
"",
|
|
},
|
|
{
|
|
"mS",
|
|
msProvider,
|
|
"",
|
|
},
|
|
{
|
|
"{B5946137-7b9f-4925-Af80-51abD60b20d5}",
|
|
msProvider,
|
|
"",
|
|
},
|
|
{
|
|
"Microsoft Software Shadow Copy provider 1.0",
|
|
msProvider,
|
|
"",
|
|
},
|
|
{
|
|
"{04560982-3d7d-4bbc-84f7-0712f833a28f}",
|
|
nil,
|
|
`invalid VSS provider "{04560982-3d7d-4bbc-84f7-0712f833a28f}"`,
|
|
},
|
|
{
|
|
"non-existent provider",
|
|
nil,
|
|
`invalid VSS provider "non-existent provider"`,
|
|
},
|
|
}
|
|
|
|
_ = ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
|
|
|
|
for i, test := range setTests {
|
|
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
|
|
id, err := getProviderID(test.provider)
|
|
|
|
if err != nil && id != nil {
|
|
t.Fatalf("err!=nil but id=%v", id)
|
|
}
|
|
|
|
if test.result != "" || err != nil {
|
|
var result string
|
|
if err != nil {
|
|
result = err.Error()
|
|
}
|
|
if test.result != result || test.result == "" {
|
|
t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.result, result)
|
|
}
|
|
} else if !ole.IsEqualGUID(id, test.id) {
|
|
t.Fatalf("wrong id, want:\n %s\ngot:\n %s", test.id.String(), id.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVSSFS(t *testing.T) {
|
|
if runtime.GOOS != "windows" || HasSufficientPrivilegesForVSS() != nil {
|
|
t.Skip("vss fs test can only be run on windows with admin privileges")
|
|
}
|
|
|
|
cfg, err := ParseVSSConfig(options.Options{})
|
|
rtest.OK(t, err)
|
|
|
|
errorHandler := func(item string, err error) {
|
|
t.Fatalf("unexpected error (%v)", err)
|
|
}
|
|
messageHandler := func(msg string, args ...interface{}) {
|
|
if strings.HasPrefix(msg, "creating VSS snapshot for") || strings.HasPrefix(msg, "successfully created snapshot") {
|
|
return
|
|
}
|
|
t.Fatalf("unexpected message (%s)", fmt.Sprintf(msg, args))
|
|
}
|
|
|
|
localVss := NewLocalVss(errorHandler, messageHandler, cfg)
|
|
defer localVss.DeleteSnapshots()
|
|
|
|
tempdir := t.TempDir()
|
|
tempfile := filepath.Join(tempdir, "file")
|
|
rtest.OK(t, os.WriteFile(tempfile, []byte("example"), 0o600))
|
|
|
|
// trigger snapshot creation and
|
|
// capture FI while file still exists (should already be within the snapshot)
|
|
origFi, err := localVss.Lstat(tempfile)
|
|
rtest.OK(t, err)
|
|
|
|
// remove original file
|
|
rtest.OK(t, os.Remove(tempfile))
|
|
|
|
lstatFi, err := localVss.Lstat(tempfile)
|
|
rtest.OK(t, err)
|
|
rtest.Equals(t, origFi.Mode(), lstatFi.Mode())
|
|
|
|
f, err := localVss.OpenFile(tempfile, os.O_RDONLY, false)
|
|
rtest.OK(t, err)
|
|
data, err := io.ReadAll(f)
|
|
rtest.OK(t, err)
|
|
rtest.Equals(t, "example", string(data), "unexpected file content")
|
|
|
|
node, err := f.ToNode(false)
|
|
rtest.OK(t, err)
|
|
rtest.Equals(t, node.Mode, lstatFi.Mode())
|
|
|
|
rtest.OK(t, f.Close())
|
|
}
|