diff --git a/go.mod b/go.mod index 8c13b39..7b82dfd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gilbertchen/duplicacy -go 1.19 +go 1.20 require ( cloud.google.com/go v0.38.0 @@ -22,7 +22,7 @@ require ( github.com/minio/highwayhash v1.0.2 github.com/ncw/swift/v2 v2.0.1 github.com/pkg/sftp v1.11.0 - github.com/pkg/xattr v0.4.1 + github.com/pkg/xattr v0.4.9 github.com/vmihailenco/msgpack v4.0.4+incompatible golang.org/x/crypto v0.12.0 golang.org/x/net v0.10.0 diff --git a/go.sum b/go.sum index 005f4c7..ed56337 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,8 @@ github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI= github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/xattr v0.4.1 h1:dhclzL6EqOXNaPDWqoeb9tIxATfBSmjqL0b4DpSjwRw= github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs= +github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= +github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -225,6 +227,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/src/duplicacy_backupmanager.go b/src/duplicacy_backupmanager.go index aff3a03..96e9d11 100644 --- a/src/duplicacy_backupmanager.go +++ b/src/duplicacy_backupmanager.go @@ -826,7 +826,10 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu return 0 } } - remoteEntry.RestoreEarlyDirFlags(fullPath) + err = remoteEntry.RestoreEarlyDirFlags(fullPath, 0) // TODO: mask + if err != nil { + LOG_WARN("DOWNLOAD_FLAGS", "Failed to set early file flags on %s: %v", fullPath, err) + } directoryEntries = append(directoryEntries, remoteEntry) } else if remoteEntry.IsSpecial() { if stat, _ := os.Lstat(fullPath); stat != nil { @@ -1297,7 +1300,10 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun LOG_ERROR("DOWNLOAD_CREATE", "Failed to create the file %s for in-place writing: %v", fullPath, err) return false, nil } - entry.RestoreEarlyFileFlags(existingFile) + err = entry.RestoreEarlyFileFlags(existingFile, 0) // TODO: implement mask + if err != nil { + LOG_WARN("DOWNLOAD_FLAGS", "Failed to set early file flags on %s: %v", fullPath, err) + } n := int64(1) // There is a go bug on Windows (https://github.com/golang/go/issues/21681) that causes Seek to fail @@ -1481,7 +1487,10 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun return false, nil } } - entry.RestoreEarlyFileFlags(existingFile) + err = entry.RestoreEarlyFileFlags(existingFile, 0) // TODO: implement mask + if err != nil { + LOG_WARN("DOWNLOAD_FLAGS", "Failed to set early file flags on %s: %v", fullPath, err) + } existingFile.Seek(0, 0) @@ -1564,7 +1573,10 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun LOG_ERROR("DOWNLOAD_OPEN", "Failed to open file for writing: %v", err) return false, nil } - entry.RestoreEarlyFileFlags(newFile) + err = entry.RestoreEarlyFileFlags(newFile, 0) // TODO: implement mask + if err != nil { + LOG_WARN("DOWNLOAD_FLAGS", "Failed to set early file flags on %s: %v", fullPath, err) + } hasher := manager.config.NewFileHasher() diff --git a/src/duplicacy_entry.go b/src/duplicacy_entry.go index 62dfc28..2a2d947 100644 --- a/src/duplicacy_entry.go +++ b/src/duplicacy_entry.go @@ -591,8 +591,9 @@ func (entry *Entry) RestoreMetadata(fullPath string, fileInfo os.FileInfo, setOw } } - if entry.Attributes != nil && len(*entry.Attributes) > 0 { - entry.SetAttributesToFile(fullPath) + err := entry.SetAttributesToFile(fullPath) + if err != nil { + LOG_WARN("RESTORE_ATTR", "Failed to set extended attributes on %s: %v", entry.Path, err) } // Note that chown can remove setuid/setgid bits so should be called before chmod @@ -621,10 +622,10 @@ func (entry *Entry) RestoreMetadata(fullPath string, fileInfo os.FileInfo, setOw } } - // TODO Set flags last - // if entry.Attributes != nil && len(*entry.Attributes) > 0 { - // entry.SetFlagsToFile(fullPath) - // } + err = entry.RestoreLateFileFlags(fullPath, fileInfo, 0) // TODO: implement mask + if err != nil { + LOG_WARN("RESTORE_FLAGS", "Failed to set file flags on %s: %v", entry.Path, err) + } return true } @@ -842,9 +843,11 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string } } + fullPath := joinPath(top, entry.Path) + if entry.IsLink() { isRegular := false - isRegular, entry.Link, err = Readlink(joinPath(top, entry.Path)) + isRegular, entry.Link, err = Readlink(fullPath) if err != nil { LOG_WARN("LIST_LINK", "Failed to read the symlink %s: %v", entry.Path, err) skippedFiles = append(skippedFiles, entry.Path) @@ -854,7 +857,7 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string if isRegular { entry.Mode ^= uint32(os.ModeSymlink) } else if path == "" && (filepath.IsAbs(entry.Link) || filepath.HasPrefix(entry.Link, `\\`)) && !strings.HasPrefix(entry.Link, normalizedTop) { - stat, err := os.Stat(joinPath(top, entry.Path)) + stat, err := os.Stat(fullPath) if err != nil { LOG_WARN("LIST_LINK", "Failed to read the symlink: %v", err) skippedFiles = append(skippedFiles, entry.Path) @@ -873,14 +876,20 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string entry = newEntry } } else if entry.IsSpecial() { - if !entry.ReadSpecial(f) { - LOG_WARN("LIST_DEV", "Failed to save device node %s", entry.Path) + if err := entry.ReadSpecial(fullPath, f); err != nil { + LOG_WARN("LIST_DEV", "Failed to save device node %s: %v", entry.Path, err) skippedFiles = append(skippedFiles, entry.Path) continue } } - entry.ReadAttributes(top) + if err := entry.ReadAttributes(fullPath, f); err != nil { + LOG_WARN("LIST_ATTR", "Failed to read xattrs on %s: %v", entry.Path, err) + } + + if err := entry.ReadFileFlags(fullPath, f); err != nil { + LOG_WARN("LIST_ATTR", "Failed to read file flags on %s: %v", entry.Path, err) + } if excludeByAttribute && entry.Attributes != nil && excludedByAttribute(*entry.Attributes) { LOG_DEBUG("LIST_EXCLUDE", "%s is excluded by attribute", entry.Path) diff --git a/src/duplicacy_utils_darwin.go b/src/duplicacy_utils_darwin.go index 9649be0..a5c8c27 100644 --- a/src/duplicacy_utils_darwin.go +++ b/src/duplicacy_utils_darwin.go @@ -7,112 +7,147 @@ package duplicacy import ( "bytes" "encoding/binary" + "errors" "os" - "path/filepath" "strings" "syscall" "github.com/pkg/xattr" + "golang.org/x/sys/unix" ) -const darwin_UF_NODUMP = 0x1 +const ( + darwinFileFlagsKey = "\x00bf" +) -const darwinFileFlagsKey = "\x00bf" +var darwinIsSuperUser bool + +func init() { + darwinIsSuperUser = syscall.Geteuid() == 0 +} func excludedByAttribute(attributes map[string][]byte) bool { value, ok := attributes["com.apple.metadata:com_apple_backup_excludeItem"] excluded := ok && strings.Contains(string(value), "com.apple.backupd") if !excluded { - value, ok := attributes[darwinFileFlagsKey] - excluded = ok && (binary.LittleEndian.Uint32(value) & darwin_UF_NODUMP) != 0 + flags, ok := attributes[darwinFileFlagsKey] + excluded = ok && (binary.LittleEndian.Uint32(flags)&unix.UF_NODUMP) != 0 } return excluded } -func (entry *Entry) ReadAttributes(top string) { - fullPath := filepath.Join(top, entry.Path) - fileInfo, err := os.Lstat(fullPath) +func (entry *Entry) ReadAttributes(fullPath string, fi os.FileInfo) error { + if entry.IsSpecial() { + return nil + } + + attributes, err := xattr.LList(fullPath) if err != nil { - return + return err } - if !entry.IsSpecial() { - attributes, _ := xattr.LList(fullPath) - if len(attributes) > 0 { - entry.Attributes = &map[string][]byte{} - for _, name := range attributes { - attribute, err := xattr.LGet(fullPath, name) - if err == nil { - (*entry.Attributes)[name] = attribute - } - } + if len(attributes) > 0 { + entry.Attributes = &map[string][]byte{} + } + var allErrors error + for _, name := range attributes { + value, err := xattr.LGet(fullPath, name) + if err != nil { + allErrors = errors.Join(allErrors, err) + } else { + (*entry.Attributes)[name] = value } } - if err := entry.readFileFlags(fileInfo); err != nil { - LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err) - } + + return allErrors } -func (entry *Entry) SetAttributesToFile(fullPath string) { - if !entry.IsSpecial() { - names, _ := xattr.LList(fullPath) - for _, name := range names { - newAttribute, found := (*entry.Attributes)[name] - if found { - oldAttribute, _ := xattr.LGet(fullPath, name) - if !bytes.Equal(oldAttribute, newAttribute) { - xattr.LSet(fullPath, name, newAttribute) - } - delete(*entry.Attributes, name) - } else { - xattr.LRemove(fullPath, name) - } - } - - for name, attribute := range *entry.Attributes { - if len(name) > 0 && name[0] == '\x00' { - continue - } - xattr.LSet(fullPath, name, attribute) - } - } - if err := entry.restoreLateFileFlags(fullPath); err != nil { - LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) - } -} - -func (entry *Entry) RestoreEarlyDirFlags(path string) error { - return nil -} - -func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error { - return nil -} - -func (entry *Entry) readFileFlags(fileInfo os.FileInfo) error { - stat, ok := fileInfo.Sys().(*syscall.Stat_t) - if ok && stat.Flags != 0 { +func (entry *Entry) ReadFileFlags(fullPath string, fileInfo os.FileInfo) error { + stat, _ := fileInfo.Sys().(*syscall.Stat_t) + if stat != nil && stat.Flags != 0 { if entry.Attributes == nil { entry.Attributes = &map[string][]byte{} } v := make([]byte, 4) binary.LittleEndian.PutUint32(v, stat.Flags) (*entry.Attributes)[darwinFileFlagsKey] = v - LOG_DEBUG("ATTR_READ", "Read flags 0x%x for %s", stat.Flags, entry.Path) } return nil } -func (entry *Entry) restoreLateFileFlags(path string) error { +func (entry *Entry) SetAttributesToFile(fullPath string) error { + if entry.Attributes == nil || len(*entry.Attributes) == 0 || entry.IsSpecial() { + return nil + } + attributes := *entry.Attributes + + if _, haveFlags := attributes[darwinFileFlagsKey]; haveFlags && len(attributes) <= 1 { + return nil + } + + names, err := xattr.LList(fullPath) + if err != nil { + return err + } + for _, name := range names { + newAttribute, found := attributes[name] + if found { + oldAttribute, _ := xattr.LGet(fullPath, name) + if !bytes.Equal(oldAttribute, newAttribute) { + err = errors.Join(err, xattr.LSet(fullPath, name, newAttribute)) + } + delete(attributes, name) + } else { + err = errors.Join(err, xattr.LRemove(fullPath, name)) + } + } + + for name, attribute := range attributes { + if len(name) > 0 && name[0] == '\x00' { + continue + } + err = errors.Join(err, xattr.LSet(fullPath, name, attribute)) + } + return err +} + +func (entry *Entry) RestoreEarlyDirFlags(fullPath string, mask uint32) error { + return nil +} + +func (entry *Entry) RestoreEarlyFileFlags(f *os.File, mask uint32) error { + return nil +} + +func (entry *Entry) RestoreLateFileFlags(fullPath string, fileInfo os.FileInfo, mask uint32) error { if entry.Attributes == nil { return nil } + + if darwinIsSuperUser { + mask |= ^uint32(unix.UF_SETTABLE | unix.SF_SETTABLE) + } else { + mask |= ^uint32(unix.UF_SETTABLE) + } + + var flags uint32 + + stat := fileInfo.Sys().(*syscall.Stat_t) + if stat == nil { + return errors.New("file stat info missing") + } if v, have := (*entry.Attributes)[darwinFileFlagsKey]; have { - f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_SYMLINK, 0) + flags = binary.LittleEndian.Uint32(v) + } + + flags = (flags & ^mask) | (stat.Flags & mask) + + if flags != stat.Flags { + f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_SYMLINK, 0) if err != nil { return err } - err = syscall.Fchflags(int(f.Fd()), int(binary.LittleEndian.Uint32(v))) + err = syscall.Fchflags(int(f.Fd()), int(flags)) f.Close() return err } diff --git a/src/duplicacy_utils_linux.go b/src/duplicacy_utils_linux.go index 7dfd291..9d9560e 100644 --- a/src/duplicacy_utils_linux.go +++ b/src/duplicacy_utils_linux.go @@ -7,8 +7,9 @@ package duplicacy import ( "bytes" "encoding/binary" + "errors" + "fmt" "os" - "path/filepath" "syscall" "unsafe" @@ -51,125 +52,55 @@ func ioctl(f *os.File, request uintptr, attrp *uint32) error { return nil } -type xattrHandle struct { - f *os.File - fullPath string +func excludedByAttribute(attributes map[string][]byte) bool { + _, excluded := attributes["user.duplicacy_exclude"] + if !excluded { + flags, ok := attributes[linuxFileFlagsKey] + excluded = ok && (binary.LittleEndian.Uint32(flags)&linux_FS_NODUMP_FL) != 0 + } + return excluded } -func (x xattrHandle) list() ([]string, error) { - if x.f != nil { - return xattr.FList(x.f) - } else { - return xattr.LList(x.fullPath) +func (entry *Entry) ReadAttributes(fullPath string, fi os.FileInfo) error { + attributes, err := xattr.LList(fullPath) + if err != nil { + return err } -} - -func (x xattrHandle) get(name string) ([]byte, error) { - if x.f != nil { - return xattr.FGet(x.f, name) - } else { - return xattr.LGet(x.fullPath, name) - } -} - -func (x xattrHandle) set(name string, value []byte) error { - if x.f != nil { - return xattr.FSet(x.f, name, value) - } else { - return xattr.LSet(x.fullPath, name, value) - } -} - -func (x xattrHandle) remove(name string) error { - if x.f != nil { - return xattr.FRemove(x.f, name) - } else { - return xattr.LRemove(x.fullPath, name) - } -} - -func (entry *Entry) ReadAttributes(top string) { - fullPath := filepath.Join(top, entry.Path) - x := xattrHandle{nil, fullPath} - - if !entry.IsLink() { - var err error - x.f, err = os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) - if err != nil { - // FIXME: We really should return errors for failure to read - return - } - } - - attributes, _ := x.list() if len(attributes) > 0 { entry.Attributes = &map[string][]byte{} } + var allErrors error for _, name := range attributes { - attribute, err := x.get(name) - if err == nil { - (*entry.Attributes)[name] = attribute - } - } - - if entry.IsFile() || entry.IsDir() { - if err := entry.readFileFlags(x.f); err != nil { - LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err) - } - } - x.f.Close() -} - -func excludedByAttribute(attributes map[string][]byte) bool { - _, ok := attributes["user.duplicacy_exclude"] - return ok -} - -func (entry *Entry) SetAttributesToFile(fullPath string) { - x := xattrHandle{nil, fullPath} - if !entry.IsLink() { - var err error - x.f, err = os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0) + value, err := xattr.LGet(fullPath, name) if err != nil { - return - } - } - - names, _ := x.list() - - for _, name := range names { - newAttribute, found := (*entry.Attributes)[name] - if found { - oldAttribute, _ := x.get(name) - if !bytes.Equal(oldAttribute, newAttribute) { - x.set(name, newAttribute) - } - delete(*entry.Attributes, name) + allErrors = errors.Join(allErrors, err) } else { - x.remove(name) + (*entry.Attributes)[name] = value } } - for name, attribute := range *entry.Attributes { - if len(name) > 0 && name[0] == '\x00' { - continue - } - x.set(name, attribute) - } - if entry.IsFile() || entry.IsDir() { - if err := entry.restoreLateFileFlags(x.f); err != nil { - LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) - } - } - x.f.Close() + return allErrors } -func (entry *Entry) readFileFlags(f *os.File) error { - var flags uint32 - if err := ioctl(f, linux_FS_IOC_GETFLAGS, &flags); err != nil { +func (entry *Entry) ReadFileFlags(fullPath string, fileInfo os.FileInfo) error { + if !(entry.IsFile() || entry.IsDir()) { + return nil + } + + f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0) + if err != nil { return err } + + var flags uint32 + + err = ioctl(f, linux_FS_IOC_GETFLAGS, &flags) + f.Close() + if err != nil { + return err + } + if flags != 0 { if entry.Attributes == nil { entry.Attributes = &map[string][]byte{} @@ -177,49 +108,109 @@ func (entry *Entry) readFileFlags(f *os.File) error { v := make([]byte, 4) binary.LittleEndian.PutUint32(v, flags) (*entry.Attributes)[linuxFileFlagsKey] = v - LOG_DEBUG("ATTR_READ", "Read flags 0x%x for %s", flags, entry.Path) } return nil } -func (entry *Entry) RestoreEarlyDirFlags(path string) error { +func (entry *Entry) SetAttributesToFile(fullPath string) error { + if entry.Attributes == nil || len(*entry.Attributes) == 0 { + return nil + } + attributes := *entry.Attributes + + if _, haveFlags := attributes[linuxFileFlagsKey]; haveFlags && len(attributes) <= 1 { + return nil + } + + names, err := xattr.LList(fullPath) + if err != nil { + return err + } + for _, name := range names { + newAttribute, found := (*entry.Attributes)[name] + if found { + oldAttribute, _ := xattr.LGet(fullPath, name) + if !bytes.Equal(oldAttribute, newAttribute) { + err = errors.Join(err, xattr.LSet(fullPath, name, newAttribute)) + } + delete(*entry.Attributes, name) + } else { + err = errors.Join(err, xattr.LRemove(fullPath, name)) + } + } + + for name, attribute := range *entry.Attributes { + if len(name) > 0 && name[0] == '\x00' { + continue + } + err = errors.Join(err, xattr.LSet(fullPath, name, attribute)) + } + return err +} + +func (entry *Entry) RestoreEarlyDirFlags(fullPath string, mask uint32) error { if entry.Attributes == nil { return nil } + var flags uint32 + if v, have := (*entry.Attributes)[linuxFileFlagsKey]; have { - flags := binary.LittleEndian.Uint32(v) & linuxIocFlagsDirEarly - f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_DIRECTORY, 0) + flags = binary.LittleEndian.Uint32(v) & linuxIocFlagsDirEarly & ^mask + } + + if flags != 0 { + f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_DIRECTORY, 0) if err != nil { return err } - LOG_DEBUG("ATTR_RESTORE", "Restore dir flags (early) 0x%x for %s", flags, entry.Path) err = ioctl(f, linux_FS_IOC_SETFLAGS, &flags) f.Close() - return err + if err != nil { + return fmt.Errorf("Set flags 0x%.8x failed: %w", flags, err) + } } return nil } -func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error { +func (entry *Entry) RestoreEarlyFileFlags(f *os.File, mask uint32) error { if entry.Attributes == nil { return nil } + var flags uint32 + if v, have := (*entry.Attributes)[linuxFileFlagsKey]; have { - flags := binary.LittleEndian.Uint32(v) & linuxIocFlagsFileEarly - LOG_DEBUG("ATTR_RESTORE", "Restore flags (early) 0x%x for %s", flags, entry.Path) - return ioctl(f, linux_FS_IOC_SETFLAGS, &flags) + flags = binary.LittleEndian.Uint32(v) & linuxIocFlagsFileEarly & ^mask + } + + if flags != 0 { + err := ioctl(f, linux_FS_IOC_SETFLAGS, &flags) + if err != nil { + return fmt.Errorf("Set flags 0x%.8x failed: %w", flags, err) + } } return nil } -func (entry *Entry) restoreLateFileFlags(f *os.File) error { - if entry.Attributes == nil { +func (entry *Entry) RestoreLateFileFlags(fullPath string, fileInfo os.FileInfo, mask uint32) error { + if entry.IsLink() || entry.Attributes == nil { return nil } + var flags uint32 + if v, have := (*entry.Attributes)[linuxFileFlagsKey]; have { - flags := binary.LittleEndian.Uint32(v) & (linuxIocFlagsFileEarly | linuxIocFlagsDirEarly | linuxIocFlagsLate) - LOG_DEBUG("ATTR_RESTORE", "Restore flags (late) 0x%x for %s", flags, entry.Path) - return ioctl(f, linux_FS_IOC_SETFLAGS, &flags) + flags = binary.LittleEndian.Uint32(v) & (linuxIocFlagsFileEarly | linuxIocFlagsDirEarly | linuxIocFlagsLate) & ^mask + } + + if flags != 0 { + f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0) + if err != nil { + return err + } + err = ioctl(f, linux_FS_IOC_SETFLAGS, &flags) + f.Close() + if err != nil { + return fmt.Errorf("Set flags 0x%.8x failed: %w", flags, err) + } } return nil } diff --git a/src/duplicacy_utils_others.go b/src/duplicacy_utils_others.go index a91d207..1c87887 100644 --- a/src/duplicacy_utils_others.go +++ b/src/duplicacy_utils_others.go @@ -8,6 +8,7 @@ package duplicacy import ( + "errors" "fmt" "os" "path" @@ -57,7 +58,7 @@ func (entry *Entry) getHardLinkKey(f os.FileInfo) (key listEntryLinkKey, linked return } stat := f.Sys().(*syscall.Stat_t) - if stat == nil || stat.Nlink < 2 { + if stat == nil || stat.Nlink <= 1 { return } key.dev = uint64(stat.Dev) @@ -66,19 +67,19 @@ func (entry *Entry) getHardLinkKey(f os.FileInfo) (key listEntryLinkKey, linked return } -func (entry *Entry) ReadSpecial(fileInfo os.FileInfo) bool { +func (entry *Entry) ReadSpecial(fullPath string, fileInfo os.FileInfo) error { if fileInfo.Mode()&(os.ModeDevice|os.ModeCharDevice) == 0 { - return true + return nil } stat := fileInfo.Sys().(*syscall.Stat_t) if stat == nil { - return false + return errors.New("file stat info missing") } entry.Size = 0 rdev := uint64(stat.Rdev) entry.StartChunk = int(rdev & 0xFFFFFFFF) entry.StartOffset = int(rdev >> 32) - return true + return nil } func (entry *Entry) GetRdev() uint64 { diff --git a/src/duplicacy_utils_windows.go b/src/duplicacy_utils_windows.go index 0ba8209..e2e7904 100644 --- a/src/duplicacy_utils_windows.go +++ b/src/duplicacy_utils_windows.go @@ -116,15 +116,32 @@ func (entry *Entry) getHardLinkKey(f os.FileInfo) (key listEntryLinkKey, linked return } -func (entry *Entry) ReadAttributes(top string) { +func (entry *Entry) ReadAttributes(fullPath string, fi os.FileInfo) error { + return nil } -func (entry *Entry) SetAttributesToFile(fullPath string) { - +func (entry *Entry) ReadFileFlags(fullPath string, fileInfo os.FileInfo) error { + return nil } -func (entry *Entry) ReadSpecial(fileInfo os.FileInfo) bool { - return true +func (entry *Entry) SetAttributesToFile(fullPath string) error { + return nil +} + +func (entry *Entry) RestoreEarlyDirFlags(fullPath string, mask uint32) error { + return nil +} + +func (entry *Entry) RestoreEarlyFileFlags(f *os.File, mask uint32) error { + return nil +} + +func (entry *Entry) RestoreLateFileFlags(fullPath string, fileInfo os.FileInfo, mask uint32) error { + return nil +} + +func (entry *Entry) ReadSpecial(fullPath string, fileInfo os.FileInfo) error { + return nil } func (entry *Entry) IsSameSpecial(fileInfo os.FileInfo) bool { @@ -161,11 +178,3 @@ func SplitDir(fullPath string) (dir string, file string) { func excludedByAttribute(attributes map[string][]byte) bool { return false } - -func (entry *Entry) RestoreEarlyDirFlags(path string) error { - return nil -} - -func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error { - return nil -} diff --git a/src/duplicacy_utils_xbsd.go b/src/duplicacy_utils_xbsd.go index 96ea167..3ef891f 100644 --- a/src/duplicacy_utils_xbsd.go +++ b/src/duplicacy_utils_xbsd.go @@ -10,110 +10,148 @@ package duplicacy import ( "bytes" "encoding/binary" + "errors" "os" - "path/filepath" "syscall" "unsafe" "github.com/pkg/xattr" ) -const bsd_UF_NODUMP = 0x1 +const ( + bsd_UF_NODUMP = 0x1 + bsd_SF_SETTABLE = 0xffff0000 + bsd_UF_SETTABLE = 0x0000ffff -const bsdFileFlagsKey = "\x00bf" + bsdFileFlagsKey = "\x00bf" +) + +var bsdIsSuperUser bool + +func init() { + bsdIsSuperUser = syscall.Geteuid() == 0 +} func excludedByAttribute(attributes map[string][]byte) bool { _, excluded := attributes["duplicacy_exclude"] if !excluded { - value, ok := attributes[bsdFileFlagsKey] - excluded = ok && (binary.LittleEndian.Uint32(value) & bsd_UF_NODUMP) != 0 + flags, ok := attributes[bsdFileFlagsKey] + excluded = ok && (binary.LittleEndian.Uint32(flags)&bsd_UF_NODUMP) != 0 } return excluded } -func (entry *Entry) ReadAttributes(top string) { - fullPath := filepath.Join(top, entry.Path) - fileInfo, err := os.Lstat(fullPath) +func (entry *Entry) ReadAttributes(fullPath string, fi os.FileInfo) error { + if entry.IsSpecial() { + return nil + } + + attributes, err := xattr.LList(fullPath) if err != nil { - return + return err } - if !entry.IsSpecial() { - attributes, _ := xattr.LList(fullPath) - - if len(attributes) > 0 { - entry.Attributes = &map[string][]byte{} - for _, name := range attributes { - attribute, err := xattr.LGet(fullPath, name) - if err == nil { - (*entry.Attributes)[name] = attribute - } - } + if len(attributes) > 0 { + entry.Attributes = &map[string][]byte{} + } + var allErrors error + for _, name := range attributes { + value, err := xattr.LGet(fullPath, name) + if err != nil { + allErrors = errors.Join(allErrors, err) + } else { + (*entry.Attributes)[name] = value } } - if err := entry.readFileFlags(fileInfo); err != nil { - LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err) - } + + return allErrors } -func (entry *Entry) SetAttributesToFile(fullPath string) { - if !entry.IsSpecial() { - names, _ := xattr.LList(fullPath) - for _, name := range names { - newAttribute, found := (*entry.Attributes)[name] - if found { - oldAttribute, _ := xattr.LGet(fullPath, name) - if !bytes.Equal(oldAttribute, newAttribute) { - xattr.LSet(fullPath, name, newAttribute) - } - delete(*entry.Attributes, name) - } else { - xattr.LRemove(fullPath, name) - } - } - - for name, attribute := range *entry.Attributes { - if len(name) > 0 && name[0] == '\x00' { - continue - } - xattr.LSet(fullPath, name, attribute) - } - } - if err := entry.restoreLateFileFlags(fullPath); err != nil { - LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) - } -} - -func (entry *Entry) RestoreEarlyDirFlags(path string) error { - return nil -} - -func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error { - return nil -} - -func (entry *Entry) readFileFlags(fileInfo os.FileInfo) error { - stat, ok := fileInfo.Sys().(*syscall.Stat_t) - if ok && stat.Flags != 0 { +func (entry *Entry) ReadFileFlags(fullPath string, fileInfo os.FileInfo) error { + stat, _ := fileInfo.Sys().(*syscall.Stat_t) + if stat != nil && stat.Flags != 0 { if entry.Attributes == nil { entry.Attributes = &map[string][]byte{} } v := make([]byte, 4) binary.LittleEndian.PutUint32(v, stat.Flags) (*entry.Attributes)[bsdFileFlagsKey] = v - LOG_DEBUG("ATTR_READ", "Read flags 0x%x for %s", stat.Flags, entry.Path) } return nil } -func (entry *Entry) restoreLateFileFlags(path string) error { +func (entry *Entry) SetAttributesToFile(fullPath string) error { + if entry.Attributes == nil || len(*entry.Attributes) == 0 || entry.IsSpecial() { + return nil + } + attributes := *entry.Attributes + + if _, haveFlags := attributes[bsdFileFlagsKey]; haveFlags && len(attributes) <= 1 { + return nil + } + + names, err := xattr.LList(fullPath) + if err != nil { + return err + } + for _, name := range names { + newAttribute, found := attributes[name] + if found { + oldAttribute, _ := xattr.LGet(fullPath, name) + if !bytes.Equal(oldAttribute, newAttribute) { + err = errors.Join(err, xattr.LSet(fullPath, name, newAttribute)) + } + delete(attributes, name) + } else { + err = errors.Join(err, xattr.LRemove(fullPath, name)) + } + } + + for name, attribute := range attributes { + if len(name) > 0 && name[0] == '\x00' { + continue + } + err = errors.Join(err, xattr.LSet(fullPath, name, attribute)) + } + return err +} + +func (entry *Entry) RestoreEarlyDirFlags(fullPath string, mask uint32) error { + return nil +} + +func (entry *Entry) RestoreEarlyFileFlags(f *os.File, mask uint32) error { + return nil +} + +func (entry *Entry) RestoreLateFileFlags(fullPath string, fileInfo os.FileInfo, mask uint32) error { if entry.Attributes == nil { return nil } + + if bsdIsSuperUser { + mask |= ^uint32(bsd_UF_SETTABLE | bsd_SF_SETTABLE) + } else { + mask |= ^uint32(bsd_UF_SETTABLE) + } + + var flags uint32 + + stat := fileInfo.Sys().(*syscall.Stat_t) + if stat == nil { + return errors.New("file stat info missing") + } if v, have := (*entry.Attributes)[bsdFileFlagsKey]; have { + flags = binary.LittleEndian.Uint32(v) + } + + flags = (flags & ^mask) | (stat.Flags & mask) + + if flags != stat.Flags { + pPath, _ := syscall.BytePtrFromString(fullPath) if _, _, errno := syscall.Syscall(syscall.SYS_LCHFLAGS, - uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), - uintptr(binary.LittleEndian.Uint32((v))), 0); errno != 0 { + uintptr(unsafe.Pointer(pPath)), + uintptr(flags), 0); errno != 0 { return os.NewSyscallError("lchflags", errno) } }