2
2
mirror of https://github.com/octoleo/restic.git synced 2024-12-22 10:58:55 +00:00

vss: Implement VSS support for Windows

The VSS support works for 32 and 64-bit windows, this includes a check that
the restic version matches the OS architecture as required by VSS. The backup
operation will fail the user has not sufficient permissions to use VSS.

Snapshotting volumes also covers mountpoints but skips UNC paths.
This commit is contained in:
fgma 2020-10-24 11:35:57 +02:00 committed by Michael Eischer
parent 5fd3dbccb7
commit 5695f9ebd2
10 changed files with 1373 additions and 2 deletions

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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!

View File

@ -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:

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

188
internal/fs/fs_local_vss.go Normal file
View File

@ -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
}

49
internal/fs/vss.go Normal file
View File

@ -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 ""
}

1066
internal/fs/vss_windows.go Normal file

File diff suppressed because it is too large Load Diff