mirror of
https://github.com/restic/restic.git
synced 2025-08-12 11:27:46 +00:00
Merge pull request #2274 from fgma/master
Support for Volume Shadow Copy Service (VSS) on windows
This commit is contained in:
188
internal/fs/fs_local_vss.go
Normal file
188
internal/fs/fs_local_vss.go
Normal 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
49
internal/fs/vss.go
Normal 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
1066
internal/fs/vss_windows.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user