diff --git a/changelog/unreleased/issue-340 b/changelog/unreleased/issue-340 new file mode 100644 index 000000000..84c67f145 --- /dev/null +++ b/changelog/unreleased/issue-340 @@ -0,0 +1,12 @@ +Enhancement: Add support for Volume Shadow Copy Service (VSS) on Windows + +Volume Shadow Copy Service allows read access to files that are locked by +another process using an exclusive lock through a filesystem snapshot. Restic +was unable to backup those files before. This update enables backing up these +files. + +This needs to be enabled explicitely using the --use-fs-snapshot option of the +backup command. + +https://github.com/restic/restic/issues/340 +https://github.com/restic/restic/pull/2274 diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index a43c5a17a..3f9fece97 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -10,6 +10,7 @@ import ( "os" "path" "path/filepath" + "runtime" "strconv" "strings" "time" @@ -96,6 +97,7 @@ type BackupOptions struct { TimeStamp string WithAtime bool IgnoreInode bool + UseFsSnapshot bool } var backupOptions BackupOptions @@ -129,6 +131,9 @@ func init() { f.StringVar(&backupOptions.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)") f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories") f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number changes when checking for modified files") + if runtime.GOOS == "windows" { + f.BoolVar(&backupOptions.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)") + } } // filterExisting returns a slice of all existing items, or an error if no @@ -550,6 +555,25 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina } var targetFS fs.FS = fs.Local{} + if runtime.GOOS == "windows" && opts.UseFsSnapshot { + if !fs.HasSufficientPrivilegesForVSS() { + return errors.Fatal("user doesn't have sufficient privileges to use VSS snapshots\n") + } + + errorHandler := func(item string, err error) error { + return p.Error(item, nil, err) + } + + messageHandler := func(msg string, args ...interface{}) { + if !gopts.JSON { + p.P(msg, args...) + } + } + + localVss := fs.NewLocalVss(errorHandler, messageHandler) + defer localVss.DeleteSnapshots() + targetFS = localVss + } if opts.Stdin { if !gopts.JSON { p.V("read data from stdin") diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index ef578e589..71e03ae5f 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -21,6 +21,7 @@ import ( "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/filter" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" @@ -281,11 +282,21 @@ func testSetupBackupData(t testing.TB, env *testEnvironment) string { } func TestBackup(t *testing.T) { + testBackup(t, false) +} + +func TestBackupWithFilesystemSnapshots(t *testing.T) { + if runtime.GOOS == "windows" && fs.HasSufficientPrivilegesForVSS() { + testBackup(t, true) + } +} + +func testBackup(t *testing.T, useFsSnapshot bool) { env, cleanup := withTestEnvironment(t) defer cleanup() testSetupBackupData(t, env) - opts := BackupOptions{} + opts := BackupOptions{UseFsSnapshot: useFsSnapshot} // first backup testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) diff --git a/doc/040_backup.rst b/doc/040_backup.rst index 970038930..0cc982b0b 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -50,7 +50,24 @@ still get a nice live status display. Be aware that the live status shows the processed files and not the transferred data. Transferred volume might be lower (due to de-duplication) or higher. -If you run the command again, restic will create another snapshot of +On Windows, the ``--use-fs-snapshot`` option will use Windows' Volume Shadow Copy +Service (VSS) when creating backups. Restic will transparently create a VSS +snapshot for each volume that contains files to backup. Files are read from the +VSS snapshot instead of the regular filesystem. This allows to backup files that are +exclusively locked by another process during the backup. + +By default VSS ignores Outlook OST files. This is not a restriction of restic +but the default Windows VSS configuration. The files not to snapshot are +configured in the Windows registry under the following key: + +.. code-block:: console + + HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\BackupRestore\FilesNotToSnapshot + +For more details refer the official Windows documentation e.g. the article +``Registry Keys and Values for Backup and Restore``. + +If you run the backup command again, restic will create another snapshot of your data, but this time it's even faster and no new data was added to the repository (since all data is already there). This is de-duplication at work! diff --git a/doc/manual_rest.rst b/doc/manual_rest.rst index 14fa587a3..e29d914db 100644 --- a/doc/manual_rest.rst +++ b/doc/manual_rest.rst @@ -107,6 +107,7 @@ command: --stdin-filename filename filename to use when reading from stdin (default "stdin") --tag tag add a tag for the new snapshot (can be specified multiple times) --time time time of the backup (ex. '2012-11-01 22:08:41') (default: now) + --use-fs-snapshot use filesystem snapshot where possible (currently only Windows VSS) --with-atime store the atime for all files and directories Global Flags: diff --git a/go.mod b/go.mod index 308c823c5..efcda8cc8 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/dchest/siphash v1.2.2 github.com/dnaeon/go-vcr v1.0.1 // indirect github.com/elithrar/simple-scrypt v1.3.0 + github.com/go-ole/go-ole v1.2.4 github.com/google/go-cmp v0.5.2 github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/hashicorp/golang-lru v0.5.4 diff --git a/go.sum b/go.sum index 07347d24d..1503448bd 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/internal/fs/fs_local_vss.go b/internal/fs/fs_local_vss.go new file mode 100644 index 000000000..60e134626 --- /dev/null +++ b/internal/fs/fs_local_vss.go @@ -0,0 +1,188 @@ +package fs + +import ( + "os" + "path/filepath" + "strings" + "sync" + + "github.com/restic/restic/internal/errors" +) + +// ErrorHandler is used to report errors via callback +type ErrorHandler func(item string, err error) error + +// MessageHandler is used to report errors/messages via callbacks. +type MessageHandler func(msg string, args ...interface{}) + +// LocalVss is a wrapper around the local file system which uses windows volume +// shadow copy service (VSS) in a transparent way. +type LocalVss struct { + FS + snapshots map[string]VssSnapshot + failedSnapshots map[string]struct{} + mutex *sync.RWMutex + msgError ErrorHandler + msgMessage MessageHandler +} + +// statically ensure that LocalVss implements FS. +var _ FS = &LocalVss{} + +// NewLocalVss creates a new wrapper around the windows filesystem using volume +// shadow copy service to access locked files. +func NewLocalVss(msgError ErrorHandler, msgMessage MessageHandler) *LocalVss { + return &LocalVss{ + FS: Local{}, + snapshots: make(map[string]VssSnapshot), + failedSnapshots: make(map[string]struct{}), + mutex: &sync.RWMutex{}, + msgError: msgError, + msgMessage: msgMessage, + } +} + +// DeleteSnapshots deletes all snapshots that were created automatically. +func (fs *LocalVss) DeleteSnapshots() { + fs.mutex.Lock() + defer fs.mutex.Unlock() + + activeSnapshots := make(map[string]VssSnapshot) + + for volumeName, snapshot := range fs.snapshots { + if err := snapshot.Delete(); err != nil { + fs.msgError(volumeName, errors.Errorf("failed to delete VSS snapshot: %s", err)) + activeSnapshots[volumeName] = snapshot + } + } + + fs.snapshots = activeSnapshots +} + +// Open wraps the Open method of the underlying file system. +func (fs *LocalVss) Open(name string) (File, error) { + return os.Open(fs.snapshotPath(name)) +} + +// OpenFile wraps the Open method of the underlying file system. +func (fs *LocalVss) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + return os.OpenFile(fs.snapshotPath(name), flag, perm) +} + +// Stat wraps the Open method of the underlying file system. +func (fs *LocalVss) Stat(name string) (os.FileInfo, error) { + return os.Stat(fs.snapshotPath(name)) +} + +// Lstat wraps the Open method of the underlying file system. +func (fs *LocalVss) Lstat(name string) (os.FileInfo, error) { + return os.Lstat(fs.snapshotPath(name)) +} + +// snapshotPath returns the path inside a VSS snapshots if it already exists. +// If the path is not yet available as a snapshot, a snapshot is created. +// If creation of a snapshot fails the file's original path is returned as +// a fallback. +func (fs *LocalVss) snapshotPath(path string) string { + + fixPath := fixpath(path) + + if strings.HasPrefix(fixPath, `\\?\UNC\`) { + // UNC network shares are currently not supported so we access the regular file + // without snapshotting + // TODO: right now there is a problem in fixpath(): "\\host\share" is not returned as a UNC path + // "\\host\share\" is returned as a valid UNC path + return path + } + + fixPath = strings.TrimPrefix(fixpath(path), `\\?\`) + fixPathLower := strings.ToLower(fixPath) + volumeName := filepath.VolumeName(fixPath) + volumeNameLower := strings.ToLower(volumeName) + + fs.mutex.RLock() + + // ensure snapshot for volume exists + _, snapshotExists := fs.snapshots[volumeNameLower] + _, snapshotFailed := fs.failedSnapshots[volumeNameLower] + if !snapshotExists && !snapshotFailed { + fs.mutex.RUnlock() + fs.mutex.Lock() + defer fs.mutex.Unlock() + + _, snapshotExists = fs.snapshots[volumeNameLower] + _, snapshotFailed = fs.failedSnapshots[volumeNameLower] + + if !snapshotExists && !snapshotFailed { + vssVolume := volumeNameLower + string(filepath.Separator) + fs.msgMessage("creating VSS snapshot for [%s]\n", vssVolume) + + if snapshot, err := NewVssSnapshot(vssVolume, 120, fs.msgError); err != nil { + fs.msgError(vssVolume, errors.Errorf("failed to create snapshot for [%s]: %s\n", + vssVolume, err)) + fs.failedSnapshots[volumeNameLower] = struct{}{} + } else { + fs.snapshots[volumeNameLower] = snapshot + fs.msgMessage("successfully created snapshot for [%s]\n", vssVolume) + if len(snapshot.mountPointInfo) > 0 { + fs.msgMessage("mountpoints in snapshot volume [%s]:\n", vssVolume) + for mp, mpInfo := range snapshot.mountPointInfo { + info := "" + if !mpInfo.IsSnapshotted() { + info = " (not snapshotted)" + } + fs.msgMessage(" - %s%s\n", mp, info) + } + } + } + } + } else { + defer fs.mutex.RUnlock() + } + + var snapshotPath string + if snapshot, ok := fs.snapshots[volumeNameLower]; ok { + // handle case when data is inside mountpoint + for mountPoint, info := range snapshot.mountPointInfo { + if HasPathPrefix(mountPoint, fixPathLower) { + if !info.IsSnapshotted() { + // requested path is under mount point but mount point is + // not available as a snapshot (e.g. no filesystem support, + // removable media, etc.) + // -> try to backup without a snapshot + return path + } + + // filepath.rel() should always succeed because we checked that fixPath is either + // the same path or below mountPoint and operation is case-insensitive + relativeToMount, err := filepath.Rel(mountPoint, fixPath) + if err != nil { + panic(err) + } + + snapshotPath = fs.Join(info.GetSnapshotDeviceObject(), relativeToMount) + + if snapshotPath == info.GetSnapshotDeviceObject() { + snapshotPath += string(filepath.Separator) + } + + return snapshotPath + } + } + + // requested data is directly on the volume, not inside a mount point + snapshotPath = fs.Join(snapshot.GetSnapshotDeviceObject(), + strings.TrimPrefix(fixPath, volumeName)) + if snapshotPath == snapshot.GetSnapshotDeviceObject() { + snapshotPath = snapshotPath + string(filepath.Separator) + } + + } else { + // no snapshot is available for the requested path: + // -> try to backup without a snapshot + // TODO: log warning? + snapshotPath = path + } + + return snapshotPath +} diff --git a/internal/fs/vss.go b/internal/fs/vss.go new file mode 100644 index 000000000..a515d75b2 --- /dev/null +++ b/internal/fs/vss.go @@ -0,0 +1,49 @@ +// +build !windows + +package fs + +import ( + "github.com/restic/restic/internal/errors" +) + +// MountPoint is a dummy for non-windows platforms to let client code compile. +type MountPoint struct { +} + +// IsSnapshotted is true if this mount point was snapshotted successfully. +func (p *MountPoint) IsSnapshotted() bool { + return false +} + +// GetSnapshotDeviceObject returns root path to access the snapshot files and folders. +func (p *MountPoint) GetSnapshotDeviceObject() string { + return "" +} + +// VssSnapshot is a dummy for non-windows platforms to let client code compile. +type VssSnapshot struct { + mountPointInfo map[string]MountPoint +} + +// HasSufficientPrivilegesForVSS returns true if the user is allowed to use VSS. +func HasSufficientPrivilegesForVSS() bool { + return false +} + +// NewVssSnapshot creates a new vss snapshot. If creating the snapshots doesn't +// finish within the timeout an error is returned. +func NewVssSnapshot( + volume string, timeoutInSeconds uint, msgError ErrorHandler) (VssSnapshot, error) { + return VssSnapshot{}, errors.New("VSS snapshots are only supported on windows") +} + +// Delete deletes the created snapshot. +func (p *VssSnapshot) Delete() error { + return nil +} + +// GetSnapshotDeviceObject returns root path to access the snapshot files +// and folders. +func (p *VssSnapshot) GetSnapshotDeviceObject() string { + return "" +} diff --git a/internal/fs/vss_windows.go b/internal/fs/vss_windows.go new file mode 100644 index 000000000..b63ad4cd8 --- /dev/null +++ b/internal/fs/vss_windows.go @@ -0,0 +1,1066 @@ +// +build windows + +package fs + +import ( + "fmt" + "path/filepath" + "runtime" + "strings" + "syscall" + "unsafe" + + ole "github.com/go-ole/go-ole" + "github.com/restic/restic/internal/errors" + "golang.org/x/sys/windows" +) + +// HRESULT is a custom type for the windows api HRESULT type. +type HRESULT uint + +// HRESULT constant values necessary for using VSS api. +const ( + S_OK HRESULT = 0x00000000 + E_ACCESSDENIED HRESULT = 0x80070005 + E_OUTOFMEMORY HRESULT = 0x8007000E + E_INVALIDARG HRESULT = 0x80070057 + VSS_E_BAD_STATE HRESULT = 0x80042301 + VSS_E_UNEXPECTED HRESULT = 0x80042302 + VSS_E_PROVIDER_ALREADY_REGISTERED HRESULT = 0x80042303 + VSS_E_PROVIDER_NOT_REGISTERED HRESULT = 0x80042304 + VSS_E_PROVIDER_VETO HRESULT = 0x80042306 + VSS_E_PROVIDER_IN_USE HRESULT = 0x80042307 + VSS_E_OBJECT_NOT_FOUND HRESULT = 0x80042308 + VSS_E_VOLUME_NOT_SUPPORTED HRESULT = 0x8004230C + VSS_E_VOLUME_NOT_SUPPORTED_BY_PROVIDER HRESULT = 0x8004230E + VSS_E_OBJECT_ALREADY_EXISTS HRESULT = 0x8004230D + VSS_E_UNEXPECTED_PROVIDER_ERROR HRESULT = 0x8004230F + VSS_E_CORRUPT_XML_DOCUMENT HRESULT = 0x80042310 + VSS_E_INVALID_XML_DOCUMENT HRESULT = 0x80042311 + VSS_E_MAXIMUM_NUMBER_OF_VOLUMES_REACHED HRESULT = 0x80042312 + VSS_E_FLUSH_WRITES_TIMEOUT HRESULT = 0x80042313 + VSS_E_HOLD_WRITES_TIMEOUT HRESULT = 0x80042314 + VSS_E_UNEXPECTED_WRITER_ERROR HRESULT = 0x80042315 + VSS_E_SNAPSHOT_SET_IN_PROGRESS HRESULT = 0x80042316 + VSS_E_MAXIMUM_NUMBER_OF_SNAPSHOTS_REACHED HRESULT = 0x80042317 + VSS_E_WRITER_INFRASTRUCTURE HRESULT = 0x80042318 + VSS_E_WRITER_NOT_RESPONDING HRESULT = 0x80042319 + VSS_E_WRITER_ALREADY_SUBSCRIBED HRESULT = 0x8004231A + VSS_E_UNSUPPORTED_CONTEXT HRESULT = 0x8004231B + VSS_E_VOLUME_IN_USE HRESULT = 0x8004231D + VSS_E_MAXIMUM_DIFFAREA_ASSOCIATIONS_REACHED HRESULT = 0x8004231E + VSS_E_INSUFFICIENT_STORAGE HRESULT = 0x8004231F + VSS_E_NO_SNAPSHOTS_IMPORTED HRESULT = 0x80042320 + VSS_E_SOME_SNAPSHOTS_NOT_IMPORTED HRESULT = 0x80042321 + VSS_E_MAXIMUM_NUMBER_OF_REMOTE_MACHINES_REACHED HRESULT = 0x80042322 + VSS_E_REMOTE_SERVER_UNAVAILABLE HRESULT = 0x80042323 + VSS_E_REMOTE_SERVER_UNSUPPORTED HRESULT = 0x80042324 + VSS_E_REVERT_IN_PROGRESS HRESULT = 0x80042325 + VSS_E_REVERT_VOLUME_LOST HRESULT = 0x80042326 + VSS_E_REBOOT_REQUIRED HRESULT = 0x80042327 + VSS_E_TRANSACTION_FREEZE_TIMEOUT HRESULT = 0x80042328 + VSS_E_TRANSACTION_THAW_TIMEOUT HRESULT = 0x80042329 + VSS_E_VOLUME_NOT_LOCAL HRESULT = 0x8004232D + VSS_E_CLUSTER_TIMEOUT HRESULT = 0x8004232E + VSS_E_WRITERERROR_INCONSISTENTSNAPSHOT HRESULT = 0x800423F0 + VSS_E_WRITERERROR_OUTOFRESOURCES HRESULT = 0x800423F1 + VSS_E_WRITERERROR_TIMEOUT HRESULT = 0x800423F2 + VSS_E_WRITERERROR_RETRYABLE HRESULT = 0x800423F3 + VSS_E_WRITERERROR_NONRETRYABLE HRESULT = 0x800423F4 + VSS_E_WRITERERROR_RECOVERY_FAILED HRESULT = 0x800423F5 + VSS_E_BREAK_REVERT_ID_FAILED HRESULT = 0x800423F6 + VSS_E_LEGACY_PROVIDER HRESULT = 0x800423F7 + VSS_E_MISSING_DISK HRESULT = 0x800423F8 + VSS_E_MISSING_HIDDEN_VOLUME HRESULT = 0x800423F9 + VSS_E_MISSING_VOLUME HRESULT = 0x800423FA + VSS_E_AUTORECOVERY_FAILED HRESULT = 0x800423FB + VSS_E_DYNAMIC_DISK_ERROR HRESULT = 0x800423FC + VSS_E_NONTRANSPORTABLE_BCD HRESULT = 0x800423FD + VSS_E_CANNOT_REVERT_DISKID HRESULT = 0x800423FE + VSS_E_RESYNC_IN_PROGRESS HRESULT = 0x800423FF + VSS_E_CLUSTER_ERROR HRESULT = 0x80042400 + VSS_E_UNSELECTED_VOLUME HRESULT = 0x8004232A + VSS_E_SNAPSHOT_NOT_IN_SET HRESULT = 0x8004232B + VSS_E_NESTED_VOLUME_LIMIT HRESULT = 0x8004232C + VSS_E_NOT_SUPPORTED HRESULT = 0x8004232F + VSS_E_WRITERERROR_PARTIAL_FAILURE HRESULT = 0x80042336 + VSS_E_WRITER_STATUS_NOT_AVAILABLE HRESULT = 0x80042409 +) + +// hresultToString maps a HRESULT value to a human readable string. +var hresultToString = map[HRESULT]string{ + S_OK: "S_OK", + E_ACCESSDENIED: "E_ACCESSDENIED", + E_OUTOFMEMORY: "E_OUTOFMEMORY", + E_INVALIDARG: "E_INVALIDARG", + VSS_E_BAD_STATE: "VSS_E_BAD_STATE", + VSS_E_UNEXPECTED: "VSS_E_UNEXPECTED", + VSS_E_PROVIDER_ALREADY_REGISTERED: "VSS_E_PROVIDER_ALREADY_REGISTERED", + VSS_E_PROVIDER_NOT_REGISTERED: "VSS_E_PROVIDER_NOT_REGISTERED", + VSS_E_PROVIDER_VETO: "VSS_E_PROVIDER_VETO", + VSS_E_PROVIDER_IN_USE: "VSS_E_PROVIDER_IN_USE", + VSS_E_OBJECT_NOT_FOUND: "VSS_E_OBJECT_NOT_FOUND", + VSS_E_VOLUME_NOT_SUPPORTED: "VSS_E_VOLUME_NOT_SUPPORTED", + VSS_E_VOLUME_NOT_SUPPORTED_BY_PROVIDER: "VSS_E_VOLUME_NOT_SUPPORTED_BY_PROVIDER", + VSS_E_OBJECT_ALREADY_EXISTS: "VSS_E_OBJECT_ALREADY_EXISTS", + VSS_E_UNEXPECTED_PROVIDER_ERROR: "VSS_E_UNEXPECTED_PROVIDER_ERROR", + VSS_E_CORRUPT_XML_DOCUMENT: "VSS_E_CORRUPT_XML_DOCUMENT", + VSS_E_INVALID_XML_DOCUMENT: "VSS_E_INVALID_XML_DOCUMENT", + VSS_E_MAXIMUM_NUMBER_OF_VOLUMES_REACHED: "VSS_E_MAXIMUM_NUMBER_OF_VOLUMES_REACHED", + VSS_E_FLUSH_WRITES_TIMEOUT: "VSS_E_FLUSH_WRITES_TIMEOUT", + VSS_E_HOLD_WRITES_TIMEOUT: "VSS_E_HOLD_WRITES_TIMEOUT", + VSS_E_UNEXPECTED_WRITER_ERROR: "VSS_E_UNEXPECTED_WRITER_ERROR", + VSS_E_SNAPSHOT_SET_IN_PROGRESS: "VSS_E_SNAPSHOT_SET_IN_PROGRESS", + VSS_E_MAXIMUM_NUMBER_OF_SNAPSHOTS_REACHED: "VSS_E_MAXIMUM_NUMBER_OF_SNAPSHOTS_REACHED", + VSS_E_WRITER_INFRASTRUCTURE: "VSS_E_WRITER_INFRASTRUCTURE", + VSS_E_WRITER_NOT_RESPONDING: "VSS_E_WRITER_NOT_RESPONDING", + VSS_E_WRITER_ALREADY_SUBSCRIBED: "VSS_E_WRITER_ALREADY_SUBSCRIBED", + VSS_E_UNSUPPORTED_CONTEXT: "VSS_E_UNSUPPORTED_CONTEXT", + VSS_E_VOLUME_IN_USE: "VSS_E_VOLUME_IN_USE", + VSS_E_MAXIMUM_DIFFAREA_ASSOCIATIONS_REACHED: "VSS_E_MAXIMUM_DIFFAREA_ASSOCIATIONS_REACHED", + VSS_E_INSUFFICIENT_STORAGE: "VSS_E_INSUFFICIENT_STORAGE", + VSS_E_NO_SNAPSHOTS_IMPORTED: "VSS_E_NO_SNAPSHOTS_IMPORTED", + VSS_E_SOME_SNAPSHOTS_NOT_IMPORTED: "VSS_E_SOME_SNAPSHOTS_NOT_IMPORTED", + VSS_E_MAXIMUM_NUMBER_OF_REMOTE_MACHINES_REACHED: "VSS_E_MAXIMUM_NUMBER_OF_REMOTE_MACHINES_REACHED", + VSS_E_REMOTE_SERVER_UNAVAILABLE: "VSS_E_REMOTE_SERVER_UNAVAILABLE", + VSS_E_REMOTE_SERVER_UNSUPPORTED: "VSS_E_REMOTE_SERVER_UNSUPPORTED", + VSS_E_REVERT_IN_PROGRESS: "VSS_E_REVERT_IN_PROGRESS", + VSS_E_REVERT_VOLUME_LOST: "VSS_E_REVERT_VOLUME_LOST", + VSS_E_REBOOT_REQUIRED: "VSS_E_REBOOT_REQUIRED", + VSS_E_TRANSACTION_FREEZE_TIMEOUT: "VSS_E_TRANSACTION_FREEZE_TIMEOUT", + VSS_E_TRANSACTION_THAW_TIMEOUT: "VSS_E_TRANSACTION_THAW_TIMEOUT", + VSS_E_VOLUME_NOT_LOCAL: "VSS_E_VOLUME_NOT_LOCAL", + VSS_E_CLUSTER_TIMEOUT: "VSS_E_CLUSTER_TIMEOUT", + VSS_E_WRITERERROR_INCONSISTENTSNAPSHOT: "VSS_E_WRITERERROR_INCONSISTENTSNAPSHOT", + VSS_E_WRITERERROR_OUTOFRESOURCES: "VSS_E_WRITERERROR_OUTOFRESOURCES", + VSS_E_WRITERERROR_TIMEOUT: "VSS_E_WRITERERROR_TIMEOUT", + VSS_E_WRITERERROR_RETRYABLE: "VSS_E_WRITERERROR_RETRYABLE", + VSS_E_WRITERERROR_NONRETRYABLE: "VSS_E_WRITERERROR_NONRETRYABLE", + VSS_E_WRITERERROR_RECOVERY_FAILED: "VSS_E_WRITERERROR_RECOVERY_FAILED", + VSS_E_BREAK_REVERT_ID_FAILED: "VSS_E_BREAK_REVERT_ID_FAILED", + VSS_E_LEGACY_PROVIDER: "VSS_E_LEGACY_PROVIDER", + VSS_E_MISSING_DISK: "VSS_E_MISSING_DISK", + VSS_E_MISSING_HIDDEN_VOLUME: "VSS_E_MISSING_HIDDEN_VOLUME", + VSS_E_MISSING_VOLUME: "VSS_E_MISSING_VOLUME", + VSS_E_AUTORECOVERY_FAILED: "VSS_E_AUTORECOVERY_FAILED", + VSS_E_DYNAMIC_DISK_ERROR: "VSS_E_DYNAMIC_DISK_ERROR", + VSS_E_NONTRANSPORTABLE_BCD: "VSS_E_NONTRANSPORTABLE_BCD", + VSS_E_CANNOT_REVERT_DISKID: "VSS_E_CANNOT_REVERT_DISKID", + VSS_E_RESYNC_IN_PROGRESS: "VSS_E_RESYNC_IN_PROGRESS", + VSS_E_CLUSTER_ERROR: "VSS_E_CLUSTER_ERROR", + VSS_E_UNSELECTED_VOLUME: "VSS_E_UNSELECTED_VOLUME", + VSS_E_SNAPSHOT_NOT_IN_SET: "VSS_E_SNAPSHOT_NOT_IN_SET", + VSS_E_NESTED_VOLUME_LIMIT: "VSS_E_NESTED_VOLUME_LIMIT", + VSS_E_NOT_SUPPORTED: "VSS_E_NOT_SUPPORTED", + VSS_E_WRITERERROR_PARTIAL_FAILURE: "VSS_E_WRITERERROR_PARTIAL_FAILURE", + VSS_E_WRITER_STATUS_NOT_AVAILABLE: "VSS_E_WRITER_STATUS_NOT_AVAILABLE", +} + +// Str converts a HRESULT to a human readable string. +func (h HRESULT) Str() string { + if i, ok := hresultToString[h]; ok { + return i + } + + return "UNKNOWN" +} + +// VssError encapsulates errors retruned from calling VSS api. +type vssError struct { + text string + hresult HRESULT +} + +// NewVssError creates a new VSS api error. +func newVssError(text string, hresult HRESULT) error { + return &vssError{text: text, hresult: hresult} +} + +// NewVssError creates a new VSS api error. +func newVssErrorIfResultNotOK(text string, hresult HRESULT) error { + if hresult != S_OK { + return newVssError(text, hresult) + } + return nil +} + +// Error implements the error interface. +func (e *vssError) Error() string { + return fmt.Sprintf("VSS error: %s: %s (%#x)", e.text, e.hresult.Str(), e.hresult) +} + +// VssError encapsulates errors retruned from calling VSS api. +type vssTextError struct { + text string +} + +// NewVssTextError creates a new VSS api error. +func newVssTextError(text string) error { + return &vssTextError{text: text} +} + +// Error implements the error interface. +func (e *vssTextError) Error() string { + return fmt.Sprintf("VSS error: %s", e.text) +} + +// VssContext is a custom type for the windows api VssContext type. +type VssContext uint + +// VssContext constant values necessary for using VSS api. +const ( + VSS_CTX_BACKUP VssContext = iota + VSS_CTX_FILE_SHARE_BACKUP + VSS_CTX_NAS_ROLLBACK + VSS_CTX_APP_ROLLBACK + VSS_CTX_CLIENT_ACCESSIBLE + VSS_CTX_CLIENT_ACCESSIBLE_WRITERS + VSS_CTX_ALL +) + +// VssBackup is a custom type for the windows api VssBackup type. +type VssBackup uint + +// VssBackup constant values necessary for using VSS api. +const ( + VSS_BT_UNDEFINED VssBackup = iota + VSS_BT_FULL + VSS_BT_INCREMENTAL + VSS_BT_DIFFERENTIAL + VSS_BT_LOG + VSS_BT_COPY + VSS_BT_OTHER +) + +// VssObjectType is a custom type for the windows api VssObjectType type. +type VssObjectType uint + +// VssObjectType constant values necessary for using VSS api. +const ( + VSS_OBJECT_UNKNOWN VssObjectType = iota + VSS_OBJECT_NONE + VSS_OBJECT_SNAPSHOT_SET + VSS_OBJECT_SNAPSHOT + VSS_OBJECT_PROVIDER + VSS_OBJECT_TYPE_COUNT +) + +// UUID_IVSS defines the GUID of IVssBackupComponents. +var UUID_IVSS = ole.NewGUID("{665c1d5f-c218-414d-a05d-7fef5f9d5c86}") + +// IVssBackupComponents VSS api interface. +type IVssBackupComponents struct { + ole.IUnknown +} + +// IVssBackupComponentsVTable is the vtable for IVssBackupComponents. +type IVssBackupComponentsVTable struct { + ole.IUnknownVtbl + getWriterComponentsCount uintptr + getWriterComponents uintptr + initializeForBackup uintptr + setBackupState uintptr + initializeForRestore uintptr + setRestoreState uintptr + gatherWriterMetadata uintptr + getWriterMetadataCount uintptr + getWriterMetadata uintptr + freeWriterMetadata uintptr + addComponent uintptr + prepareForBackup uintptr + abortBackup uintptr + gatherWriterStatus uintptr + getWriterStatusCount uintptr + freeWriterStatus uintptr + getWriterStatus uintptr + setBackupSucceeded uintptr + setBackupOptions uintptr + setSelectedForRestore uintptr + setRestoreOptions uintptr + setAdditionalRestores uintptr + setPreviousBackupStamp uintptr + saveAsXML uintptr + backupComplete uintptr + addAlternativeLocationMapping uintptr + addRestoreSubcomponent uintptr + setFileRestoreStatus uintptr + addNewTarget uintptr + setRangesFilePath uintptr + preRestore uintptr + postRestore uintptr + setContext uintptr + startSnapshotSet uintptr + addToSnapshotSet uintptr + doSnapshotSet uintptr + deleteSnapshots uintptr + importSnapshots uintptr + breakSnapshotSet uintptr + getSnapshotProperties uintptr + query uintptr + isVolumeSupported uintptr + disableWriterClasses uintptr + enableWriterClasses uintptr + disableWriterInstances uintptr + exposeSnapshot uintptr + revertToSnapshot uintptr + queryRevertStatus uintptr +} + +// getVTable returns the vtable for IVssBackupComponents. +func (vss *IVssBackupComponents) getVTable() *IVssBackupComponentsVTable { + return (*IVssBackupComponentsVTable)(unsafe.Pointer(vss.RawVTable)) +} + +// AbortBackup calls the equivalent VSS api. +func (vss *IVssBackupComponents) AbortBackup() error { + result, _, _ := syscall.Syscall(vss.getVTable().abortBackup, 1, + uintptr(unsafe.Pointer(vss)), 0, 0) + + return newVssErrorIfResultNotOK("AbortBackup() failed", HRESULT(result)) +} + +// InitializeForBackup calls the equivalent VSS api. +func (vss *IVssBackupComponents) InitializeForBackup() error { + result, _, _ := syscall.Syscall(vss.getVTable().initializeForBackup, 2, + uintptr(unsafe.Pointer(vss)), 0, 0) + + return newVssErrorIfResultNotOK("InitializeForBackup() failed", HRESULT(result)) +} + +// SetContext calls the equivalent VSS api. +func (vss *IVssBackupComponents) SetContext(context VssContext) error { + result, _, _ := syscall.Syscall(vss.getVTable().setContext, 2, + uintptr(unsafe.Pointer(vss)), uintptr(context), 0) + + return newVssErrorIfResultNotOK("SetContext() failed", HRESULT(result)) +} + +// GatherWriterMetadata calls the equivalent VSS api. +func (vss *IVssBackupComponents) GatherWriterMetadata() (*IVSSAsync, error) { + var oleIUnknown *ole.IUnknown + result, _, _ := syscall.Syscall(vss.getVTable().gatherWriterMetadata, 2, + uintptr(unsafe.Pointer(vss)), uintptr(unsafe.Pointer(&oleIUnknown)), 0) + + err := newVssErrorIfResultNotOK("GatherWriterMetadata() failed", HRESULT(result)) + return vss.convertToVSSAsync(oleIUnknown, err) +} + +// convertToVSSAsync looks up IVSSAsync interface if given result +// is a success. +func (vss *IVssBackupComponents) convertToVSSAsync( + oleIUnknown *ole.IUnknown, err error) (*IVSSAsync, error) { + if err != nil { + return nil, err + } + + comInterface, err := queryInterface(oleIUnknown, UIID_IVSS_ASYNC) + if err != nil { + return nil, err + } + + iVssAsync := (*IVSSAsync)(unsafe.Pointer(comInterface)) + return iVssAsync, nil +} + +// IsVolumeSupported calls the equivalent VSS api. +func (vss *IVssBackupComponents) IsVolumeSupported(volumeName string) (bool, error) { + volumeNamePointer, err := syscall.UTF16PtrFromString(volumeName) + if err != nil { + panic(err) + } + + var isSupportedRaw uint32 + var result uintptr + + if runtime.GOARCH == "386" { + id := (*[4]uintptr)(unsafe.Pointer(ole.IID_NULL)) + + result, _, _ = syscall.Syscall9(vss.getVTable().isVolumeSupported, 7, + uintptr(unsafe.Pointer(vss)), id[0], id[1], id[2], id[3], + uintptr(unsafe.Pointer(volumeNamePointer)), uintptr(unsafe.Pointer(&isSupportedRaw)), 0, + 0) + } else { + result, _, _ = syscall.Syscall6(vss.getVTable().isVolumeSupported, 4, + uintptr(unsafe.Pointer(vss)), uintptr(unsafe.Pointer(ole.IID_NULL)), + uintptr(unsafe.Pointer(volumeNamePointer)), uintptr(unsafe.Pointer(&isSupportedRaw)), 0, + 0) + } + + var isSupported bool + if isSupportedRaw == 0 { + isSupported = false + } else { + isSupported = true + } + + return isSupported, newVssErrorIfResultNotOK("IsVolumeSupported() failed", HRESULT(result)) +} + +// StartSnapshotSet calls the equivalent VSS api. +func (vss *IVssBackupComponents) StartSnapshotSet() (ole.GUID, error) { + var snapshotSetID ole.GUID + result, _, _ := syscall.Syscall(vss.getVTable().startSnapshotSet, 2, + uintptr(unsafe.Pointer(vss)), uintptr(unsafe.Pointer(&snapshotSetID)), 0, + ) + + return snapshotSetID, newVssErrorIfResultNotOK("StartSnapshotSet() failed", HRESULT(result)) +} + +// AddToSnapshotSet calls the equivalent VSS api. +func (vss *IVssBackupComponents) AddToSnapshotSet(volumeName string, idSnapshot *ole.GUID) error { + volumeNamePointer, err := syscall.UTF16PtrFromString(volumeName) + if err != nil { + panic(err) + } + + var result uintptr = 0 + + if runtime.GOARCH == "386" { + id := (*[4]uintptr)(unsafe.Pointer(ole.IID_NULL)) + + result, _, _ = syscall.Syscall9(vss.getVTable().addToSnapshotSet, 7, + uintptr(unsafe.Pointer(vss)), uintptr(unsafe.Pointer(volumeNamePointer)), id[0], id[1], + id[2], id[3], uintptr(unsafe.Pointer(idSnapshot)), 0, 0) + } else { + result, _, _ = syscall.Syscall6(vss.getVTable().addToSnapshotSet, 4, + uintptr(unsafe.Pointer(vss)), uintptr(unsafe.Pointer(volumeNamePointer)), + uintptr(unsafe.Pointer(ole.IID_NULL)), uintptr(unsafe.Pointer(idSnapshot)), 0, 0) + } + + return newVssErrorIfResultNotOK("AddToSnapshotSet() failed", HRESULT(result)) +} + +// PrepareForBackup calls the equivalent VSS api. +func (vss *IVssBackupComponents) PrepareForBackup() (*IVSSAsync, error) { + var oleIUnknown *ole.IUnknown + result, _, _ := syscall.Syscall(vss.getVTable().prepareForBackup, 2, + uintptr(unsafe.Pointer(vss)), uintptr(unsafe.Pointer(&oleIUnknown)), 0) + + err := newVssErrorIfResultNotOK("PrepareForBackup() failed", HRESULT(result)) + return vss.convertToVSSAsync(oleIUnknown, err) +} + +// apiBoolToInt converts a bool for use calling the VSS api +func apiBoolToInt(input bool) uint { + if input { + return 1 + } + + return 0 +} + +// SetBackupState calls the equivalent VSS api. +func (vss *IVssBackupComponents) SetBackupState(selectComponents bool, + backupBootableSystemState bool, backupType VssBackup, partialFileSupport bool, +) error { + selectComponentsVal := apiBoolToInt(selectComponents) + backupBootableSystemStateVal := apiBoolToInt(backupBootableSystemState) + partialFileSupportVal := apiBoolToInt(partialFileSupport) + + result, _, _ := syscall.Syscall6(vss.getVTable().setBackupState, 5, + uintptr(unsafe.Pointer(vss)), uintptr(selectComponentsVal), + uintptr(backupBootableSystemStateVal), uintptr(backupType), uintptr(partialFileSupportVal), + 0) + + return newVssErrorIfResultNotOK("SetBackupState() failed", HRESULT(result)) +} + +// DoSnapshotSet calls the equivalent VSS api. +func (vss *IVssBackupComponents) DoSnapshotSet() (*IVSSAsync, error) { + var oleIUnknown *ole.IUnknown + result, _, _ := syscall.Syscall(vss.getVTable().doSnapshotSet, 2, uintptr(unsafe.Pointer(vss)), + uintptr(unsafe.Pointer(&oleIUnknown)), 0) + + err := newVssErrorIfResultNotOK("DoSnapshotSet() failed", HRESULT(result)) + return vss.convertToVSSAsync(oleIUnknown, err) +} + +// DeleteSnapshots calls the equivalent VSS api. +func (vss *IVssBackupComponents) DeleteSnapshots(snapshotID ole.GUID) (int32, ole.GUID, error) { + var deletedSnapshots int32 = 0 + var nondeletedSnapshotID ole.GUID + var result uintptr = 0 + + if runtime.GOARCH == "386" { + id := (*[4]uintptr)(unsafe.Pointer(ole.IID_NULL)) + + result, _, _ = syscall.Syscall9(vss.getVTable().deleteSnapshots, 9, + uintptr(unsafe.Pointer(vss)), id[0], id[1], id[2], id[3], + uintptr(VSS_OBJECT_SNAPSHOT), uintptr(1), uintptr(unsafe.Pointer(&deletedSnapshots)), + uintptr(unsafe.Pointer(&nondeletedSnapshotID)), + ) + } else { + result, _, _ = syscall.Syscall6(vss.getVTable().deleteSnapshots, 6, + uintptr(unsafe.Pointer(vss)), uintptr(unsafe.Pointer(&snapshotID)), + uintptr(VSS_OBJECT_SNAPSHOT), uintptr(1), uintptr(unsafe.Pointer(&deletedSnapshots)), + uintptr(unsafe.Pointer(&nondeletedSnapshotID))) + } + + err := newVssErrorIfResultNotOK("DeleteSnapshots() failed", HRESULT(result)) + return deletedSnapshots, nondeletedSnapshotID, err +} + +// GetSnapshotProperties calls the equivalent VSS api. +func (vss *IVssBackupComponents) GetSnapshotProperties(snapshotID ole.GUID, + properties *VssSnapshotProperties) error { + var result uintptr = 0 + + if runtime.GOARCH == "386" { + id := (*[4]uintptr)(unsafe.Pointer(ole.IID_NULL)) + + result, _, _ = syscall.Syscall6(vss.getVTable().getSnapshotProperties, 6, + uintptr(unsafe.Pointer(vss)), id[0], id[1], id[2], id[3], + uintptr(unsafe.Pointer(properties))) + } else { + result, _, _ = syscall.Syscall(vss.getVTable().getSnapshotProperties, 3, + uintptr(unsafe.Pointer(vss)), uintptr(unsafe.Pointer(&snapshotID)), + uintptr(unsafe.Pointer(properties))) + } + + return newVssErrorIfResultNotOK("GetSnapshotProperties() failed", HRESULT(result)) +} + +// vssFreeSnapshotProperties calls the equivalent VSS api. +func vssFreeSnapshotProperties(properties *VssSnapshotProperties) error { + proc, err := findVssProc("VssFreeSnapshotProperties") + if err != nil { + return err + } + + proc.Call(uintptr(unsafe.Pointer(properties))) + return nil +} + +// BackupComplete calls the equivalent VSS api. +func (vss *IVssBackupComponents) BackupComplete() (*IVSSAsync, error) { + var oleIUnknown *ole.IUnknown + result, _, _ := syscall.Syscall(vss.getVTable().backupComplete, 2, uintptr(unsafe.Pointer(vss)), + uintptr(unsafe.Pointer(&oleIUnknown)), 0) + + err := newVssErrorIfResultNotOK("BackupComplete() failed", HRESULT(result)) + return vss.convertToVSSAsync(oleIUnknown, err) +} + +// VssSnapshotProperties defines the properties of a VSS snapshot as part of the VSS api. +type VssSnapshotProperties struct { + snapshotID ole.GUID + snapshotSetID ole.GUID + snapshotsCount uint32 + snapshotDeviceObject *uint16 + originalVolumeName *uint16 + originatingMachine *uint16 + serviceMachine *uint16 + exposedName *uint16 + exposedPath *uint16 + providerID ole.GUID + snapshotAttributes uint32 + creationTimestamp uint64 + status uint +} + +// GetSnapshotDeviceObject returns root path to access the snapshot files +// and folders. +func (p *VssSnapshotProperties) GetSnapshotDeviceObject() string { + return ole.UTF16PtrToString(p.snapshotDeviceObject) +} + +// UIID_IVSS_ASYNC defines to GUID of IVSSAsync. +var UIID_IVSS_ASYNC = ole.NewGUID("{507C37B4-CF5B-4e95-B0AF-14EB9767467E}") + +// IVSSAsync VSS api interface. +type IVSSAsync struct { + ole.IUnknown +} + +// IVSSAsyncVTable is the vtable for IVSSAsync. +type IVSSAsyncVTable struct { + ole.IUnknownVtbl + cancel uintptr + wait uintptr + queryStatus uintptr +} + +// Constants for IVSSAsync api. +const ( + VSS_S_ASYNC_PENDING = 0x00042309 + VSS_S_ASYNC_FINISHED = 0x0004230A + VSS_S_ASYNC_CANCELLED = 0x0004230B +) + +// getVTable returns the vtable for IVSSAsync. +func (vssAsync *IVSSAsync) getVTable() *IVSSAsyncVTable { + return (*IVSSAsyncVTable)(unsafe.Pointer(vssAsync.RawVTable)) +} + +// Cancel calls the equivalent VSS api. +func (vssAsync *IVSSAsync) Cancel() HRESULT { + result, _, _ := syscall.Syscall(vssAsync.getVTable().cancel, 1, + uintptr(unsafe.Pointer(vssAsync)), 0, 0) + return HRESULT(result) +} + +// Wait calls the equivalent VSS api. +func (vssAsync *IVSSAsync) Wait(millis uint32) HRESULT { + result, _, _ := syscall.Syscall(vssAsync.getVTable().wait, 2, uintptr(unsafe.Pointer(vssAsync)), + uintptr(millis), 0) + return HRESULT(result) +} + +// QueryStatus calls the equivalent VSS api. +func (vssAsync *IVSSAsync) QueryStatus() (HRESULT, uint32) { + var state uint32 = 0 + result, _, _ := syscall.Syscall(vssAsync.getVTable().queryStatus, 3, + uintptr(unsafe.Pointer(vssAsync)), uintptr(unsafe.Pointer(&state)), 0) + return HRESULT(result), state +} + +// WaitUntilAsyncFinished waits until either the async call is finshed or +// the given timeout is reached. +func (vssAsync *IVSSAsync) WaitUntilAsyncFinished(millis uint32) error { + hresult := vssAsync.Wait(millis) + err := newVssErrorIfResultNotOK("Wait() failed", hresult) + if err != nil { + vssAsync.Cancel() + return err + } + + hresult, state := vssAsync.QueryStatus() + err = newVssErrorIfResultNotOK("QueryStatus() failed", hresult) + if err != nil { + vssAsync.Cancel() + return err + } + + if state == VSS_S_ASYNC_CANCELLED { + return newVssTextError("async operation cancelled") + } + + if state == VSS_S_ASYNC_PENDING { + vssAsync.Cancel() + return newVssTextError("async operation pending") + } + + if state != VSS_S_ASYNC_FINISHED { + err = newVssErrorIfResultNotOK("async operation failed", HRESULT(state)) + if err != nil { + return err + } + } + + return nil +} + +// MountPoint wraps all information of a snapshot of a mountpoint on a volume. +type MountPoint struct { + isSnapshotted bool + snapshotSetID ole.GUID + snapshotProperties VssSnapshotProperties + snapshotDeviceObject string +} + +// IsSnapshotted is true if this mount point was snapshotted successfully. +func (p *MountPoint) IsSnapshotted() bool { + return p.isSnapshotted +} + +// GetSnapshotDeviceObject returns root path to access the snapshot files and folders. +func (p *MountPoint) GetSnapshotDeviceObject() string { + return p.snapshotDeviceObject +} + +// VssSnapshot wraps windows volume shadow copy api (vss) via a simple +// interface to create and delete a vss snapshot. +type VssSnapshot struct { + iVssBackupComponents *IVssBackupComponents + snapshotID ole.GUID + snapshotProperties VssSnapshotProperties + snapshotDeviceObject string + mountPointInfo map[string]MountPoint + timeoutInMillis uint32 +} + +// GetSnapshotDeviceObject returns root path to access the snapshot files +// and folders. +func (p *VssSnapshot) GetSnapshotDeviceObject() string { + return p.snapshotDeviceObject +} + +// initializeCOMInterface initialize an instance of the VSS COM api +func initializeVssCOMInterface() (*ole.IUnknown, uintptr, error) { + vssInstance, err := loadIVssBackupComponentsConstructor() + if err != nil { + return nil, 0, err + } + + // ensure COM is initialized before use + ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) + + var oleIUnknown *ole.IUnknown + result, _, _ := vssInstance.Call(uintptr(unsafe.Pointer(&oleIUnknown))) + + return oleIUnknown, result, nil +} + +// HasSufficientPrivilegesForVSS returns true if the user is allowed to use VSS. +func HasSufficientPrivilegesForVSS() bool { + oleIUnknown, result, err := initializeVssCOMInterface() + if oleIUnknown != nil { + oleIUnknown.Release() + } + + if err != nil { + return false + } + + return !(HRESULT(result) == E_ACCESSDENIED) +} + +// NewVssSnapshot creates a new vss snapshot. If creating the snapshots doesn't +// finish within the timeout an error is returned. +func NewVssSnapshot( + volume string, timeoutInSeconds uint, msgError ErrorHandler) (VssSnapshot, error) { + is64Bit, err := isRunningOn64BitWindows() + + if err != nil { + return VssSnapshot{}, newVssTextError(fmt.Sprintf( + "Failed to detect windows architecture: %s", err.Error())) + } + + if (is64Bit && runtime.GOARCH != "amd64") || (!is64Bit && runtime.GOARCH != "386") { + return VssSnapshot{}, newVssTextError(fmt.Sprintf("executables compiled for %s can't use "+ + "VSS on other architectures. Please use an executable compiled for your platform.", + runtime.GOARCH)) + } + + timeoutInMillis := uint32(timeoutInSeconds * 1000) + + oleIUnknown, result, err := initializeVssCOMInterface() + if err != nil { + if oleIUnknown != nil { + oleIUnknown.Release() + } + return VssSnapshot{}, err + } + defer oleIUnknown.Release() + + switch HRESULT(result) { + case S_OK: + case E_ACCESSDENIED: + return VssSnapshot{}, newVssTextError(fmt.Sprintf("%s (%#x) The caller does not have "+ + "sufficient backup privileges or is not an administrator.", HRESULT(result).Str(), + result)) + default: + return VssSnapshot{}, newVssTextError(fmt.Sprintf("Failed to create VSS instance: %s (%#x)", + HRESULT(result).Str(), result)) + } + + comInterface, err := queryInterface(oleIUnknown, UUID_IVSS) + if err != nil { + return VssSnapshot{}, err + } + + /* + https://microsoft.public.win32.programmer.kernel.narkive.com/aObDj2dD/volume-shadow-copy-backupcomplete-and-vss-e-bad-state + + CreateVSSBackupComponents(); + InitializeForBackup(); + SetBackupState(); + GatherWriterMetadata(); + StartSnapshotSet(); + AddToSnapshotSet(); + PrepareForBackup(); + DoSnapshotSet(); + GetSnapshotProperties(); + + VssFreeSnapshotProperties(); + BackupComplete(); + */ + + iVssBackupComponents := (*IVssBackupComponents)(unsafe.Pointer(comInterface)) + + if err := iVssBackupComponents.InitializeForBackup(); err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + if err := iVssBackupComponents.SetContext(VSS_CTX_BACKUP); err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + // see https://techcommunity.microsoft.com/t5/Storage-at-Microsoft/What-is-the-difference-between-VSS-Full-Backup-and-VSS-Copy/ba-p/423575 + + if err := iVssBackupComponents.SetBackupState(false, false, VSS_BT_COPY, false); err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + err = callAsyncFunctionAndWait(iVssBackupComponents.GatherWriterMetadata, + "GatherWriterMetadata", timeoutInMillis) + if err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + if isSupported, err := iVssBackupComponents.IsVolumeSupported(volume); err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, err + } else if !isSupported { + iVssBackupComponents.Release() + return VssSnapshot{}, newVssTextError(fmt.Sprintf("Snapshots are not supported for volume "+ + "%s", volume)) + } + + snapshotSetID, err := iVssBackupComponents.StartSnapshotSet() + if err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + if err := iVssBackupComponents.AddToSnapshotSet(volume, &snapshotSetID); err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + mountPoints, err := enumerateMountedFolders(volume) + if err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, newVssTextError(fmt.Sprintf( + "failed to enumerate mount points for volume %s: %s", volume, err)) + } + + mountPointInfo := make(map[string]MountPoint) + + for _, mountPoint := range mountPoints { + // ensure every mountpoint is available even without a valid + // snapshot because we need to consider this when backing up files + mountPointInfo[mountPoint] = MountPoint{isSnapshotted: false} + + if isSupported, err := iVssBackupComponents.IsVolumeSupported(mountPoint); err != nil { + continue + } else if !isSupported { + continue + } + + var mountPointSnapshotSetID ole.GUID + err := iVssBackupComponents.AddToSnapshotSet(mountPoint, &mountPointSnapshotSetID) + if err != nil { + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + mountPointInfo[mountPoint] = MountPoint{isSnapshotted: true, + snapshotSetID: mountPointSnapshotSetID} + } + + err = callAsyncFunctionAndWait(iVssBackupComponents.PrepareForBackup, "PrepareForBackup", + timeoutInMillis) + if err != nil { + // After calling PrepareForBackup one needs to call AbortBackup() before releasing the VSS + // instance for proper cleanup. + // It is not neccessary to call BackupComplete before releasing the VSS instance afterwards. + iVssBackupComponents.AbortBackup() + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + err = callAsyncFunctionAndWait(iVssBackupComponents.DoSnapshotSet, "DoSnapshotSet", + timeoutInMillis) + if err != nil { + iVssBackupComponents.AbortBackup() + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + var snapshotProperties VssSnapshotProperties + err = iVssBackupComponents.GetSnapshotProperties(snapshotSetID, &snapshotProperties) + if err != nil { + iVssBackupComponents.AbortBackup() + iVssBackupComponents.Release() + return VssSnapshot{}, err + } + + for mountPoint, info := range mountPointInfo { + + if !info.isSnapshotted { + continue + } + + err := iVssBackupComponents.GetSnapshotProperties(info.snapshotSetID, + &info.snapshotProperties) + if err != nil { + msgError(mountPoint, errors.Errorf( + "VSS error: GetSnapshotProperties() for mount point %s returned error: ", + mountPoint, err)) + info.isSnapshotted = false + } else { + info.snapshotDeviceObject = info.snapshotProperties.GetSnapshotDeviceObject() + } + + mountPointInfo[mountPoint] = info + } + + return VssSnapshot{iVssBackupComponents, snapshotSetID, snapshotProperties, + snapshotProperties.GetSnapshotDeviceObject(), mountPointInfo, timeoutInMillis}, nil +} + +// Delete deletes the created snapshot. +func (p *VssSnapshot) Delete() error { + var err error + if err = vssFreeSnapshotProperties(&p.snapshotProperties); err != nil { + return err + } + + for _, mountPoint := range p.mountPointInfo { + if mountPoint.isSnapshotted { + if err = vssFreeSnapshotProperties(&mountPoint.snapshotProperties); err != nil { + return err + } + } + } + + if p.iVssBackupComponents != nil { + defer p.iVssBackupComponents.Release() + + err = callAsyncFunctionAndWait(p.iVssBackupComponents.BackupComplete, "BackupComplete", + p.timeoutInMillis) + if err != nil { + return err + } + + if _, _, e := p.iVssBackupComponents.DeleteSnapshots(p.snapshotID); e != nil { + err = newVssTextError(fmt.Sprintf("Failed to delete snapshot: %s", e.Error())) + p.iVssBackupComponents.AbortBackup() + if err != nil { + return err + } + } + } + + return nil +} + +// asyncCallFunc is the callback type for callAsyncFunctionAndWait. +type asyncCallFunc func() (*IVSSAsync, error) + +// callAsyncFunctionAndWait calls an async functions and waits for it to either +// finish or timeout. +func callAsyncFunctionAndWait(function asyncCallFunc, name string, timeoutInMillis uint32) error { + iVssAsync, err := function() + if err != nil { + return err + } + + if iVssAsync == nil { + return newVssTextError(fmt.Sprintf("%s() returned nil", name)) + } + + err = iVssAsync.WaitUntilAsyncFinished(timeoutInMillis) + iVssAsync.Release() + return err +} + +// loadIVssBackupComponentsConstructor finds the constructor of the VSS api +// inside the VSS dynamic library. +func loadIVssBackupComponentsConstructor() (*windows.LazyProc, error) { + createInstanceName := "?CreateVssBackupComponents@@YAJPEAPEAVIVssBackupComponents@@@Z" + + if runtime.GOARCH == "386" { + createInstanceName = "?CreateVssBackupComponents@@YGJPAPAVIVssBackupComponents@@@Z" + } + + return findVssProc(createInstanceName) +} + +// findVssProc find a function with the given name inside the VSS api +// dynamic library. +func findVssProc(procName string) (*windows.LazyProc, error) { + vssDll := windows.NewLazySystemDLL("VssApi.dll") + err := vssDll.Load() + if err != nil { + return &windows.LazyProc{}, err + } + + proc := vssDll.NewProc(procName) + err = proc.Find() + if err != nil { + return &windows.LazyProc{}, err + } + + return proc, nil +} + +// queryInterface is a wrapper around the windows QueryInterface api. +func queryInterface(oleIUnknown *ole.IUnknown, guid *ole.GUID) (*interface{}, error) { + var ivss *interface{} + + result, _, _ := syscall.Syscall(oleIUnknown.VTable().QueryInterface, 3, + uintptr(unsafe.Pointer(oleIUnknown)), uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(&ivss))) + if result != 0 { + return nil, newVssError("QueryInterface failed", HRESULT(result)) + } + + return ivss, nil +} + +// isRunningOn64BitWindows returns true if running on 64-bit windows. +func isRunningOn64BitWindows() (bool, error) { + if runtime.GOARCH == "amd64" { + return true, nil + } + + isWow64 := false + err := windows.IsWow64Process(windows.CurrentProcess(), &isWow64) + if err != nil { + return false, err + } + + return isWow64, nil +} + +// enumerateMountedFolders returns all mountpoints on the given volume. +func enumerateMountedFolders(volume string) ([]string, error) { + var mountedFolders []string + + volumeNamePointer, err := syscall.UTF16PtrFromString(volume) + if err != nil { + return mountedFolders, err + } + + volumeMountPointBuffer := make([]uint16, windows.MAX_LONG_PATH) + handle, err := windows.FindFirstVolumeMountPoint(volumeNamePointer, &volumeMountPointBuffer[0], + windows.MAX_LONG_PATH) + if err != nil { + // if there are no volumes an error is returned + return mountedFolders, nil + } + + defer windows.FindVolumeMountPointClose(handle) + + volumeMountPoint := syscall.UTF16ToString(volumeMountPointBuffer) + mountedFolders = append(mountedFolders, cleanupVolumeMountPoint(volume, volumeMountPoint)) + + for { + err = windows.FindNextVolumeMountPoint(handle, &volumeMountPointBuffer[0], + windows.MAX_LONG_PATH) + + if err != nil { + if err == syscall.ERROR_NO_MORE_FILES { + break + } + + return mountedFolders, + newVssTextError("FindNextVolumeMountPoint() failed: " + err.Error()) + } + + volumeMountPoint := syscall.UTF16ToString(volumeMountPointBuffer) + mountedFolders = append(mountedFolders, cleanupVolumeMountPoint(volume, volumeMountPoint)) + } + + return mountedFolders, nil +} + +func cleanupVolumeMountPoint(volume, mountPoint string) string { + return strings.ToLower(filepath.Join(volume, mountPoint) + string(filepath.Separator)) +}