2024-08-26 21:03:25 +00:00
package fs
2015-08-14 13:57:47 +00:00
import (
2024-02-23 00:31:20 +00:00
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"strings"
2024-08-04 16:23:39 +00:00
"sync"
2015-08-14 13:57:47 +00:00
"syscall"
2024-02-23 00:31:20 +00:00
"unsafe"
2016-08-21 15:46:23 +00:00
2024-02-23 00:31:20 +00:00
"github.com/restic/restic/internal/debug"
2017-07-23 12:21:03 +00:00
"github.com/restic/restic/internal/errors"
2024-08-26 21:03:25 +00:00
"github.com/restic/restic/internal/restic"
2024-02-23 00:31:20 +00:00
"golang.org/x/sys/windows"
)
var (
modAdvapi32 = syscall . NewLazyDLL ( "advapi32.dll" )
procEncryptFile = modAdvapi32 . NewProc ( "EncryptFileW" )
procDecryptFile = modAdvapi32 . NewProc ( "DecryptFileW" )
2024-08-03 22:03:30 +00:00
2024-08-04 16:23:39 +00:00
// eaSupportedVolumesMap is a map of volumes to boolean values indicating if they support extended attributes.
eaSupportedVolumesMap = sync . Map { }
2015-08-14 13:57:47 +00:00
)
2024-08-05 22:03:43 +00:00
const (
extendedPathPrefix = ` \\?\ `
uncPathPrefix = ` \\?\UNC\ `
globalRootPrefix = ` \\?\GLOBALROOT\ `
2024-08-12 01:25:58 +00:00
volumeGUIDPrefix = ` \\?\Volume { `
2024-08-05 22:03:43 +00:00
)
2021-05-27 19:29:51 +00:00
// mknod is not supported on Windows.
2024-06-05 22:06:57 +00:00
func mknod ( _ string , _ uint32 , _ uint64 ) ( err error ) {
2015-08-16 11:24:21 +00:00
return errors . New ( "device nodes cannot be created on windows" )
2015-08-14 13:57:47 +00:00
}
// Windows doesn't need lchown
2024-06-05 22:06:57 +00:00
func lchown ( _ string , _ int , _ int ) ( err error ) {
2015-08-14 13:57:47 +00:00
return nil
}
2024-02-23 00:31:20 +00:00
// restoreSymlinkTimestamps restores timestamps for symlinks
2024-08-26 20:35:22 +00:00
func nodeRestoreSymlinkTimestamps ( path string , utimes [ 2 ] syscall . Timespec ) error {
2022-10-30 10:02:31 +00:00
// tweaked version of UtimesNano from go/src/syscall/syscall_windows.go
2024-07-21 13:58:41 +00:00
pathp , e := syscall . UTF16PtrFromString ( fixpath ( path ) )
2022-10-30 10:02:31 +00:00
if e != nil {
return e
}
h , e := syscall . CreateFile ( pathp ,
syscall . FILE_WRITE_ATTRIBUTES , syscall . FILE_SHARE_WRITE , nil , syscall . OPEN_EXISTING ,
syscall . FILE_FLAG_BACKUP_SEMANTICS | syscall . FILE_FLAG_OPEN_REPARSE_POINT , 0 )
if e != nil {
return e
}
2024-02-23 00:31:20 +00:00
defer func ( ) {
err := syscall . Close ( h )
if err != nil {
debug . Log ( "Error closing file handle for %s: %v\n" , path , err )
}
} ( )
2022-10-30 10:02:31 +00:00
a := syscall . NsecToFiletime ( syscall . TimespecToNsec ( utimes [ 0 ] ) )
w := syscall . NsecToFiletime ( syscall . TimespecToNsec ( utimes [ 1 ] ) )
return syscall . SetFileTime ( h , nil , & a , & w )
2017-02-02 11:23:13 +00:00
}
2024-05-17 20:18:20 +00:00
// restore extended attributes for windows
2024-08-26 21:03:25 +00:00
func nodeRestoreExtendedAttributes ( node * restic . Node , path string ) ( err error ) {
2024-06-05 22:06:57 +00:00
count := len ( node . ExtendedAttributes )
if count > 0 {
2024-07-21 14:00:47 +00:00
eas := make ( [ ] extendedAttribute , count )
2024-06-05 22:06:57 +00:00
for i , attr := range node . ExtendedAttributes {
2024-07-21 14:00:47 +00:00
eas [ i ] = extendedAttribute { Name : attr . Name , Value : attr . Value }
2024-06-05 22:06:57 +00:00
}
2024-05-17 20:18:20 +00:00
if errExt := restoreExtendedAttributes ( node . Type , path , eas ) ; errExt != nil {
return errExt
}
}
return nil
2017-02-02 11:23:13 +00:00
}
2024-05-17 20:18:20 +00:00
// fill extended attributes in the node. This also includes the Generic attributes for windows.
2024-08-26 21:03:25 +00:00
func nodeFillExtendedAttributes ( node * restic . Node , path string , _ bool ) ( err error ) {
2024-05-17 20:18:20 +00:00
var fileHandle windows . Handle
2024-07-21 14:00:47 +00:00
if fileHandle , err = openHandleForEA ( node . Type , path , false ) ; fileHandle == 0 {
2024-06-06 04:40:21 +00:00
return nil
}
2024-05-17 20:18:20 +00:00
if err != nil {
2024-06-05 22:06:57 +00:00
return errors . Errorf ( "get EA failed while opening file handle for path %v, with: %v" , path , err )
2024-05-17 20:18:20 +00:00
}
2024-06-06 04:40:21 +00:00
defer closeFileHandle ( fileHandle , path ) // Replaced inline defer with named function call
2024-05-17 20:18:20 +00:00
//Get the windows Extended Attributes using the file handle
2024-07-21 14:00:47 +00:00
var extAtts [ ] extendedAttribute
extAtts , err = fgetEA ( fileHandle )
2024-05-17 20:18:20 +00:00
debug . Log ( "fillExtendedAttributes(%v) %v" , path , extAtts )
if err != nil {
2024-06-05 22:06:57 +00:00
return errors . Errorf ( "get EA failed for path %v, with: %v" , path , err )
}
if len ( extAtts ) == 0 {
2024-05-17 20:18:20 +00:00
return nil
}
//Fill the ExtendedAttributes in the node using the name/value pairs in the windows EA
for _ , attr := range extAtts {
2024-08-26 21:03:25 +00:00
extendedAttr := restic . ExtendedAttribute {
2024-05-17 20:18:20 +00:00
Name : attr . Name ,
Value : attr . Value ,
}
2024-01-31 19:48:03 +00:00
2024-05-17 20:18:20 +00:00
node . ExtendedAttributes = append ( node . ExtendedAttributes , extendedAttr )
}
2015-08-14 13:57:47 +00:00
return nil
}
2024-06-06 04:40:21 +00:00
// closeFileHandle safely closes a file handle and logs any errors.
func closeFileHandle ( fileHandle windows . Handle , path string ) {
err := windows . CloseHandle ( fileHandle )
if err != nil {
debug . Log ( "Error closing file handle for %s: %v\n" , path , err )
}
}
2024-06-05 22:06:57 +00:00
// restoreExtendedAttributes handles restore of the Windows Extended Attributes to the specified path.
// The Windows API requires setting of all the Extended Attributes in one call.
2024-07-21 14:00:47 +00:00
func restoreExtendedAttributes ( nodeType restic . NodeType , path string , eas [ ] extendedAttribute ) ( err error ) {
2024-06-05 22:06:57 +00:00
var fileHandle windows . Handle
2024-07-21 14:00:47 +00:00
if fileHandle , err = openHandleForEA ( nodeType , path , true ) ; fileHandle == 0 {
2024-06-06 04:40:21 +00:00
return nil
}
2024-06-05 22:06:57 +00:00
if err != nil {
return errors . Errorf ( "set EA failed while opening file handle for path %v, with: %v" , path , err )
2024-05-17 20:18:20 +00:00
}
2024-06-06 04:40:21 +00:00
defer closeFileHandle ( fileHandle , path ) // Replaced inline defer with named function call
2024-06-14 18:17:06 +00:00
// clear old unexpected xattrs by setting them to an empty value
2024-07-21 14:00:47 +00:00
oldEAs , err := fgetEA ( fileHandle )
2024-06-14 18:17:06 +00:00
if err != nil {
return err
}
for _ , oldEA := range oldEAs {
found := false
for _ , ea := range eas {
if strings . EqualFold ( ea . Name , oldEA . Name ) {
found = true
break
}
}
if ! found {
2024-07-21 14:00:47 +00:00
eas = append ( eas , extendedAttribute { Name : oldEA . Name , Value : nil } )
2024-06-14 18:17:06 +00:00
}
}
2024-07-21 14:00:47 +00:00
if err = fsetEA ( fileHandle , eas ) ; err != nil {
2024-06-05 22:06:57 +00:00
return errors . Errorf ( "set EA failed for path %v, with: %v" , path , err )
2024-05-17 20:18:20 +00:00
}
2024-06-05 22:06:57 +00:00
return nil
2024-05-17 20:18:20 +00:00
}
2024-02-23 00:31:20 +00:00
// restoreGenericAttributes restores generic attributes for Windows
2024-08-26 21:03:25 +00:00
func nodeRestoreGenericAttributes ( node * restic . Node , path string , warn func ( msg string ) ) ( err error ) {
2024-02-23 00:31:20 +00:00
if len ( node . GenericAttributes ) == 0 {
return nil
}
var errs [ ] error
windowsAttributes , unknownAttribs , err := genericAttributesToWindowsAttrs ( node . GenericAttributes )
if err != nil {
return fmt . Errorf ( "error parsing generic attribute for: %s : %v" , path , err )
}
if windowsAttributes . CreationTime != nil {
if err := restoreCreationTime ( path , windowsAttributes . CreationTime ) ; err != nil {
errs = append ( errs , fmt . Errorf ( "error restoring creation time for: %s : %v" , path , err ) )
}
}
if windowsAttributes . FileAttributes != nil {
if err := restoreFileAttributes ( path , windowsAttributes . FileAttributes ) ; err != nil {
errs = append ( errs , fmt . Errorf ( "error restoring file attributes for: %s : %v" , path , err ) )
}
}
2024-02-24 20:25:28 +00:00
if windowsAttributes . SecurityDescriptor != nil {
2024-07-21 14:00:47 +00:00
if err := setSecurityDescriptor ( path , windowsAttributes . SecurityDescriptor ) ; err != nil {
2024-02-24 20:25:28 +00:00
errs = append ( errs , fmt . Errorf ( "error restoring security descriptor for: %s : %v" , path , err ) )
}
}
2024-02-23 00:31:20 +00:00
2024-08-26 21:03:25 +00:00
restic . HandleUnknownGenericAttributesFound ( unknownAttribs , warn )
2024-02-23 00:31:20 +00:00
return errors . CombineErrors ( errs ... )
}
2024-07-01 22:45:59 +00:00
// genericAttributesToWindowsAttrs converts the generic attributes map to a WindowsAttributes and also returns a string of unknown attributes that it could not convert.
2024-08-27 14:00:29 +00:00
func genericAttributesToWindowsAttrs ( attrs map [ restic . GenericAttributeType ] json . RawMessage ) ( windowsAttributes restic . WindowsAttributes , unknownAttribs [ ] restic . GenericAttributeType , err error ) {
2024-02-23 00:31:20 +00:00
waValue := reflect . ValueOf ( & windowsAttributes ) . Elem ( )
2024-08-26 21:03:25 +00:00
unknownAttribs , err = restic . GenericAttributesToOSAttrs ( attrs , reflect . TypeOf ( windowsAttributes ) , & waValue , "windows" )
2024-02-23 00:31:20 +00:00
return windowsAttributes , unknownAttribs , err
}
// restoreCreationTime gets the creation time from the data and sets it to the file/folder at
// the specified path.
func restoreCreationTime ( path string , creationTime * syscall . Filetime ) ( err error ) {
pathPointer , err := syscall . UTF16PtrFromString ( path )
if err != nil {
return err
}
handle , err := syscall . CreateFile ( pathPointer ,
syscall . FILE_WRITE_ATTRIBUTES , syscall . FILE_SHARE_WRITE , nil ,
syscall . OPEN_EXISTING , syscall . FILE_FLAG_BACKUP_SEMANTICS , 0 )
if err != nil {
return err
}
defer func ( ) {
if err := syscall . Close ( handle ) ; err != nil {
debug . Log ( "Error closing file handle for %s: %v\n" , path , err )
}
} ( )
return syscall . SetFileTime ( handle , creationTime , nil , nil )
}
// restoreFileAttributes gets the File Attributes from the data and sets them to the file/folder
// at the specified path.
func restoreFileAttributes ( path string , fileAttributes * uint32 ) ( err error ) {
pathPointer , err := syscall . UTF16PtrFromString ( path )
if err != nil {
return err
}
err = fixEncryptionAttribute ( path , fileAttributes , pathPointer )
if err != nil {
debug . Log ( "Could not change encryption attribute for path: %s: %v" , path , err )
}
return syscall . SetFileAttributes ( pathPointer , * fileAttributes )
}
// fixEncryptionAttribute checks if a file needs to be marked encrypted and is not already encrypted, it sets
// the FILE_ATTRIBUTE_ENCRYPTED. Conversely, if the file needs to be marked unencrypted and it is already
// marked encrypted, it removes the FILE_ATTRIBUTE_ENCRYPTED.
func fixEncryptionAttribute ( path string , attrs * uint32 , pathPointer * uint16 ) ( err error ) {
if * attrs & windows . FILE_ATTRIBUTE_ENCRYPTED != 0 {
// File should be encrypted.
err = encryptFile ( pathPointer )
if err != nil {
2024-08-26 21:03:25 +00:00
if IsAccessDenied ( err ) || errors . Is ( err , windows . ERROR_FILE_READ_ONLY ) {
2024-02-23 00:31:20 +00:00
// If existing file already has readonly or system flag, encrypt file call fails.
// The readonly and system flags will be set again at the end of this func if they are needed.
2024-08-26 21:03:25 +00:00
err = ResetPermissions ( path )
2024-05-31 19:47:50 +00:00
if err != nil {
return fmt . Errorf ( "failed to encrypt file: failed to reset permissions: %s : %v" , path , err )
}
2024-07-21 14:00:47 +00:00
err = clearSystem ( path )
2024-02-23 00:31:20 +00:00
if err != nil {
return fmt . Errorf ( "failed to encrypt file: failed to clear system flag: %s : %v" , path , err )
}
err = encryptFile ( pathPointer )
if err != nil {
2024-05-31 19:47:50 +00:00
return fmt . Errorf ( "failed retry to encrypt file: %s : %v" , path , err )
2024-02-23 00:31:20 +00:00
}
} else {
return fmt . Errorf ( "failed to encrypt file: %s : %v" , path , err )
}
}
} else {
existingAttrs , err := windows . GetFileAttributes ( pathPointer )
if err != nil {
return fmt . Errorf ( "failed to get file attributes for existing file: %s : %v" , path , err )
}
if existingAttrs & windows . FILE_ATTRIBUTE_ENCRYPTED != 0 {
// File should not be encrypted, but its already encrypted. Decrypt it.
err = decryptFile ( pathPointer )
if err != nil {
2024-08-26 21:03:25 +00:00
if IsAccessDenied ( err ) || errors . Is ( err , windows . ERROR_FILE_READ_ONLY ) {
2024-02-23 00:31:20 +00:00
// If existing file already has readonly or system flag, decrypt file call fails.
// The readonly and system flags will be set again after this func if they are needed.
2024-08-26 21:03:25 +00:00
err = ResetPermissions ( path )
2024-05-31 19:47:50 +00:00
if err != nil {
return fmt . Errorf ( "failed to encrypt file: failed to reset permissions: %s : %v" , path , err )
}
2024-07-21 14:00:47 +00:00
err = clearSystem ( path )
2024-02-23 00:31:20 +00:00
if err != nil {
return fmt . Errorf ( "failed to decrypt file: failed to clear system flag: %s : %v" , path , err )
}
err = decryptFile ( pathPointer )
if err != nil {
2024-05-31 19:47:50 +00:00
return fmt . Errorf ( "failed retry to decrypt file: %s : %v" , path , err )
2024-02-23 00:31:20 +00:00
}
} else {
return fmt . Errorf ( "failed to decrypt file: %s : %v" , path , err )
}
}
}
}
return err
}
// encryptFile set the encrypted flag on the file.
func encryptFile ( pathPointer * uint16 ) error {
// Call EncryptFile function
ret , _ , err := procEncryptFile . Call ( uintptr ( unsafe . Pointer ( pathPointer ) ) )
if ret == 0 {
return err
}
return nil
}
// decryptFile removes the encrypted flag from the file.
func decryptFile ( pathPointer * uint16 ) error {
// Call DecryptFile function
ret , _ , err := procDecryptFile . Call ( uintptr ( unsafe . Pointer ( pathPointer ) ) )
if ret == 0 {
return err
}
return nil
}
2024-08-26 20:35:22 +00:00
// nodeFillGenericAttributes fills in the generic attributes for windows like File Attributes,
2024-08-04 16:23:39 +00:00
// Created time and Security Descriptors.
// It also checks if the volume supports extended attributes and stores the result in a map
// so that it does not have to be checked again for subsequent calls for paths in the same volume.
2024-08-24 21:43:45 +00:00
func nodeFillGenericAttributes ( node * restic . Node , path string , stat * ExtendedFileInfo ) ( allowExtended bool , err error ) {
2024-02-23 00:31:20 +00:00
if strings . Contains ( filepath . Base ( path ) , ":" ) {
2024-08-04 16:23:39 +00:00
// Do not process for Alternate Data Streams in Windows
2024-02-23 00:31:20 +00:00
// Also do not allow processing of extended attributes for ADS.
return false , nil
}
2024-08-04 16:36:13 +00:00
2024-08-03 22:03:30 +00:00
if strings . HasSuffix ( filepath . Clean ( path ) , ` \ ` ) {
2024-08-04 16:23:39 +00:00
// filepath.Clean(path) ends with '\' for Windows root volume paths only
// Do not process file attributes, created time and sd for windows root volume paths
// Security descriptors are not supported for root volume paths.
// Though file attributes and created time are supported for root volume paths,
// we ignore them and we do not want to replace them during every restore.
2024-08-05 22:03:43 +00:00
allowExtended , err = checkAndStoreEASupport ( path )
2024-08-04 17:05:40 +00:00
if err != nil {
return false , err
}
2024-08-04 16:23:39 +00:00
return allowExtended , nil
}
var sd * [ ] byte
2024-07-09 17:51:44 +00:00
if node . Type == restic . NodeTypeFile || node . Type == restic . NodeTypeDir {
2024-08-04 17:05:40 +00:00
// Check EA support and get security descriptor for file/dir only
2024-08-05 22:03:43 +00:00
allowExtended , err = checkAndStoreEASupport ( path )
2024-08-04 17:05:40 +00:00
if err != nil {
return false , err
}
2024-07-21 14:00:47 +00:00
if sd , err = getSecurityDescriptor ( path ) ; err != nil {
2024-08-10 16:38:04 +00:00
return allowExtended , err
2024-02-24 20:25:28 +00:00
}
2024-08-04 16:23:39 +00:00
}
2024-08-24 21:43:45 +00:00
winFI := stat . Sys ( ) . ( * syscall . Win32FileAttributeData )
2024-08-04 16:23:39 +00:00
// Add Windows attributes
2024-08-27 14:00:29 +00:00
node . GenericAttributes , err = restic . WindowsAttrsToGenericAttributes ( restic . WindowsAttributes {
2024-08-24 21:43:45 +00:00
CreationTime : & winFI . CreationTime ,
FileAttributes : & winFI . FileAttributes ,
2024-08-04 16:23:39 +00:00
SecurityDescriptor : sd ,
} )
return allowExtended , err
}
2024-08-04 19:19:13 +00:00
// checkAndStoreEASupport checks if the volume of the path supports extended attributes and stores the result in a map
2024-08-04 16:23:39 +00:00
// If the result is already in the map, it returns the result from the map.
2024-08-04 19:19:13 +00:00
func checkAndStoreEASupport ( path string ) ( isEASupportedVolume bool , err error ) {
2024-08-11 07:23:47 +00:00
var volumeName string
volumeName , err = prepareVolumeName ( path )
if err != nil {
return false , err
}
if volumeName != "" {
// First check if the manually prepared volume name is already in the map
eaSupportedValue , exists := eaSupportedVolumesMap . Load ( volumeName )
if exists {
2024-08-11 07:48:25 +00:00
// Cache hit, immediately return the cached value
2024-08-11 07:23:47 +00:00
return eaSupportedValue . ( bool ) , nil
}
// If not found, check if EA is supported with manually prepared volume name
2024-07-21 14:00:47 +00:00
isEASupportedVolume , err = pathSupportsExtendedAttributes ( volumeName + ` \ ` )
2024-08-12 01:25:58 +00:00
// If the prepared volume name is not valid, we will fetch the actual volume name next.
2024-08-11 07:48:25 +00:00
if err != nil && ! errors . Is ( err , windows . DNS_ERROR_INVALID_NAME ) {
2024-08-12 01:25:58 +00:00
debug . Log ( "Error checking if extended attributes are supported for prepared volume name %s: %v" , volumeName , err )
// There can be multiple errors like path does not exist, bad network path, etc.
// We just gracefully disallow extended attributes for cases.
return false , nil
2024-08-04 19:19:13 +00:00
}
}
2024-07-21 14:00:47 +00:00
// If an entry is not found, get the actual volume name
volumeNameActual , err := getVolumePathName ( path )
2024-08-11 07:23:47 +00:00
if err != nil {
2024-08-12 01:25:58 +00:00
debug . Log ( "Error getting actual volume name %s for path %s: %v" , volumeName , path , err )
// There can be multiple errors like path does not exist, bad network path, etc.
// We just gracefully disallow extended attributes for cases.
return false , nil
2024-08-04 19:19:13 +00:00
}
2024-08-11 07:23:47 +00:00
if volumeNameActual != volumeName {
// If the actual volume name is different, check cache for the actual volume name
eaSupportedValue , exists := eaSupportedVolumesMap . Load ( volumeNameActual )
if exists {
2024-08-11 07:48:25 +00:00
// Cache hit, immediately return the cached value
2024-08-11 07:23:47 +00:00
return eaSupportedValue . ( bool ) , nil
}
// If the actual volume name is different and is not in the map, again check if the new volume supports extended attributes with the actual volume name
2024-07-21 14:00:47 +00:00
isEASupportedVolume , err = pathSupportsExtendedAttributes ( volumeNameActual + ` \ ` )
2024-08-12 01:25:58 +00:00
// Debug log for cases where the prepared volume name is not valid
2024-08-11 07:23:47 +00:00
if err != nil {
2024-08-12 01:25:58 +00:00
debug . Log ( "Error checking if extended attributes are supported for actual volume name %s: %v" , volumeNameActual , err )
// There can be multiple errors like path does not exist, bad network path, etc.
// We just gracefully disallow extended attributes for cases.
return false , nil
} else {
debug . Log ( "Checking extended attributes. Prepared volume name: %s, actual volume name: %s, isEASupportedVolume: %v, err: %v" , volumeName , volumeNameActual , isEASupportedVolume , err )
2024-08-11 07:23:47 +00:00
}
2024-08-04 16:23:39 +00:00
}
2024-08-12 01:25:58 +00:00
if volumeNameActual != "" {
eaSupportedVolumesMap . Store ( volumeNameActual , isEASupportedVolume )
}
2024-08-11 07:23:47 +00:00
return isEASupportedVolume , err
}
2024-02-23 00:31:20 +00:00
2024-08-11 07:23:47 +00:00
// prepareVolumeName prepares the volume name for different cases in Windows
func prepareVolumeName ( path string ) ( volumeName string , err error ) {
// Check if it's an extended length path
if strings . HasPrefix ( path , globalRootPrefix ) {
// Extract the VSS snapshot volume name eg. `\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyXX`
if parts := strings . SplitN ( path , ` \ ` , 7 ) ; len ( parts ) >= 6 {
volumeName = strings . Join ( parts [ : 6 ] , ` \ ` )
} else {
volumeName = filepath . VolumeName ( path )
}
} else {
2024-08-12 01:25:58 +00:00
if ! strings . HasPrefix ( path , volumeGUIDPrefix ) { // Handle volume GUID path
if strings . HasPrefix ( path , uncPathPrefix ) {
// Convert \\?\UNC\ extended path to standard path to get the volume name correctly
path = ` \\ ` + path [ len ( uncPathPrefix ) : ]
} else if strings . HasPrefix ( path , extendedPathPrefix ) {
//Extended length path prefix needs to be trimmed to get the volume name correctly
path = path [ len ( extendedPathPrefix ) : ]
} else {
// Use the absolute path
path , err = filepath . Abs ( path )
if err != nil {
return "" , fmt . Errorf ( "failed to get absolute path: %w" , err )
}
2024-08-11 07:23:47 +00:00
}
}
volumeName = filepath . VolumeName ( path )
2024-02-23 00:31:20 +00:00
}
2024-08-11 07:23:47 +00:00
return volumeName , nil
2024-02-23 00:31:20 +00:00
}