From 1d392a36f9df04840364dce29f553a41b449f3c8 Mon Sep 17 00:00:00 2001 From: aneesh-n <99904+aneesh-n@users.noreply.github.com> Date: Sun, 11 Aug 2024 01:23:47 -0600 Subject: [PATCH] Fix extended attributes handling for VSS snapshots --- internal/fs/ea_windows.go | 18 +++++++ internal/restic/node_windows.go | 90 +++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 28 deletions(-) diff --git a/internal/fs/ea_windows.go b/internal/fs/ea_windows.go index d19a1ee6a..bf7b02fd4 100644 --- a/internal/fs/ea_windows.go +++ b/internal/fs/ea_windows.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "errors" "fmt" + "strings" "syscall" "unsafe" @@ -298,3 +299,20 @@ func PathSupportsExtendedAttributes(path string) (supported bool, err error) { supported = (fileSystemFlags & windows.FILE_SUPPORTS_EXTENDED_ATTRIBUTES) != 0 return supported, nil } + +// GetVolumePathName returns the volume path name for the given path. +func GetVolumePathName(path string) (volumeName string, err error) { + utf16Path, err := windows.UTF16PtrFromString(path) + if err != nil { + return "", err + } + // Get the volume path (e.g., "D:") + var volumePath [windows.MAX_PATH + 1]uint16 + err = windows.GetVolumePathName(utf16Path, &volumePath[0], windows.MAX_PATH+1) + if err != nil { + return "", err + } + // Trim any trailing backslashes + volumeName = strings.TrimRight(windows.UTF16ToString(volumePath[:]), "\\") + return volumeName, nil +} diff --git a/internal/restic/node_windows.go b/internal/restic/node_windows.go index ceb304d0c..6adb51f0d 100644 --- a/internal/restic/node_windows.go +++ b/internal/restic/node_windows.go @@ -407,40 +407,74 @@ func (node *Node) fillGenericAttributes(path string, fi os.FileInfo, stat *statT // checkAndStoreEASupport checks if the volume of the path supports extended attributes and stores the result in a map // If the result is already in the map, it returns the result from the map. func checkAndStoreEASupport(path string) (isEASupportedVolume bool, err error) { - // Check if it's an extended length 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 if strings.HasPrefix(path, globalRootPrefix) { - // EAs are not supported for \\?\GLOBALROOT i.e. VSS snapshots - return false, nil - } else { - // Use the absolute path - path, err = filepath.Abs(path) - if err != nil { - return false, fmt.Errorf("failed to get absolute path: %w", err) - } - } - volumeName := filepath.VolumeName(path) - if volumeName == "" { - return false, nil - } - eaSupportedValue, exists := eaSupportedVolumesMap.Load(volumeName) - if exists { - return eaSupportedValue.(bool), nil + var volumeName string + volumeName, err = prepareVolumeName(path) + if err != nil { + return false, err } - // Add backslash to the volume name to ensure it is a valid path - isEASupportedVolume, err = fs.PathSupportsExtendedAttributes(volumeName + `\`) - if err == nil { - eaSupportedVolumesMap.Store(volumeName, isEASupportedVolume) + if volumeName != "" { + // First check if the manually prepared volume name is already in the map + eaSupportedValue, exists := eaSupportedVolumesMap.Load(volumeName) + if exists { + return eaSupportedValue.(bool), nil + } + // If not found, check if EA is supported with manually prepared volume name + isEASupportedVolume, err = fs.PathSupportsExtendedAttributes(volumeName + `\`) + if err != nil { + return false, err + } } + // If an entry is not found, get the actual volume name using the GetVolumePathName function + volumeNameActual, err := fs.GetVolumePathName(path) + if err != nil { + return false, err + } + if volumeNameActual != volumeName { + // If the actual volume name is different, check cache for the actual volume name + eaSupportedValue, exists := eaSupportedVolumesMap.Load(volumeNameActual) + if exists { + 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 + isEASupportedVolume, err = fs.PathSupportsExtendedAttributes(volumeNameActual + `\`) + if err != nil { + return false, err + } + } + eaSupportedVolumesMap.Store(volumeNameActual, isEASupportedVolume) return isEASupportedVolume, err } +// 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 { + 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) + } + } + volumeName = filepath.VolumeName(path) + } + return volumeName, nil +} + // windowsAttrsToGenericAttributes converts the WindowsAttributes to a generic attributes map using reflection func WindowsAttrsToGenericAttributes(windowsAttributes WindowsAttributes) (attrs map[GenericAttributeType]json.RawMessage, err error) { // Get the value of the WindowsAttributes