mirror of
https://github.com/jkl1337/duplicacy.git
synced 2026-01-02 11:44:45 -06:00
Initial implementation of file/inode flags (Linux, BSD, darwin)
Basic support for BSD and Darwin style chflags (stat flags). Applies these flags at the end of file restore. Supports linux style ioctl_iflags(2) in a 2 step process. Flags that need to be applied prior to writes such as compress and especially no-COW are applied immediately upon file open. The flags format is backwards compatible. An attribute starting with a null byte is used to store flags in the entry attributes table. With an old version of duplicacy the restore of this attribute should silently fail (effectively be ignored). Fixes xattr restore to use O_NOFOLLOW so attributes are applied to symlink. TODO: Tests, possible option to switch off mutable/append prior to restore of existing file similar to rsync. Does not apply attributes or flags to the top most directory.
This commit is contained in:
@@ -780,6 +780,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
remoteEntry.RestoreEarlyDirFlags(fullPath)
|
||||||
directoryEntries = append(directoryEntries, remoteEntry)
|
directoryEntries = append(directoryEntries, remoteEntry)
|
||||||
} else {
|
} else {
|
||||||
// We can't download files here since fileEntries needs to be sorted
|
// We can't download files here since fileEntries needs to be sorted
|
||||||
@@ -1194,6 +1195,7 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
|||||||
LOG_ERROR("DOWNLOAD_CREATE", "Failed to create the file %s for in-place writing: %v", fullPath, err)
|
LOG_ERROR("DOWNLOAD_CREATE", "Failed to create the file %s for in-place writing: %v", fullPath, err)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
entry.RestoreEarlyFileFlags(existingFile)
|
||||||
|
|
||||||
n := int64(1)
|
n := int64(1)
|
||||||
// There is a go bug on Windows (https://github.com/golang/go/issues/21681) that causes Seek to fail
|
// There is a go bug on Windows (https://github.com/golang/go/issues/21681) that causes Seek to fail
|
||||||
@@ -1377,6 +1379,7 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entry.RestoreEarlyFileFlags(existingFile)
|
||||||
|
|
||||||
existingFile.Seek(0, 0)
|
existingFile.Seek(0, 0)
|
||||||
|
|
||||||
@@ -1459,6 +1462,7 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
|||||||
LOG_ERROR("DOWNLOAD_OPEN", "Failed to open file for writing: %v", err)
|
LOG_ERROR("DOWNLOAD_OPEN", "Failed to open file for writing: %v", err)
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
entry.RestoreEarlyFileFlags(newFile)
|
||||||
|
|
||||||
hasher := manager.config.NewFileHasher()
|
hasher := manager.config.NewFileHasher()
|
||||||
|
|
||||||
|
|||||||
@@ -771,6 +771,12 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f.Mode()&(os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
|
||||||
|
LOG_WARN("LIST_SKIP", "Skipped non-regular file %s", entry.Path)
|
||||||
|
skippedFiles = append(skippedFiles, entry.Path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
entry.ReadAttributes(top)
|
entry.ReadAttributes(top)
|
||||||
|
|
||||||
if excludeByAttribute && entry.Attributes != nil && excludedByAttribute(*entry.Attributes) {
|
if excludeByAttribute && entry.Attributes != nil && excludedByAttribute(*entry.Attributes) {
|
||||||
@@ -778,12 +784,6 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Mode()&(os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
|
|
||||||
LOG_WARN("LIST_SKIP", "Skipped non-regular file %s", entry.Path)
|
|
||||||
skippedFiles = append(skippedFiles, entry.Path)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
directoryList = append(directoryList, entry)
|
directoryList = append(directoryList, entry)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
53
src/duplicacy_utils_bsd.go
Normal file
53
src/duplicacy_utils_bsd.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) Acrosync LLC. All rights reserved.
|
||||||
|
// Free for personal use and commercial trial
|
||||||
|
// Commercial use requires per-user licenses available from https://duplicacy.com
|
||||||
|
|
||||||
|
//go:build freebsd || netbsd || darwin
|
||||||
|
// +build freebsd netbsd darwin
|
||||||
|
|
||||||
|
package duplicacy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bsdFileFlagsKey = "\x00bf"
|
||||||
|
|
||||||
|
func (entry *Entry) ReadFileFlags(f *os.File) error {
|
||||||
|
fileInfo, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stat, ok := fileInfo.Sys().(*syscall.Stat_t)
|
||||||
|
if ok && 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) RestoreEarlyDirFlags(path string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) RestoreLateFileFlags(f *os.File) error {
|
||||||
|
if entry.Attributes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if v, have := (*entry.Attributes)[bsdFileFlagsKey]; have {
|
||||||
|
LOG_DEBUG("ATTR_RESTORE", "Restore flags 0x%x for %s", binary.LittleEndian.Uint32(v), entry.Path)
|
||||||
|
return syscall.Fchflags(int(f.Fd()), int(binary.LittleEndian.Uint32(v)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
112
src/duplicacy_utils_linux.go
Normal file
112
src/duplicacy_utils_linux.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) Acrosync LLC. All rights reserved.
|
||||||
|
// Free for personal use and commercial trial
|
||||||
|
// Commercial use requires per-user licenses available from https://duplicacy.com
|
||||||
|
|
||||||
|
package duplicacy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
linux_FS_SECRM_FL = 0x00000001 /* Secure deletion */
|
||||||
|
linux_FS_UNRM_FL = 0x00000002 /* Undelete */
|
||||||
|
linux_FS_COMPR_FL = 0x00000004 /* Compress file */
|
||||||
|
linux_FS_SYNC_FL = 0x00000008 /* Synchronous updates */
|
||||||
|
linux_FS_IMMUTABLE_FL = 0x00000010 /* Immutable file */
|
||||||
|
linux_FS_APPEND_FL = 0x00000020 /* writes to file may only append */
|
||||||
|
linux_FS_NODUMP_FL = 0x00000040 /* do not dump file */
|
||||||
|
linux_FS_NOATIME_FL = 0x00000080 /* do not update atime */
|
||||||
|
linux_FS_NOCOMP_FL = 0x00000400 /* Don't compress */
|
||||||
|
linux_FS_JOURNAL_DATA_FL = 0x00004000 /* Reserved for ext3 */
|
||||||
|
linux_FS_NOTAIL_FL = 0x00008000 /* file tail should not be merged */
|
||||||
|
linux_FS_DIRSYNC_FL = 0x00010000 /* dirsync behaviour (directories only) */
|
||||||
|
linux_FS_TOPDIR_FL = 0x00020000 /* Top of directory hierarchies*/
|
||||||
|
linux_FS_NOCOW_FL = 0x00800000 /* Do not cow file */
|
||||||
|
linux_FS_PROJINHERIT_FL = 0x20000000 /* Create with parents projid */
|
||||||
|
|
||||||
|
linux_FS_IOC_GETFLAGS uintptr = 0x80086601
|
||||||
|
linux_FS_IOC_SETFLAGS uintptr = 0x40086602
|
||||||
|
|
||||||
|
linuxIocFlagsFileEarly = linux_FS_SECRM_FL | linux_FS_UNRM_FL | linux_FS_COMPR_FL | linux_FS_NODUMP_FL | linux_FS_NOATIME_FL | linux_FS_NOCOMP_FL | linux_FS_JOURNAL_DATA_FL | linux_FS_NOTAIL_FL | linux_FS_NOCOW_FL
|
||||||
|
linuxIocFlagsDirEarly = linux_FS_TOPDIR_FL | linux_FS_PROJINHERIT_FL
|
||||||
|
linuxIocFlagsLate = linux_FS_SYNC_FL | linux_FS_IMMUTABLE_FL | linux_FS_APPEND_FL | linux_FS_DIRSYNC_FL
|
||||||
|
|
||||||
|
linuxFileFlagsKey = "\x00lf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ioctl(f *os.File, request uintptr, attrp *uint32) error {
|
||||||
|
argp := uintptr(unsafe.Pointer(attrp))
|
||||||
|
|
||||||
|
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, argp); errno != 0 {
|
||||||
|
return os.NewSyscallError("ioctl", errno)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) ReadFileFlags(f *os.File) error {
|
||||||
|
var flags uint32
|
||||||
|
if err := ioctl(f, linux_FS_IOC_GETFLAGS, &flags); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if flags != 0 {
|
||||||
|
if entry.Attributes == nil {
|
||||||
|
entry.Attributes = &map[string][]byte{}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
if entry.Attributes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error {
|
||||||
|
if entry.Attributes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) RestoreLateFileFlags(f *os.File) error {
|
||||||
|
if entry.Attributes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func excludedByAttribute(attributes map[string][]byte) bool {
|
||||||
|
_, ok := attributes["user.duplicacy_exclude"]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
@@ -48,9 +48,12 @@ func SetOwner(fullPath string, entry *Entry, fileInfo *os.FileInfo) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) ReadAttributes(top string) {
|
func (entry *Entry) ReadAttributes(top string) {
|
||||||
|
|
||||||
fullPath := filepath.Join(top, entry.Path)
|
fullPath := filepath.Join(top, entry.Path)
|
||||||
attributes, _ := xattr.List(fullPath)
|
f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
attributes, _ := xattr.FList(f)
|
||||||
if len(attributes) > 0 {
|
if len(attributes) > 0 {
|
||||||
entry.Attributes = &map[string][]byte{}
|
entry.Attributes = &map[string][]byte{}
|
||||||
for _, name := range attributes {
|
for _, name := range attributes {
|
||||||
@@ -60,30 +63,42 @@ func (entry *Entry) ReadAttributes(top string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := entry.ReadFileFlags(f); err != nil {
|
||||||
|
LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (entry *Entry) SetAttributesToFile(fullPath string) {
|
func (entry *Entry) SetAttributesToFile(fullPath string) {
|
||||||
names, _ := xattr.List(fullPath)
|
f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
names, _ := xattr.FList(f)
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
|
|
||||||
|
|
||||||
newAttribute, found := (*entry.Attributes)[name]
|
newAttribute, found := (*entry.Attributes)[name]
|
||||||
if found {
|
if found {
|
||||||
oldAttribute, _ := xattr.Get(fullPath, name)
|
oldAttribute, _ := xattr.FGet(f, name)
|
||||||
if !bytes.Equal(oldAttribute, newAttribute) {
|
if !bytes.Equal(oldAttribute, newAttribute) {
|
||||||
xattr.Set(fullPath, name, newAttribute)
|
xattr.FSet(f, name, newAttribute)
|
||||||
}
|
}
|
||||||
delete(*entry.Attributes, name)
|
delete(*entry.Attributes, name)
|
||||||
} else {
|
} else {
|
||||||
xattr.Remove(fullPath, name)
|
xattr.FRemove(f, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for name, attribute := range *entry.Attributes {
|
for name, attribute := range *entry.Attributes {
|
||||||
xattr.Set(fullPath, name, attribute)
|
if len(name) > 0 && name[0] == '\x00' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
xattr.FSet(f, name, attribute)
|
||||||
}
|
}
|
||||||
|
if err := entry.RestoreLateFileFlags(f); err != nil {
|
||||||
|
LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinPath(components ...string) string {
|
func joinPath(components ...string) string {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
// Free for personal use and commercial trial
|
// Free for personal use and commercial trial
|
||||||
// Commercial use requires per-user licenses available from https://duplicacy.com
|
// Commercial use requires per-user licenses available from https://duplicacy.com
|
||||||
|
|
||||||
//go:build freebsd || netbsd || linux || solaris
|
//go:build freebsd || netbsd || solaris
|
||||||
// +build freebsd netbsd linux solaris
|
// +build freebsd netbsd solaris
|
||||||
|
|
||||||
package duplicacy
|
package duplicacy
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,18 @@ func SplitDir(fullPath string) (dir string, file string) {
|
|||||||
return fullPath[:i+1], fullPath[i+1:]
|
return fullPath[:i+1], fullPath[i+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func excludedByAttribute(attributes map[string][]byte) bool {
|
func (entry *Entry) ReadFileFlags(f *os.File) error {
|
||||||
return false
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) RestoreEarlyDirFlags(path string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *Entry) RestoreLateFileFlags(f *os.File) error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user