diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index 55b6ee4b3..32f5b2a4d 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -264,6 +264,10 @@ func (arch *Archiver) trackItem(item string, previous, current *restic.Node, s I // nodeFromFileInfo returns the restic node from an os.FileInfo. func (arch *Archiver) nodeFromFileInfo(snPath, filename string, meta ToNoder, ignoreXattrListError bool) (*restic.Node, error) { node, err := meta.ToNode(ignoreXattrListError) + if node == nil { + // do not filter error on total failure + return node, err + } if !arch.WithAtime { node.AccessTime = node.ModTime } diff --git a/internal/fs/file_windows.go b/internal/fs/file_windows.go index d7aabf360..bea38511c 100644 --- a/internal/fs/file_windows.go +++ b/internal/fs/file_windows.go @@ -8,7 +8,6 @@ import ( "strings" "time" - "github.com/restic/restic/internal/restic" "golang.org/x/sys/windows" ) @@ -115,22 +114,13 @@ func clearAttribute(path string, attribute uint32) error { } // openHandleForEA return a file handle for file or dir for setting/getting EAs -func openHandleForEA(nodeType restic.NodeType, path string, writeAccess bool) (handle windows.Handle, err error) { +func openHandleForEA(path string, writeAccess bool) (handle windows.Handle, err error) { path = fixpath(path) fileAccess := windows.FILE_READ_EA if writeAccess { fileAccess = fileAccess | windows.FILE_WRITE_EA } - switch nodeType { - case restic.NodeTypeFile: - utf16Path := windows.StringToUTF16Ptr(path) - handle, err = windows.CreateFile(utf16Path, uint32(fileAccess), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0) - case restic.NodeTypeDir: - utf16Path := windows.StringToUTF16Ptr(path) - handle, err = windows.CreateFile(utf16Path, uint32(fileAccess), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) - default: - return 0, nil - } - return handle, err + utf16Path := windows.StringToUTF16Ptr(path) + return windows.CreateFile(utf16Path, uint32(fileAccess), 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_BACKUP_SEMANTICS, 0) } diff --git a/internal/fs/fs_local.go b/internal/fs/fs_local.go index fc6c69cf2..b2dd454b5 100644 --- a/internal/fs/fs_local.go +++ b/internal/fs/fs_local.go @@ -90,12 +90,14 @@ type localFile struct { flag int f *os.File fi *ExtendedFileInfo + meta metadataHandle } // See the File interface for a description of each method var _ File = &localFile{} func newLocalFile(name string, flag int, metadataOnly bool) (*localFile, error) { + meta := newPathMetadataHandle(name, flag) var f *os.File if !metadataOnly { var err error @@ -105,11 +107,14 @@ func newLocalFile(name string, flag int, metadataOnly bool) (*localFile, error) } _ = setFlags(f) } - return &localFile{ + file := &localFile{ name: name, flag: flag, f: f, - }, nil + meta: meta, + } + + return file, nil } func (f *localFile) MakeReadable() error { @@ -130,20 +135,17 @@ func (f *localFile) cacheFI() error { if f.fi != nil { return nil } - var fi os.FileInfo var err error if f.f != nil { - fi, err = f.f.Stat() - } else if f.flag&O_NOFOLLOW != 0 { - fi, err = os.Lstat(f.name) + fi, err := f.f.Stat() + if err != nil { + return err + } + f.fi = extendedStat(fi) } else { - fi, err = os.Stat(f.name) + f.fi, err = f.meta.Stat() } - if err != nil { - return err - } - f.fi = extendedStat(fi) - return nil + return err } func (f *localFile) Stat() (*ExtendedFileInfo, error) { @@ -156,7 +158,7 @@ func (f *localFile) ToNode(ignoreXattrListError bool) (*restic.Node, error) { if err := f.cacheFI(); err != nil { return nil, err } - return nodeFromFileInfo(f.name, f.fi, ignoreXattrListError) + return nodeFromFileInfo(f.name, &cachedMetadataHandle{f.meta, f}, ignoreXattrListError) } func (f *localFile) Read(p []byte) (n int, err error) { @@ -173,3 +175,14 @@ func (f *localFile) Close() error { } return nil } + +// metadata handle with FileInfo from localFile +// This ensures that Stat() and ToNode() use the exact same data. +type cachedMetadataHandle struct { + metadataHandle + f *localFile +} + +func (c *cachedMetadataHandle) Stat() (*ExtendedFileInfo, error) { + return c.f.Stat() +} diff --git a/internal/fs/meta.go b/internal/fs/meta.go new file mode 100644 index 000000000..24d72471c --- /dev/null +++ b/internal/fs/meta.go @@ -0,0 +1,52 @@ +package fs + +import ( + "os" + + "github.com/restic/restic/internal/restic" +) + +type metadataHandle interface { + Name() string + Stat() (*ExtendedFileInfo, error) + Readlink() (string, error) + Xattr(ignoreListError bool) ([]restic.ExtendedAttribute, error) + // windows only + SecurityDescriptor() (*[]byte, error) +} + +type pathMetadataHandle struct { + name string + flag int +} + +var _ metadataHandle = &pathMetadataHandle{} + +func newPathMetadataHandle(name string, flag int) *pathMetadataHandle { + return &pathMetadataHandle{ + name: fixpath(name), + flag: flag, + } +} + +func (p *pathMetadataHandle) Name() string { + return p.name +} + +func (p *pathMetadataHandle) Stat() (*ExtendedFileInfo, error) { + var fi os.FileInfo + var err error + if p.flag&O_NOFOLLOW != 0 { + fi, err = os.Lstat(p.name) + } else { + fi, err = os.Stat(p.name) + } + if err != nil { + return nil, err + } + return extendedStat(fi), nil +} + +func (p *pathMetadataHandle) Readlink() (string, error) { + return os.Readlink(p.name) +} diff --git a/internal/fs/meta_noxattr.go b/internal/fs/meta_noxattr.go new file mode 100644 index 000000000..24dec496f --- /dev/null +++ b/internal/fs/meta_noxattr.go @@ -0,0 +1,10 @@ +//go:build aix || dragonfly || netbsd || openbsd +// +build aix dragonfly netbsd openbsd + +package fs + +import "github.com/restic/restic/internal/restic" + +func (p *pathMetadataHandle) Xattr(ignoreListError bool) ([]restic.ExtendedAttribute, error) { + return nil, nil +} diff --git a/internal/fs/meta_unix.go b/internal/fs/meta_unix.go new file mode 100644 index 000000000..14bb223bd --- /dev/null +++ b/internal/fs/meta_unix.go @@ -0,0 +1,8 @@ +//go:build !windows +// +build !windows + +package fs + +func (p *pathMetadataHandle) SecurityDescriptor() (*[]byte, error) { + return nil, nil +} diff --git a/internal/fs/meta_windows.go b/internal/fs/meta_windows.go new file mode 100644 index 000000000..dac41ac91 --- /dev/null +++ b/internal/fs/meta_windows.go @@ -0,0 +1,47 @@ +package fs + +import ( + "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/restic" + "golang.org/x/sys/windows" +) + +func (p *pathMetadataHandle) Xattr(_ bool) ([]restic.ExtendedAttribute, error) { + allowExtended, err := checkAndStoreEASupport(p.name) + if err != nil || !allowExtended { + return nil, err + } + + var fileHandle windows.Handle + if fileHandle, err = openHandleForEA(p.name, false); err != nil { + return nil, errors.Errorf("get EA failed while opening file handle for path %v, with: %v", p.name, err) + } + defer closeFileHandle(fileHandle, p.name) + + //Get the windows Extended Attributes using the file handle + var extAtts []extendedAttribute + extAtts, err = fgetEA(fileHandle) + debug.Log("fillExtendedAttributes(%v) %v", p.name, extAtts) + if err != nil { + return nil, errors.Errorf("get EA failed for path %v, with: %v", p.name, err) + } + if len(extAtts) == 0 { + return nil, nil + } + + extendedAttrs := make([]restic.ExtendedAttribute, 0, len(extAtts)) + for _, attr := range extAtts { + extendedAttr := restic.ExtendedAttribute{ + Name: attr.Name, + Value: attr.Value, + } + + extendedAttrs = append(extendedAttrs, extendedAttr) + } + return extendedAttrs, nil +} + +func (p *pathMetadataHandle) SecurityDescriptor() (*[]byte, error) { + return getSecurityDescriptor(p.name) +} diff --git a/internal/fs/meta_xattr.go b/internal/fs/meta_xattr.go new file mode 100644 index 000000000..e04049ffc --- /dev/null +++ b/internal/fs/meta_xattr.go @@ -0,0 +1,40 @@ +//go:build darwin || freebsd || linux || solaris +// +build darwin freebsd linux solaris + +package fs + +import ( + "fmt" + "os" + + "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/restic" +) + +func (p *pathMetadataHandle) Xattr(ignoreListError bool) ([]restic.ExtendedAttribute, error) { + xattrs, err := listxattr(p.name) + debug.Log("fillExtendedAttributes(%v) %v %v", p.name, xattrs, err) + if err != nil { + if ignoreListError && isListxattrPermissionError(err) { + return nil, nil + } + return nil, err + } + + extendedAttrs := make([]restic.ExtendedAttribute, 0, len(xattrs)) + for _, attr := range xattrs { + attrVal, err := getxattr(p.name, attr) + if err != nil { + fmt.Fprintf(os.Stderr, "can not obtain extended attribute %v for %v:\n", attr, p.name) + continue + } + attr := restic.ExtendedAttribute{ + Name: attr, + Value: attrVal, + } + + extendedAttrs = append(extendedAttrs, attr) + } + + return extendedAttrs, nil +} diff --git a/internal/fs/node.go b/internal/fs/node.go index 058d9cc7b..432af1eba 100644 --- a/internal/fs/node.go +++ b/internal/fs/node.go @@ -13,17 +13,22 @@ import ( "github.com/restic/restic/internal/restic" ) -// nodeFromFileInfo returns a new node from the given path and FileInfo. It -// returns the first error that is encountered, together with a node. -func nodeFromFileInfo(path string, fi *ExtendedFileInfo, ignoreXattrListError bool) (*restic.Node, error) { +// nodeFromFileInfo returns a new node from the given path and metadata handle. +// Can return a nil node if no file metadata can be retrieved. Otherwise, it returns +// the first error that is encountered, together with a node. +func nodeFromFileInfo(path string, meta metadataHandle, ignoreXattrListError bool) (*restic.Node, error) { + fi, err := meta.Stat() + if err != nil { + return nil, err + } node := buildBasicNode(path, fi) - if err := nodeFillExtendedStat(node, path, fi); err != nil { + if err := nodeFillExtendedStat(node, meta, fi); err != nil { return node, err } - err := nodeFillGenericAttributes(node, path, fi) - err = errors.Join(err, nodeFillExtendedAttributes(node, path, ignoreXattrListError)) + err = nodeFillGenericAttributes(node, meta, fi) + err = errors.Join(err, nodeFillExtendedAttributes(node, meta, ignoreXattrListError)) return node, err } @@ -66,7 +71,7 @@ func nodeTypeFromFileInfo(mode os.FileMode) restic.NodeType { return restic.NodeTypeInvalid } -func nodeFillExtendedStat(node *restic.Node, path string, stat *ExtendedFileInfo) error { +func nodeFillExtendedStat(node *restic.Node, meta metadataHandle, stat *ExtendedFileInfo) error { node.Inode = stat.Inode node.DeviceID = stat.DeviceID node.ChangeTime = stat.ChangeTime @@ -84,7 +89,7 @@ func nodeFillExtendedStat(node *restic.Node, path string, stat *ExtendedFileInfo case restic.NodeTypeDir: case restic.NodeTypeSymlink: var err error - node.LinkTarget, err = os.Readlink(fixpath(path)) + node.LinkTarget, err = meta.Readlink() node.Links = stat.Links if err != nil { return errors.WithStack(err) diff --git a/internal/fs/node_noxattr.go b/internal/fs/node_noxattr.go index 281a16dbd..58135d3d7 100644 --- a/internal/fs/node_noxattr.go +++ b/internal/fs/node_noxattr.go @@ -13,6 +13,6 @@ func nodeRestoreExtendedAttributes(_ *restic.Node, _ string) error { } // nodeFillExtendedAttributes is a no-op -func nodeFillExtendedAttributes(_ *restic.Node, _ string, _ bool) error { +func nodeFillExtendedAttributes(_ *restic.Node, _ metadataHandle, _ bool) error { return nil } diff --git a/internal/fs/node_unix.go b/internal/fs/node_unix.go index e88e54251..2d8f97dfc 100644 --- a/internal/fs/node_unix.go +++ b/internal/fs/node_unix.go @@ -19,6 +19,6 @@ func nodeRestoreGenericAttributes(node *restic.Node, _ string, warn func(msg str } // nodeFillGenericAttributes is a no-op. -func nodeFillGenericAttributes(_ *restic.Node, _ string, _ *ExtendedFileInfo) error { +func nodeFillGenericAttributes(_ *restic.Node, _ metadataHandle, _ *ExtendedFileInfo) error { return nil } diff --git a/internal/fs/node_windows.go b/internal/fs/node_windows.go index 74cf6c0e5..a01f7a1b6 100644 --- a/internal/fs/node_windows.go +++ b/internal/fs/node_windows.go @@ -84,56 +84,18 @@ func nodeRestoreExtendedAttributes(node *restic.Node, path string) (err error) { } // fill extended attributes in the node -// 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 nodeFillExtendedAttributes(node *restic.Node, path string, _ bool) (err error) { - if strings.Contains(filepath.Base(path), ":") { +func nodeFillExtendedAttributes(node *restic.Node, meta metadataHandle, _ bool) (err error) { + if strings.Contains(filepath.Base(meta.Name()), ":") { // Do not process for Alternate Data Streams in Windows return nil } - // only capture xattrs for file/dir if node.Type != restic.NodeTypeFile && node.Type != restic.NodeTypeDir { return nil } - allowExtended, err := checkAndStoreEASupport(path) - if err != nil { - return err - } - if !allowExtended { - return nil - } - - var fileHandle windows.Handle - if fileHandle, err = openHandleForEA(node.Type, path, false); fileHandle == 0 { - return nil - } - if err != nil { - return errors.Errorf("get EA failed while opening file handle for path %v, with: %v", path, err) - } - defer closeFileHandle(fileHandle, path) // Replaced inline defer with named function call - //Get the windows Extended Attributes using the file handle - var extAtts []extendedAttribute - extAtts, err = fgetEA(fileHandle) - debug.Log("fillExtendedAttributes(%v) %v", path, extAtts) - if err != nil { - return errors.Errorf("get EA failed for path %v, with: %v", path, err) - } - 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 { - extendedAttr := restic.ExtendedAttribute{ - Name: attr.Name, - Value: attr.Value, - } - - node.ExtendedAttributes = append(node.ExtendedAttributes, extendedAttr) - } - return nil + node.ExtendedAttributes, err = meta.Xattr(false) + return err } // closeFileHandle safely closes a file handle and logs any errors. @@ -147,11 +109,13 @@ func closeFileHandle(fileHandle windows.Handle, path string) { // 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 restic.NodeType, path string, eas []extendedAttribute) (err error) { - var fileHandle windows.Handle - if fileHandle, err = openHandleForEA(nodeType, path, true); fileHandle == 0 { + // only restore xattrs for file/dir + if nodeType != restic.NodeTypeFile && nodeType != restic.NodeTypeDir { return nil } - if err != nil { + + var fileHandle windows.Handle + if fileHandle, err = openHandleForEA(path, true); err != nil { return errors.Errorf("set EA failed while opening file handle for path %v, with: %v", path, err) } defer closeFileHandle(fileHandle, path) // Replaced inline defer with named function call @@ -336,7 +300,8 @@ 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 { +func nodeFillGenericAttributes(node *restic.Node, meta metadataHandle, stat *ExtendedFileInfo) error { + path := meta.Name() if strings.Contains(filepath.Base(path), ":") { // Do not process for Alternate Data Streams in Windows return nil @@ -356,7 +321,7 @@ func nodeFillGenericAttributes(node *restic.Node, path string, stat *ExtendedFil var sd *[]byte if node.Type == restic.NodeTypeFile || node.Type == restic.NodeTypeDir { - if sd, err = getSecurityDescriptor(path); err != nil { + if sd, err = meta.SecurityDescriptor(); err != nil { return err } } diff --git a/internal/fs/node_xattr.go b/internal/fs/node_xattr.go index e1ddf9826..08c83b3d4 100644 --- a/internal/fs/node_xattr.go +++ b/internal/fs/node_xattr.go @@ -4,11 +4,9 @@ package fs import ( - "fmt" "os" "syscall" - "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" @@ -92,30 +90,8 @@ func nodeRestoreExtendedAttributes(node *restic.Node, path string) error { return nil } -func nodeFillExtendedAttributes(node *restic.Node, 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([]restic.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 := restic.ExtendedAttribute{ - Name: attr, - Value: attrVal, - } - - node.ExtendedAttributes = append(node.ExtendedAttributes, attr) - } - - return nil +func nodeFillExtendedAttributes(node *restic.Node, meta metadataHandle, ignoreListError bool) error { + var err error + node.ExtendedAttributes, err = meta.Xattr(ignoreListError) + return err } diff --git a/internal/fs/node_xattr_all_test.go b/internal/fs/node_xattr_all_test.go index 690302f70..0efe4cbb3 100644 --- a/internal/fs/node_xattr_all_test.go +++ b/internal/fs/node_xattr_all_test.go @@ -31,7 +31,8 @@ func setAndVerifyXattr(t *testing.T, file string, attrs []restic.ExtendedAttribu nodeActual := &restic.Node{ Type: restic.NodeTypeFile, } - rtest.OK(t, nodeFillExtendedAttributes(nodeActual, file, false)) + meta := newPathMetadataHandle(file, 0) + rtest.OK(t, nodeFillExtendedAttributes(nodeActual, meta, false)) rtest.Assert(t, nodeActual.Equals(*node), "xattr mismatch got %v expected %v", nodeActual.ExtendedAttributes, node.ExtendedAttributes) }