forked from TrueCloudLab/restic
5695f9ebd2
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.
188 lines
5.8 KiB
Go
188 lines
5.8 KiB
Go
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
|
|
}
|