diff --git a/internal/fs/node_windows.go b/internal/fs/node_windows.go index 74cf6c0e5..a37c0ee34 100644 --- a/internal/fs/node_windows.go +++ b/internal/fs/node_windows.go @@ -187,29 +187,33 @@ func nodeRestoreGenericAttributes(node *restic.Node, path string, warn func(msg 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 node.IsMainFile() { + 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.FileAttributes != nil { - if err := restoreFileAttributes(path, windowsAttributes.FileAttributes); err != nil { - errs = append(errs, fmt.Errorf("error restoring file attributes 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.SecurityDescriptor != nil { - if err := setSecurityDescriptor(path, windowsAttributes.SecurityDescriptor); err != nil { - errs = append(errs, fmt.Errorf("error restoring security descriptor 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)) + } + } + if windowsAttributes.SecurityDescriptor != nil { + if err := setSecurityDescriptor(path, windowsAttributes.SecurityDescriptor); err != nil { + errs = append(errs, fmt.Errorf("error restoring security descriptor for: %s : %v", path, err)) + } } - } - restic.HandleUnknownGenericAttributesFound(unknownAttribs, warn) - return errors.Join(errs...) + node.RemoveExtraStreams(path) + restic.HandleUnknownGenericAttributesFound(unknownAttribs, warn) + return errors.Join(errs...) + } + return nil } // genericAttributesToWindowsAttrs converts the generic attributes map to a WindowsAttributes and also returns a string of unknown attributes that it could not convert. @@ -336,10 +340,20 @@ func decryptFile(pathPointer *uint16) error { // nodeFillGenericAttributes fills in the generic attributes for windows like File Attributes, // Created time and Security Descriptors. -func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFileInfo) error { - if strings.Contains(filepath.Base(path), ":") { - // Do not process for Alternate Data Streams in Windows - return nil +// 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. +func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFileInfo) (err error) { + isAds := restic.IsAds(path) + var attrs restic.WindowsAttributes + if isAds { + attrs, err = getWindowsAttributesForAds(stat, &isAds) + if err != nil { + return err + } + node.GenericAttributes, err = restic.WindowsAttrsToGenericAttributes(attrs) + // Do not process remaining generic attributes for Alternate Data Streams in Windows + // Also do not allow to process extended attributes for ADS. + return err } isVolume, err := isVolumePath(path) @@ -361,15 +375,49 @@ func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFil } } - winFI := stat.sys.(*syscall.Win32FileAttributeData) + attrs, err = getWindowsAttributes(stat, sd, path, isAds) + if err != nil { + return err + } + node.GenericAttributes, err = restic.WindowsAttrsToGenericAttributes(attrs) + return err +} - // Add Windows attributes - node.GenericAttributes, err = restic.WindowsAttrsToGenericAttributes(restic.WindowsAttributes{ +func getWindowsAttributesForAds(stat *ExtendedFileInfo, isAds *bool) (restic.WindowsAttributes, error) { + winFI := stat.sys.(*syscall.Win32FileAttributeData) + return restic.WindowsAttributes{ + CreationTime: &winFI.CreationTime, + FileAttributes: &winFI.FileAttributes, + IsADS: isAds, + }, nil +} + +func getWindowsAttributes(stat *ExtendedFileInfo, sd *[]byte, path string, isAds bool) (restic.WindowsAttributes, error) { + winFI := stat.sys.(*syscall.Win32FileAttributeData) + attrs := restic.WindowsAttributes{ CreationTime: &winFI.CreationTime, FileAttributes: &winFI.FileAttributes, SecurityDescriptor: sd, - }) - return err + } + if isAds { + attrs.IsADS = &isAds + } else { + hasAds := getHasAds(path) + if len(hasAds) > 0 { + attrs.HasADS = &hasAds + } + } + return attrs, nil +} + +func getHasAds(path string) (hasAds []string) { + s, names, err := restic.GetADStreamNames(path) + if s { + hasAds = names + } else if err != nil { + debug.Log("Could not fetch ads information for %v %v.", path, err) + } + return hasAds } // checkAndStoreEASupport checks if the volume of the path supports extended attributes and stores the result in a map diff --git a/internal/restic/node.go b/internal/restic/node.go index c572996a5..f808d1e61 100644 --- a/internal/restic/node.go +++ b/internal/restic/node.go @@ -46,13 +46,19 @@ const ( TypeFileAttributes GenericAttributeType = "windows.file_attributes" // TypeSecurityDescriptor is the GenericAttributeType used for storing security descriptors including owner, group, discretionary access control list (DACL), system access control list (SACL)) for windows files within the generic attributes map. TypeSecurityDescriptor GenericAttributeType = "windows.security_descriptor" + // TypeHasADS is the GenericAttributeType used for to indicate that a file has Alternate Data Streams attached to it. + // The value will have a | separate list of the ADS attached to the file. Those files will have a generic attribute TypeIsADS. + TypeHasADS GenericAttributeType = "windows.has_ads" + // TypeIsADS is the GenericAttributeType used for to indicate that a file represents an Alternate Data Streams and is attached to (child of) a file in the value. + // The file in the value will be a file which has a generic attribute TypeHasADS. + TypeIsADS GenericAttributeType = "windows.is_ads" // Generic Attributes for other OS types should be defined here. ) // init is called when the package is initialized. Any new GenericAttributeTypes being created must be added here as well. func init() { - storeGenericAttributeType(TypeCreationTime, TypeFileAttributes, TypeSecurityDescriptor) + storeGenericAttributeType(TypeCreationTime, TypeFileAttributes, TypeSecurityDescriptor, TypeHasADS, TypeIsADS) } // genericAttributesForOS maintains a map of known genericAttributesForOS to the OSType diff --git a/internal/restic/node_windows.go b/internal/restic/node_windows.go index 7df426665..cbdfa303e 100644 --- a/internal/restic/node_windows.go +++ b/internal/restic/node_windows.go @@ -2,11 +2,16 @@ package restic import ( "encoding/json" + "os" "reflect" "runtime" "syscall" + + "github.com/restic/restic/internal/debug" ) +const AdsSeparator = "|" + // WindowsAttributes are the genericAttributes for Windows OS type WindowsAttributes struct { // CreationTime is used for storing creation time for windows files. @@ -16,6 +21,11 @@ type WindowsAttributes struct { // SecurityDescriptor is used for storing security descriptors which includes // owner, group, discretionary access control list (DACL), system access control list (SACL) SecurityDescriptor *[]byte `generic:"security_descriptor"` + // HasADS is used for storing if a file has an Alternate Data Stream attached to it. + HasADS *[]string `generic:"has_ads"` + // IsADS is used for storing if a file is an Alternate Data Stream and is attached to (child of) a file in the value. + // The file in the value will be a file which has a generic attribute TypeHasADS. + IsADS *bool `generic:"is_ads"` } // windowsAttrsToGenericAttributes converts the WindowsAttributes to a generic attributes map using reflection @@ -24,3 +34,56 @@ func WindowsAttrsToGenericAttributes(windowsAttributes WindowsAttributes) (attrs windowsAttributesValue := reflect.ValueOf(windowsAttributes) return OSAttrsToGenericAttributes(reflect.TypeOf(windowsAttributes), &windowsAttributesValue, runtime.GOOS) } + +// IsMainFile indicates if this is the main file and not a secondary file like an ads stream. +// This is used for functionalities we want to skip for secondary (ads) files. +// Eg. For Windows we do not want to count the secondary files +func (node Node) IsMainFile() bool { + return string(node.GenericAttributes[TypeIsADS]) != "true" +} + +// RemoveExtraStreams removes any extra streams on the file which are not present in the +// backed up state in the generic attribute TypeHasAds. +func (node Node) RemoveExtraStreams(path string) { + success, existingStreams, _ := GetADStreamNames(path) + if success { + var adsValues []string + + hasAdsBytes := node.GenericAttributes[TypeHasADS] + if hasAdsBytes != nil { + var adsArray []string + err := json.Unmarshal(hasAdsBytes, &adsArray) + if err == nil { + adsValues = adsArray + } + } + + extraStreams := filterItems(adsValues, existingStreams) + for _, extraStream := range extraStreams { + streamToRemove := path + extraStream + err := os.Remove(streamToRemove) + if err != nil { + debug.Log("Error removing stream: %s : %s", streamToRemove, err) + } + } + } +} + +// filterItems filters out which items are in evalArray which are not in referenceArray. +func filterItems(referenceArray, evalArray []string) (result []string) { + // Create a map to store elements of referenceArray for fast lookup + referenceArrayMap := make(map[string]bool) + for _, item := range referenceArray { + referenceArrayMap[item] = true + } + + // Iterate through elements of evalArray + for _, item := range evalArray { + // Check if the item is not in referenceArray + if !referenceArrayMap[item] { + // Append to the result array + result = append(result, item) + } + } + return result +}