forked from TrueCloudLab/restic
vss: Add volume filtering
Add options to exclude all mountpoints and arbitrary volumes from snapshotting.
This commit is contained in:
parent
7470e5356e
commit
c4f67c0064
3 changed files with 148 additions and 51 deletions
|
@ -14,7 +14,9 @@ import (
|
||||||
|
|
||||||
// VSSConfig holds extended options of windows volume shadow copy service.
|
// VSSConfig holds extended options of windows volume shadow copy service.
|
||||||
type VSSConfig struct {
|
type VSSConfig struct {
|
||||||
Timeout time.Duration `option:"timeout" help:"time that the VSS can spend creating snapshots before timing out"`
|
ExcludeAllMountPoints bool `option:"excludeallmountpoints" help:"exclude mountpoints from snapshotting on all volumes"`
|
||||||
|
ExcludeVolumes string `option:"excludevolumes" help:"semicolon separated list of volumes to exclude from snapshotting (ex. 'c:\\;e:\\mnt;\\\\?\\Volume{...}')"`
|
||||||
|
Timeout time.Duration `option:"timeout" help:"time that the VSS can spend creating snapshot before timing out"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -47,31 +49,59 @@ type ErrorHandler func(item string, err error) error
|
||||||
// MessageHandler is used to report errors/messages via callbacks.
|
// MessageHandler is used to report errors/messages via callbacks.
|
||||||
type MessageHandler func(msg string, args ...interface{})
|
type MessageHandler func(msg string, args ...interface{})
|
||||||
|
|
||||||
|
// VolumeFilter is used to filter volumes by it's mount point or GUID path.
|
||||||
|
type VolumeFilter func(volume string) bool
|
||||||
|
|
||||||
// LocalVss is a wrapper around the local file system which uses windows volume
|
// LocalVss is a wrapper around the local file system which uses windows volume
|
||||||
// shadow copy service (VSS) in a transparent way.
|
// shadow copy service (VSS) in a transparent way.
|
||||||
type LocalVss struct {
|
type LocalVss struct {
|
||||||
FS
|
FS
|
||||||
snapshots map[string]VssSnapshot
|
snapshots map[string]VssSnapshot
|
||||||
failedSnapshots map[string]struct{}
|
failedSnapshots map[string]struct{}
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
msgError ErrorHandler
|
msgError ErrorHandler
|
||||||
msgMessage MessageHandler
|
msgMessage MessageHandler
|
||||||
timeout time.Duration
|
excludeAllMountPoints bool
|
||||||
|
excludeVolumes map[string]struct{}
|
||||||
|
timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// statically ensure that LocalVss implements FS.
|
// statically ensure that LocalVss implements FS.
|
||||||
var _ FS = &LocalVss{}
|
var _ FS = &LocalVss{}
|
||||||
|
|
||||||
|
// parseMountPoints try to convert semicolon separated list of mount points
|
||||||
|
// to map of lowercased volume GUID pathes. Mountpoints already in volume
|
||||||
|
// GUID path format will be validated and normalized.
|
||||||
|
func parseMountPoints(list string, msgError ErrorHandler) (volumes map[string]struct{}) {
|
||||||
|
if list == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, s := range strings.Split(list, ";") {
|
||||||
|
if v, err := GetVolumeNameForVolumeMountPoint(s); err != nil {
|
||||||
|
msgError(s, errors.Errorf("failed to parse vss.excludevolumes [%s]: %s", s, err))
|
||||||
|
} else {
|
||||||
|
if volumes == nil {
|
||||||
|
volumes = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
volumes[strings.ToLower(v)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// NewLocalVss creates a new wrapper around the windows filesystem using volume
|
// NewLocalVss creates a new wrapper around the windows filesystem using volume
|
||||||
// shadow copy service to access locked files.
|
// shadow copy service to access locked files.
|
||||||
func NewLocalVss(msgError ErrorHandler, msgMessage MessageHandler, cfg VSSConfig) *LocalVss {
|
func NewLocalVss(msgError ErrorHandler, msgMessage MessageHandler, cfg VSSConfig) *LocalVss {
|
||||||
return &LocalVss{
|
return &LocalVss{
|
||||||
FS: Local{},
|
FS: Local{},
|
||||||
snapshots: make(map[string]VssSnapshot),
|
snapshots: make(map[string]VssSnapshot),
|
||||||
failedSnapshots: make(map[string]struct{}),
|
failedSnapshots: make(map[string]struct{}),
|
||||||
msgError: msgError,
|
msgError: msgError,
|
||||||
msgMessage: msgMessage,
|
msgMessage: msgMessage,
|
||||||
timeout: cfg.Timeout,
|
excludeAllMountPoints: cfg.ExcludeAllMountPoints,
|
||||||
|
excludeVolumes: parseMountPoints(cfg.ExcludeVolumes, msgError),
|
||||||
|
timeout: cfg.Timeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +142,24 @@ func (fs *LocalVss) Lstat(name string) (os.FileInfo, error) {
|
||||||
return os.Lstat(fs.snapshotPath(name))
|
return os.Lstat(fs.snapshotPath(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isMountPointExcluded is true if given mountpoint excluded by user.
|
||||||
|
func (fs *LocalVss) isMountPointExcluded(mountPoint string) bool {
|
||||||
|
if fs.excludeVolumes == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
volume, err := GetVolumeNameForVolumeMountPoint(mountPoint)
|
||||||
|
if err != nil {
|
||||||
|
fs.msgError(mountPoint, errors.Errorf("failed to get volume from mount point [%s]: %s", mountPoint, err))
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := fs.excludeVolumes[strings.ToLower(volume)]
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
// snapshotPath returns the path inside a VSS snapshots if it already exists.
|
// 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 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
|
// If creation of a snapshot fails the file's original path is returned as
|
||||||
|
@ -148,23 +196,36 @@ func (fs *LocalVss) snapshotPath(path string) string {
|
||||||
|
|
||||||
if !snapshotExists && !snapshotFailed {
|
if !snapshotExists && !snapshotFailed {
|
||||||
vssVolume := volumeNameLower + string(filepath.Separator)
|
vssVolume := volumeNameLower + string(filepath.Separator)
|
||||||
fs.msgMessage("creating VSS snapshot for [%s]\n", vssVolume)
|
|
||||||
|
|
||||||
if snapshot, err := NewVssSnapshot(vssVolume, fs.timeout, fs.msgError); err != nil {
|
if fs.isMountPointExcluded(vssVolume) {
|
||||||
_ = fs.msgError(vssVolume, errors.Errorf("failed to create snapshot for [%s]: %s",
|
fs.msgMessage("snapshots for [%s] excluded by user\n", vssVolume)
|
||||||
vssVolume, err))
|
|
||||||
fs.failedSnapshots[volumeNameLower] = struct{}{}
|
fs.failedSnapshots[volumeNameLower] = struct{}{}
|
||||||
} else {
|
} else {
|
||||||
fs.snapshots[volumeNameLower] = snapshot
|
fs.msgMessage("creating VSS snapshot for [%s]\n", vssVolume)
|
||||||
fs.msgMessage("successfully created snapshot for [%s]\n", vssVolume)
|
|
||||||
if len(snapshot.mountPointInfo) > 0 {
|
var filter VolumeFilter
|
||||||
fs.msgMessage("mountpoints in snapshot volume [%s]:\n", vssVolume)
|
if !fs.excludeAllMountPoints {
|
||||||
for mp, mpInfo := range snapshot.mountPointInfo {
|
filter = func(volume string) bool {
|
||||||
info := ""
|
return !fs.isMountPointExcluded(volume)
|
||||||
if !mpInfo.IsSnapshotted() {
|
}
|
||||||
info = " (not snapshotted)"
|
}
|
||||||
|
|
||||||
|
if snapshot, err := NewVssSnapshot(vssVolume, fs.timeout, filter, fs.msgError); err != nil {
|
||||||
|
fs.msgError(vssVolume, errors.Errorf("failed to create snapshot for [%s]: %s",
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
fs.msgMessage(" - %s%s\n", mp, info)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,10 +33,16 @@ func HasSufficientPrivilegesForVSS() error {
|
||||||
return errors.New("VSS snapshots are only supported on windows")
|
return errors.New("VSS snapshots are only supported on windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetVolumeNameForVolumeMountPoint clear input parameter
|
||||||
|
// and calls the equivalent windows api.
|
||||||
|
func GetVolumeNameForVolumeMountPoint(mountPoint string) (string, error) {
|
||||||
|
return mountPoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewVssSnapshot creates a new vss snapshot. If creating the snapshots doesn't
|
// NewVssSnapshot creates a new vss snapshot. If creating the snapshots doesn't
|
||||||
// finish within the timeout an error is returned.
|
// finish within the timeout an error is returned.
|
||||||
func NewVssSnapshot(
|
func NewVssSnapshot(
|
||||||
_ string, _ time.Duration, _ ErrorHandler) (VssSnapshot, error) {
|
_ string, _ time.Duration, _ VolumeFilter, _ ErrorHandler) (VssSnapshot, error) {
|
||||||
return VssSnapshot{}, errors.New("VSS snapshots are only supported on windows")
|
return VssSnapshot{}, errors.New("VSS snapshots are only supported on windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -733,10 +733,33 @@ func HasSufficientPrivilegesForVSS() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetVolumeNameForVolumeMountPoint clear input parameter
|
||||||
|
// and calls the equivalent windows api.
|
||||||
|
func GetVolumeNameForVolumeMountPoint(mountPoint string) (string, error) {
|
||||||
|
if mountPoint != "" && mountPoint[len(mountPoint)-1] != filepath.Separator {
|
||||||
|
mountPoint += string(filepath.Separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
mountPointPointer, err := syscall.UTF16PtrFromString(mountPoint)
|
||||||
|
if err != nil {
|
||||||
|
return mountPoint, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// A reasonable size for the buffer to accommodate the largest possible
|
||||||
|
// volume GUID path is 50 characters.
|
||||||
|
volumeNameBuffer := make([]uint16, 50)
|
||||||
|
if err := windows.GetVolumeNameForVolumeMountPoint(
|
||||||
|
mountPointPointer, &volumeNameBuffer[0], 50); err != nil {
|
||||||
|
return mountPoint, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return syscall.UTF16ToString(volumeNameBuffer), nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewVssSnapshot creates a new vss snapshot. If creating the snapshots doesn't
|
// NewVssSnapshot creates a new vss snapshot. If creating the snapshots doesn't
|
||||||
// finish within the timeout an error is returned.
|
// finish within the timeout an error is returned.
|
||||||
func NewVssSnapshot(
|
func NewVssSnapshot(
|
||||||
volume string, timeout time.Duration, msgError ErrorHandler) (VssSnapshot, error) {
|
volume string, timeout time.Duration, filter VolumeFilter, msgError ErrorHandler) (VssSnapshot, error) {
|
||||||
is64Bit, err := isRunningOn64BitWindows()
|
is64Bit, err := isRunningOn64BitWindows()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -828,35 +851,42 @@ func NewVssSnapshot(
|
||||||
return VssSnapshot{}, err
|
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)
|
mountPointInfo := make(map[string]MountPoint)
|
||||||
|
|
||||||
for _, mountPoint := range mountPoints {
|
// if filter==nil just don't process mount points for this volume at all
|
||||||
// ensure every mountpoint is available even without a valid
|
if filter != nil {
|
||||||
// snapshot because we need to consider this when backing up files
|
mountPoints, err := enumerateMountedFolders(volume)
|
||||||
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 {
|
if err != nil {
|
||||||
iVssBackupComponents.Release()
|
iVssBackupComponents.Release()
|
||||||
return VssSnapshot{}, err
|
|
||||||
|
return VssSnapshot{}, newVssTextError(fmt.Sprintf(
|
||||||
|
"failed to enumerate mount points for volume %s: %s", volume, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
mountPointInfo[mountPoint] = MountPoint{isSnapshotted: true,
|
for _, mountPoint := range mountPoints {
|
||||||
snapshotSetID: mountPointSnapshotSetID}
|
// 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 !filter(mountPoint) {
|
||||||
|
continue
|
||||||
|
} else 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",
|
err = callAsyncFunctionAndWait(iVssBackupComponents.PrepareForBackup, "PrepareForBackup",
|
||||||
|
|
Loading…
Reference in a new issue