2020-10-24 09:35:57 +00:00
package fs
import (
"os"
"path/filepath"
2020-11-06 00:41:02 +00:00
"runtime"
2020-10-24 09:35:57 +00:00
"strings"
"sync"
2020-11-06 00:41:02 +00:00
"time"
2020-10-24 09:35:57 +00:00
"github.com/restic/restic/internal/errors"
2020-11-06 00:41:02 +00:00
"github.com/restic/restic/internal/options"
2024-10-18 17:27:42 +00:00
"github.com/restic/restic/internal/restic"
2020-10-24 09:35:57 +00:00
)
2020-11-06 00:41:02 +00:00
// VSSConfig holds extended options of windows volume shadow copy service.
type VSSConfig struct {
2024-04-28 22:21:33 +00:00
ExcludeAllMountPoints bool ` option:"exclude-all-mount-points" help:"exclude mountpoints from snapshotting on all volumes" `
ExcludeVolumes string ` option:"exclude-volumes" help:"semicolon separated list of volumes to exclude from snapshotting (ex. 'c:\\;e:\\mnt;\\\\?\\Volume { ...}')" `
2020-11-06 00:41:02 +00:00
Timeout time . Duration ` option:"timeout" help:"time that the VSS can spend creating snapshot before timing out" `
2021-03-22 20:31:19 +00:00
Provider string ` option:"provider" help:"VSS provider identifier which will be used for snapshotting" `
2020-11-06 00:41:02 +00:00
}
func init ( ) {
if runtime . GOOS == "windows" {
options . Register ( "vss" , VSSConfig { } )
}
}
// NewVSSConfig returns a new VSSConfig with the default values filled in.
func NewVSSConfig ( ) VSSConfig {
2020-11-06 00:41:02 +00:00
return VSSConfig {
Timeout : time . Second * 120 ,
}
2020-11-06 00:41:02 +00:00
}
// ParseVSSConfig parses a VSS extended options to VSSConfig struct.
func ParseVSSConfig ( o options . Options ) ( VSSConfig , error ) {
cfg := NewVSSConfig ( )
o = o . Extract ( "vss" )
if err := o . Apply ( "vss" , & cfg ) ; err != nil {
return VSSConfig { } , err
}
return cfg , nil
}
2020-11-10 03:48:05 +00:00
// ErrorHandler is used to report errors via callback.
type ErrorHandler func ( item string , err error )
2020-10-24 09:35:57 +00:00
// MessageHandler is used to report errors/messages via callbacks.
type MessageHandler func ( msg string , args ... interface { } )
2020-11-06 00:41:02 +00:00
// VolumeFilter is used to filter volumes by it's mount point or GUID path.
type VolumeFilter func ( volume string ) bool
2020-10-24 09:35:57 +00:00
// 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
2020-11-06 00:41:02 +00:00
snapshots map [ string ] VssSnapshot
failedSnapshots map [ string ] struct { }
mutex sync . RWMutex
msgError ErrorHandler
msgMessage MessageHandler
excludeAllMountPoints bool
excludeVolumes map [ string ] struct { }
timeout time . Duration
2021-03-22 20:31:19 +00:00
provider string
2020-10-24 09:35:57 +00:00
}
// statically ensure that LocalVss implements FS.
var _ FS = & LocalVss { }
2020-11-06 00:41:02 +00:00
// parseMountPoints try to convert semicolon separated list of mount points
2024-07-01 22:45:59 +00:00
// to map of lowercased volume GUID paths. Mountpoints already in volume
2020-11-06 00:41:02 +00:00
// 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 , ";" ) {
2024-07-21 14:00:47 +00:00
if v , err := getVolumeNameForVolumeMountPoint ( s ) ; err != nil {
2024-04-28 22:21:33 +00:00
msgError ( s , errors . Errorf ( "failed to parse vss.exclude-volumes [%s]: %s" , s , err ) )
2020-11-06 00:41:02 +00:00
} else {
if volumes == nil {
volumes = make ( map [ string ] struct { } )
}
volumes [ strings . ToLower ( v ) ] = struct { } { }
}
}
return
}
2020-10-24 09:35:57 +00:00
// NewLocalVss creates a new wrapper around the windows filesystem using volume
// shadow copy service to access locked files.
2020-11-06 00:41:02 +00:00
func NewLocalVss ( msgError ErrorHandler , msgMessage MessageHandler , cfg VSSConfig ) * LocalVss {
2020-10-24 09:35:57 +00:00
return & LocalVss {
2020-11-06 00:41:02 +00:00
FS : Local { } ,
snapshots : make ( map [ string ] VssSnapshot ) ,
failedSnapshots : make ( map [ string ] struct { } ) ,
msgError : msgError ,
msgMessage : msgMessage ,
excludeAllMountPoints : cfg . ExcludeAllMountPoints ,
excludeVolumes : parseMountPoints ( cfg . ExcludeVolumes , msgError ) ,
timeout : cfg . Timeout ,
2021-03-22 20:31:19 +00:00
provider : cfg . Provider ,
2020-10-24 09:35:57 +00:00
}
}
// 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 {
2020-11-10 03:48:05 +00:00
fs . msgError ( volumeName , errors . Errorf ( "failed to delete VSS snapshot: %s" , err ) )
2020-10-24 09:35:57 +00:00
activeSnapshots [ volumeName ] = snapshot
}
}
fs . snapshots = activeSnapshots
}
// OpenFile wraps the Open method of the underlying file system.
2024-11-02 16:47:54 +00:00
func ( fs * LocalVss ) OpenFile ( name string , flag int ) ( File , error ) {
return fs . FS . OpenFile ( fs . snapshotPath ( name ) , flag )
2020-10-24 09:35:57 +00:00
}
2024-08-27 13:34:39 +00:00
// Stat wraps the Stat method of the underlying file system.
2020-10-24 09:35:57 +00:00
func ( fs * LocalVss ) Stat ( name string ) ( os . FileInfo , error ) {
2024-10-18 17:30:05 +00:00
return fs . FS . Stat ( fs . snapshotPath ( name ) )
2020-10-24 09:35:57 +00:00
}
2024-08-27 13:34:39 +00:00
// Lstat wraps the Lstat method of the underlying file system.
2020-10-24 09:35:57 +00:00
func ( fs * LocalVss ) Lstat ( name string ) ( os . FileInfo , error ) {
2024-10-18 17:30:05 +00:00
return fs . FS . Lstat ( fs . snapshotPath ( name ) )
2020-10-24 09:35:57 +00:00
}
2024-10-18 17:27:42 +00:00
func ( fs * LocalVss ) NodeFromFileInfo ( path string , fi os . FileInfo , ignoreXattrListError bool ) ( * restic . Node , error ) {
return fs . FS . NodeFromFileInfo ( fs . snapshotPath ( path ) , fi , ignoreXattrListError )
}
2024-04-28 22:23:50 +00:00
// isMountPointIncluded is true if given mountpoint included by user.
func ( fs * LocalVss ) isMountPointIncluded ( mountPoint string ) bool {
2020-11-06 00:41:02 +00:00
if fs . excludeVolumes == nil {
2024-04-28 22:23:50 +00:00
return true
2020-11-06 00:41:02 +00:00
}
2024-07-21 14:00:47 +00:00
volume , err := getVolumeNameForVolumeMountPoint ( mountPoint )
2020-11-06 00:41:02 +00:00
if err != nil {
fs . msgError ( mountPoint , errors . Errorf ( "failed to get volume from mount point [%s]: %s" , mountPoint , err ) )
2024-04-28 22:23:50 +00:00
return true
2020-11-06 00:41:02 +00:00
}
_ , ok := fs . excludeVolumes [ strings . ToLower ( volume ) ]
2024-04-28 22:23:50 +00:00
return ! ok
2020-11-06 00:41:02 +00:00
}
2020-10-24 09:35:57 +00:00
// 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
}
2024-11-01 14:39:58 +00:00
fixPath = strings . TrimPrefix ( fixPath , ` \\?\ ` )
2020-10-24 09:35:57 +00:00
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 )
2024-04-28 22:23:50 +00:00
if ! fs . isMountPointIncluded ( vssVolume ) {
2020-11-06 00:41:02 +00:00
fs . msgMessage ( "snapshots for [%s] excluded by user\n" , vssVolume )
2020-10-24 09:35:57 +00:00
fs . failedSnapshots [ volumeNameLower ] = struct { } { }
} else {
2020-11-06 00:41:02 +00:00
fs . msgMessage ( "creating VSS snapshot for [%s]\n" , vssVolume )
2024-04-28 22:23:50 +00:00
var includeVolume VolumeFilter
2020-11-06 00:41:02 +00:00
if ! fs . excludeAllMountPoints {
2024-04-28 22:23:50 +00:00
includeVolume = func ( volume string ) bool {
return fs . isMountPointIncluded ( volume )
2020-11-06 00:41:02 +00:00
}
}
2024-04-28 22:23:50 +00:00
if snapshot , err := NewVssSnapshot ( fs . provider , vssVolume , fs . timeout , includeVolume , fs . msgError ) ; err != nil {
2020-11-06 00:41:02 +00:00
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 )
2020-10-24 09:35:57 +00:00
}
}
}
}
}
} 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 ( ) {
2021-01-30 21:34:41 +00:00
snapshotPath += string ( filepath . Separator )
2020-10-24 09:35:57 +00:00
}
} else {
// no snapshot is available for the requested path:
// -> try to backup without a snapshot
// TODO: log warning?
snapshotPath = path
}
return snapshotPath
}