diff --git a/internal/restic/node.go b/internal/restic/node.go index 807ee0c0f..5bdc5ba27 100644 --- a/internal/restic/node.go +++ b/internal/restic/node.go @@ -284,16 +284,6 @@ func (node Node) restoreMetadata(path string, warn func(msg string)) error { return firsterr } -func (node Node) restoreExtendedAttributes(path string) error { - for _, attr := range node.ExtendedAttributes { - err := Setxattr(path, attr.Name, attr.Value) - if err != nil { - return err - } - } - return nil -} - func (node Node) RestoreTimestamps(path string) error { var utimes = [...]syscall.Timespec{ syscall.NsecToTimespec(node.AccessTime.UnixNano()), @@ -726,34 +716,6 @@ func (node *Node) fillExtra(path string, fi os.FileInfo, ignoreXattrListError bo return err } -func (node *Node) fillExtendedAttributes(path string, ignoreListError bool) error { - xattrs, err := Listxattr(path) - debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err) - if err != nil { - if ignoreListError && IsListxattrPermissionError(err) { - return nil - } - return err - } - - node.ExtendedAttributes = make([]ExtendedAttribute, 0, len(xattrs)) - for _, attr := range xattrs { - attrVal, err := Getxattr(path, attr) - if err != nil { - fmt.Fprintf(os.Stderr, "can not obtain extended attribute %v for %v:\n", attr, path) - continue - } - attr := ExtendedAttribute{ - Name: attr, - Value: attrVal, - } - - node.ExtendedAttributes = append(node.ExtendedAttributes, attr) - } - - return nil -} - func mkfifo(path string, mode uint32) (err error) { return mknod(path, mode|syscall.S_IFIFO, 0) } diff --git a/internal/restic/node_aix.go b/internal/restic/node_aix.go index 8ee9022c9..32f63af15 100644 --- a/internal/restic/node_aix.go +++ b/internal/restic/node_aix.go @@ -23,25 +23,21 @@ func (s statT) atim() syscall.Timespec { return toTimespec(s.Atim) } func (s statT) mtim() syscall.Timespec { return toTimespec(s.Mtim) } func (s statT) ctim() syscall.Timespec { return toTimespec(s.Ctim) } -// Getxattr is a no-op on AIX. -func Getxattr(path, name string) ([]byte, error) { - return nil, nil +// restoreExtendedAttributes is a no-op on AIX. +func (node Node) restoreExtendedAttributes(_ string) error { + return nil } -// Listxattr is a no-op on AIX. -func Listxattr(path string) ([]string, error) { - return nil, nil +// fillExtendedAttributes is a no-op on AIX. +func (node *Node) fillExtendedAttributes(_ string, _ bool) error { + return nil } +// IsListxattrPermissionError is a no-op on AIX. func IsListxattrPermissionError(_ error) bool { return false } -// Setxattr is a no-op on AIX. -func Setxattr(path, name string, data []byte) error { - return nil -} - // restoreGenericAttributes is no-op on AIX. func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error { return node.handleAllUnknownGenericAttributesFound(warn) diff --git a/internal/restic/node_netbsd.go b/internal/restic/node_netbsd.go index cf1fa36bd..0fe46a3f2 100644 --- a/internal/restic/node_netbsd.go +++ b/internal/restic/node_netbsd.go @@ -13,25 +13,21 @@ func (s statT) atim() syscall.Timespec { return s.Atimespec } func (s statT) mtim() syscall.Timespec { return s.Mtimespec } func (s statT) ctim() syscall.Timespec { return s.Ctimespec } -// Getxattr is a no-op on netbsd. -func Getxattr(path, name string) ([]byte, error) { - return nil, nil +// restoreExtendedAttributes is a no-op on netbsd. +func (node Node) restoreExtendedAttributes(_ string) error { + return nil } -// Listxattr is a no-op on netbsd. -func Listxattr(path string) ([]string, error) { - return nil, nil +// fillExtendedAttributes is a no-op on netbsd. +func (node *Node) fillExtendedAttributes(_ string, _ bool) error { + return nil } +// IsListxattrPermissionError is a no-op on netbsd. func IsListxattrPermissionError(_ error) bool { return false } -// Setxattr is a no-op on netbsd. -func Setxattr(path, name string, data []byte) error { - return nil -} - // restoreGenericAttributes is no-op on netbsd. func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error { return node.handleAllUnknownGenericAttributesFound(warn) diff --git a/internal/restic/node_openbsd.go b/internal/restic/node_openbsd.go index 4f1c0dacb..71841f59f 100644 --- a/internal/restic/node_openbsd.go +++ b/internal/restic/node_openbsd.go @@ -13,25 +13,21 @@ func (s statT) atim() syscall.Timespec { return s.Atim } func (s statT) mtim() syscall.Timespec { return s.Mtim } func (s statT) ctim() syscall.Timespec { return s.Ctim } -// Getxattr is a no-op on openbsd. -func Getxattr(path, name string) ([]byte, error) { - return nil, nil +// restoreExtendedAttributes is a no-op on openbsd. +func (node Node) restoreExtendedAttributes(_ string) error { + return nil } -// Listxattr is a no-op on openbsd. -func Listxattr(path string) ([]string, error) { - return nil, nil +// fillExtendedAttributes is a no-op on openbsd. +func (node *Node) fillExtendedAttributes(_ string, _ bool) error { + return nil } +// IsListxattrPermissionError is a no-op on openbsd. func IsListxattrPermissionError(_ error) bool { return false } -// Setxattr is a no-op on openbsd. -func Setxattr(path, name string, data []byte) error { - return nil -} - // restoreGenericAttributes is no-op on openbsd. func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) error { return node.handleAllUnknownGenericAttributesFound(warn) diff --git a/internal/restic/node_test.go b/internal/restic/node_test.go index ea271faab..99ea48bbb 100644 --- a/internal/restic/node_test.go +++ b/internal/restic/node_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "runtime" + "strings" "testing" "time" @@ -205,8 +206,18 @@ func TestNodeRestoreAt(t *testing.T) { var nodePath string if test.ExtendedAttributes != nil { if runtime.GOOS == "windows" { - // restic does not support xattrs on windows - return + // In windows extended attributes are case insensitive and windows returns + // the extended attributes in UPPER case. + // Update the tests to use UPPER case xattr names for windows. + extAttrArr := test.ExtendedAttributes + // Iterate through the array using pointers + for i := 0; i < len(extAttrArr); i++ { + // Get the pointer to the current element + namePtr := &extAttrArr[i].Name + + // Modify the value through the pointer + *namePtr = strings.ToUpper(*namePtr) + } } // tempdir might be backed by a filesystem that does not support diff --git a/internal/restic/node_windows.go b/internal/restic/node_windows.go index 0c6d3775e..881c394be 100644 --- a/internal/restic/node_windows.go +++ b/internal/restic/node_windows.go @@ -70,26 +70,109 @@ func (node Node) restoreSymlinkTimestamps(path string, utimes [2]syscall.Timespe return syscall.SetFileTime(h, nil, &a, &w) } -// Getxattr retrieves extended attribute data associated with path. -func Getxattr(path, name string) ([]byte, error) { - return nil, nil -} - -// Listxattr retrieves a list of names of extended attributes associated with the -// given path in the file system. -func Listxattr(path string) ([]string, error) { - return nil, nil -} - -func IsListxattrPermissionError(_ error) bool { - return false -} - -// Setxattr associates name and data together as an attribute of path. -func Setxattr(path, name string, data []byte) error { +// restore extended attributes for windows +func (node Node) restoreExtendedAttributes(path string) (err error) { + eas := []fs.ExtendedAttribute{} + for _, attr := range node.ExtendedAttributes { + extr := new(fs.ExtendedAttribute) + extr.Name = attr.Name + extr.Value = attr.Value + eas = append(eas, *extr) + } + if len(eas) > 0 { + if errExt := restoreExtendedAttributes(node.Type, path, eas); errExt != nil { + return errExt + } + } return nil } +// fill extended attributes in the node. This also includes the Generic attributes for windows. +func (node *Node) fillExtendedAttributes(path string, _ bool) (err error) { + var fileHandle windows.Handle + + //Get file handle for file or dir + if node.Type == "file" { + if strings.HasSuffix(filepath.Clean(path), `\`) { + return nil + } + utf16Path := windows.StringToUTF16Ptr(path) + fileAccessRightReadWriteEA := (0x8 | 0x10) + fileHandle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0) + } else if node.Type == "dir" { + utf16Path := windows.StringToUTF16Ptr(path) + fileAccessRightReadWriteEA := (0x8 | 0x10) + fileHandle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) + } else { + return nil + } + if err != nil { + err = errors.Errorf("open file failed for path: %s, with: %v", path, err) + return err + } + defer func() { + err := windows.CloseHandle(fileHandle) + if err != nil { + debug.Log("Error closing file handle for %s: %v\n", path, err) + } + }() + + //Get the windows Extended Attributes using the file handle + extAtts, err := fs.GetFileEA(fileHandle) + debug.Log("fillExtendedAttributes(%v) %v", path, extAtts) + if err != nil { + debug.Log("open file failed for path: %s : %v", path, err) + return err + } else if len(extAtts) == 0 { + return nil + } + + //Fill the ExtendedAttributes in the node using the name/value pairs in the windows EA + for _, attr := range extAtts { + if err != nil { + err = errors.Errorf("can not obtain extended attribute for path %v, attr: %v, err: %v\n,", path, attr, err) + continue + } + extendedAttr := ExtendedAttribute{ + Name: attr.Name, + Value: attr.Value, + } + + node.ExtendedAttributes = append(node.ExtendedAttributes, extendedAttr) + } + return nil +} + +// 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. +func restoreExtendedAttributes(nodeType, path string, eas []fs.ExtendedAttribute) (err error) { + var fileHandle windows.Handle + switch nodeType { + case "file": + utf16Path := windows.StringToUTF16Ptr(path) + fileAccessRightReadWriteEA := (0x8 | 0x10) + fileHandle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0) + case "dir": + utf16Path := windows.StringToUTF16Ptr(path) + fileAccessRightReadWriteEA := (0x8 | 0x10) + fileHandle, err = windows.CreateFile(utf16Path, uint32(fileAccessRightReadWriteEA), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) + default: + return nil + } + defer func() { + err := windows.CloseHandle(fileHandle) + if err != nil { + debug.Log("Error closing file handle for %s: %v\n", path, err) + } + }() + if err != nil { + err = errors.Errorf("open file failed for path %v, with: %v:\n", path, err) + } else if err = fs.SetFileEA(fileHandle, eas); err != nil { + err = errors.Errorf("set EA failed for path %v, with: %v:\n", path, err) + } + return err +} + type statT syscall.Win32FileAttributeData func toStatT(i interface{}) (*statT, bool) { diff --git a/internal/restic/node_windows_test.go b/internal/restic/node_windows_test.go index 57fc51e07..f89e2aeee 100644 --- a/internal/restic/node_windows_test.go +++ b/internal/restic/node_windows_test.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "syscall" "testing" @@ -265,3 +266,68 @@ func TestNewGenericAttributeType(t *testing.T) { test.Assert(t, len(ua) == 0, "Unkown attributes: %s found for path: %s", ua, testPath) } } + +func TestRestoreExtendedAttributes(t *testing.T) { + t.Parallel() + tempDir := t.TempDir() + expectedNodes := []Node{ + { + Name: "testfile", + Type: "file", + Mode: 0644, + ModTime: parseTime("2005-05-14 21:07:03.111"), + AccessTime: parseTime("2005-05-14 21:07:04.222"), + ChangeTime: parseTime("2005-05-14 21:07:05.333"), + ExtendedAttributes: []ExtendedAttribute{ + {"user.foo", []byte("bar")}, + }, + }, + { + Name: "testdirectory", + Type: "dir", + Mode: 0755, + ModTime: parseTime("2005-05-14 21:07:03.111"), + AccessTime: parseTime("2005-05-14 21:07:04.222"), + ChangeTime: parseTime("2005-05-14 21:07:05.333"), + ExtendedAttributes: []ExtendedAttribute{ + {"user.foo", []byte("bar")}, + }, + }, + } + for _, testNode := range expectedNodes { + testPath, node := restoreAndGetNode(t, tempDir, testNode, false) + + var handle windows.Handle + var err error + utf16Path := windows.StringToUTF16Ptr(testPath) + if node.Type == "file" { + handle, err = windows.CreateFile(utf16Path, windows.FILE_READ_EA, 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0) + } else if node.Type == "dir" { + handle, err = windows.CreateFile(utf16Path, windows.FILE_READ_EA, 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) + } + test.OK(t, errors.Wrapf(err, "Error opening file/directory for: %s", testPath)) + defer func() { + err := windows.Close(handle) + test.OK(t, errors.Wrapf(err, "Error closing file for: %s", testPath)) + }() + + if len(node.ExtendedAttributes) > 0 { + extAttr, err := fs.GetFileEA(handle) + test.OK(t, errors.Wrapf(err, "Error getting extended attributes for: %s", testPath)) + test.Equals(t, len(node.ExtendedAttributes), len(extAttr)) + + for _, expectedExtAttr := range node.ExtendedAttributes { + var foundExtAttr *fs.ExtendedAttribute + for _, ea := range extAttr { + if strings.EqualFold(ea.Name, expectedExtAttr.Name) { + foundExtAttr = &ea + break + + } + } + test.Assert(t, foundExtAttr != nil, "Expected extended attribute not found") + test.Equals(t, expectedExtAttr.Value, foundExtAttr.Value) + } + } + } +} diff --git a/internal/restic/node_xattr.go b/internal/restic/node_xattr.go index 8b080e74f..a55fcb2db 100644 --- a/internal/restic/node_xattr.go +++ b/internal/restic/node_xattr.go @@ -4,23 +4,25 @@ package restic import ( + "fmt" "os" "syscall" + "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/pkg/xattr" ) -// Getxattr retrieves extended attribute data associated with path. -func Getxattr(path, name string) ([]byte, error) { +// getxattr retrieves extended attribute data associated with path. +func getxattr(path, name string) ([]byte, error) { b, err := xattr.LGet(path, name) return b, handleXattrErr(err) } -// Listxattr retrieves a list of names of extended attributes associated with the +// listxattr retrieves a list of names of extended attributes associated with the // given path in the file system. -func Listxattr(path string) ([]string, error) { +func listxattr(path string) ([]string, error) { l, err := xattr.LList(path) return l, handleXattrErr(err) } @@ -33,8 +35,8 @@ func IsListxattrPermissionError(err error) bool { return false } -// Setxattr associates name and data together as an attribute of path. -func Setxattr(path, name string, data []byte) error { +// setxattr associates name and data together as an attribute of path. +func setxattr(path, name string, data []byte) error { return handleXattrErr(xattr.LSet(path, name, data)) } @@ -66,3 +68,41 @@ func (node *Node) restoreGenericAttributes(_ string, warn func(msg string)) erro func (node *Node) fillGenericAttributes(_ string, _ os.FileInfo, _ *statT) (allowExtended bool, err error) { return true, nil } + +func (node Node) restoreExtendedAttributes(path string) error { + for _, attr := range node.ExtendedAttributes { + err := setxattr(path, attr.Name, attr.Value) + if err != nil { + return err + } + } + return nil +} + +func (node *Node) fillExtendedAttributes(path string, ignoreListError bool) error { + xattrs, err := listxattr(path) + debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err) + if err != nil { + if ignoreListError && IsListxattrPermissionError(err) { + return nil + } + return err + } + + node.ExtendedAttributes = make([]ExtendedAttribute, 0, len(xattrs)) + for _, attr := range xattrs { + attrVal, err := getxattr(path, attr) + if err != nil { + fmt.Fprintf(os.Stderr, "can not obtain extended attribute %v for %v:\n", attr, path) + continue + } + attr := ExtendedAttribute{ + Name: attr, + Value: attrVal, + } + + node.ExtendedAttributes = append(node.ExtendedAttributes, attr) + } + + return nil +}