Compare commits

..

6 Commits

Author SHA1 Message Date
498a948b34 gofmt some files 2023-10-04 01:23:03 -05:00
2679751896 Fix handling of hardlinks and special files
Also, don't attempt xattr ops on special files on the BSD likes.

TODO: Should have a way to allow restore without special files,
otherwise very cumbersome for a regular user.
2023-10-04 01:12:13 -05:00
66a938af67 Support for hardlinks to symlinks
This is a specialized use-case, but it is indeed possible to do this
and can be used if xattrs are attached to the symlink.
2023-10-04 01:12:11 -05:00
015d2200da Fix handling of xattrs with symlinks
Fix Linux, Darwin, and other BSD (untested) to allow proper
handling of xattrs with symlinks. On Linux we cannot use the f*
syscalls for symlinks because symlinks cannot be opened.
File flags must be handled differently on darwin and other BSD due
to the lack of the LCHFLAGS syscall on darwin, and the fact that it
is emulated in libc. However, we do have O_SYMLINK on darwin.
2023-10-04 01:11:20 -05:00
96e7c93a2c Support backup and restore of special files on POSIX style systems
Special files are device nodes and named pipes. The necessity of the
former is clear, the latter is debatable.
In order to preserve backward compatibility, the device number is
encoded in the StartChunk/StartOffset fields of the entry.
2023-10-03 16:26:19 -05:00
f06779659e Don't overwrite symlinks if file already exists 2023-10-03 15:08:47 -05:00
11 changed files with 557 additions and 194 deletions

View File

@@ -21,7 +21,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/vmihailenco/msgpack" "github.com/vmihailenco/msgpack"
) )
// BackupManager performs the two major operations, backup and restore, and passes other operations, mostly related to // BackupManager performs the two major operations, backup and restore, and passes other operations, mostly related to
@@ -36,9 +36,9 @@ type BackupManager struct {
config *Config // contains a number of options config *Config // contains a number of options
nobackupFile string // don't backup directory when this file name is found nobackupFile string // don't backup directory when this file name is found
filtersFile string // the path to the filters file filtersFile string // the path to the filters file
excludeByAttribute bool // don't backup file based on file attribute excludeByAttribute bool // don't backup file based on file attribute
cachePath string cachePath string
} }
@@ -146,7 +146,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
if manager.config.DataShards != 0 && manager.config.ParityShards != 0 { if manager.config.DataShards != 0 && manager.config.ParityShards != 0 {
LOG_INFO("BACKUP_ERASURECODING", "Erasure coding is enabled with %d data shards and %d parity shards", LOG_INFO("BACKUP_ERASURECODING", "Erasure coding is enabled with %d data shards and %d parity shards",
manager.config.DataShards, manager.config.ParityShards) manager.config.DataShards, manager.config.ParityShards)
} }
if manager.config.rsaPublicKey != nil && len(manager.config.FileKey) > 0 { if manager.config.rsaPublicKey != nil && len(manager.config.FileKey) > 0 {
@@ -187,7 +187,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
// If the listing operation is fast and this is an initial backup, list all chunks and // If the listing operation is fast and this is an initial backup, list all chunks and
// put them in the cache. // put them in the cache.
if (manager.storage.IsFastListing() && remoteSnapshot.Revision == 0) { if manager.storage.IsFastListing() && remoteSnapshot.Revision == 0 {
LOG_INFO("BACKUP_LIST", "Listing all chunks") LOG_INFO("BACKUP_LIST", "Listing all chunks")
allChunks, _ := manager.SnapshotManager.ListAllFiles(manager.storage, "chunks/") allChunks, _ := manager.SnapshotManager.ListAllFiles(manager.storage, "chunks/")
@@ -222,7 +222,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
var totalModifiedFileSize int64 // total size of modified files var totalModifiedFileSize int64 // total size of modified files
var uploadedModifiedFileSize int64 // portions that have been uploaded (including cache hits) var uploadedModifiedFileSize int64 // portions that have been uploaded (including cache hits)
var preservedFileSize int64 // total size of unmodified files var preservedFileSize int64 // total size of unmodified files
localSnapshot := CreateEmptySnapshot(manager.snapshotID) localSnapshot := CreateEmptySnapshot(manager.snapshotID)
localSnapshot.Revision = remoteSnapshot.Revision + 1 localSnapshot.Revision = remoteSnapshot.Revision + 1
@@ -239,7 +239,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
// List local files // List local files
defer CatchLogException() defer CatchLogException()
localSnapshot.ListLocalFiles(shadowTop, manager.nobackupFile, manager.filtersFile, manager.excludeByAttribute, localListingChannel, &skippedDirectories, &skippedFiles) localSnapshot.ListLocalFiles(shadowTop, manager.nobackupFile, manager.filtersFile, manager.excludeByAttribute, localListingChannel, &skippedDirectories, &skippedFiles)
} () }()
go func() { go func() {
// List remote files // List remote files
@@ -261,7 +261,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
}) })
} }
close(remoteListingChannel) close(remoteListingChannel)
} () }()
// Create the local file list // Create the local file list
localEntryList, err := CreateEntryList(manager.snapshotID, manager.cachePath, maximumInMemoryEntries) localEntryList, err := CreateEntryList(manager.snapshotID, manager.cachePath, maximumInMemoryEntries)
@@ -275,7 +275,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
var remoteEntry *Entry var remoteEntry *Entry
remoteListingOK := true remoteListingOK := true
for { for {
localEntry := <- localListingChannel localEntry := <-localListingChannel
if localEntry == nil { if localEntry == nil {
break break
} }
@@ -289,7 +289,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
compareResult = localEntry.Compare(remoteEntry) compareResult = localEntry.Compare(remoteEntry)
} else { } else {
if remoteListingOK { if remoteListingOK {
remoteEntry, remoteListingOK = <- remoteListingChannel remoteEntry, remoteListingOK = <-remoteListingChannel
} }
if !remoteListingOK { if !remoteListingOK {
compareResult = -1 compareResult = -1
@@ -304,7 +304,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
remoteEntry = nil remoteEntry = nil
} }
if compareResult == 0 { if compareResult == 0 {
// No need to check if it is in hash mode -- in that case remote listing is nil // No need to check if it is in hash mode -- in that case remote listing is nil
if localEntry.IsSameAs(remoteEntry) && localEntry.IsFile() { if localEntry.IsSameAs(remoteEntry) && localEntry.IsFile() {
if localEntry.Size > 0 { if localEntry.Size > 0 {
@@ -339,8 +339,8 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
// compareResult must be < 0; the local file is new // compareResult must be < 0; the local file is new
totalModifiedFileSize += localEntry.Size totalModifiedFileSize += localEntry.Size
if localEntry.Size > 0 { if localEntry.Size > 0 {
// A size of -1 indicates this is a modified file that will be uploaded // A size of -1 indicates this is a modified file that will be uploaded
localEntry.Size = -1 localEntry.Size = -1
} }
} }
@@ -448,7 +448,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
_, found := chunkCache[chunkID] _, found := chunkCache[chunkID]
if found { if found {
if time.Now().Unix() - lastUploadingTime > keepUploadAlive { if time.Now().Unix()-lastUploadingTime > keepUploadAlive {
LOG_INFO("UPLOAD_KEEPALIVE", "Skip chunk cache to keep connection alive") LOG_INFO("UPLOAD_KEEPALIVE", "Skip chunk cache to keep connection alive")
found = false found = false
} }
@@ -558,7 +558,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
if showStatistics { if showStatistics {
LOG_INFO("BACKUP_STATS", "Files: %d total, %s bytes; %d new, %s bytes", LOG_INFO("BACKUP_STATS", "Files: %d total, %s bytes; %d new, %s bytes",
localEntryList.NumberOfEntries - int64(len(skippedFiles)), localEntryList.NumberOfEntries-int64(len(skippedFiles)),
PrettyNumber(preservedFileSize+uploadedFileSize), PrettyNumber(preservedFileSize+uploadedFileSize),
len(localEntryList.ModifiedEntries), PrettyNumber(uploadedFileSize)) len(localEntryList.ModifiedEntries), PrettyNumber(uploadedFileSize))
@@ -686,7 +686,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
// List local files // List local files
defer CatchLogException() defer CatchLogException()
localSnapshot.ListLocalFiles(top, manager.nobackupFile, manager.filtersFile, manager.excludeByAttribute, localListingChannel, nil, nil) localSnapshot.ListLocalFiles(top, manager.nobackupFile, manager.filtersFile, manager.excludeByAttribute, localListingChannel, nil, nil)
} () }()
remoteSnapshot := manager.SnapshotManager.DownloadSnapshot(manager.snapshotID, revision) remoteSnapshot := manager.SnapshotManager.DownloadSnapshot(manager.snapshotID, revision)
manager.SnapshotManager.DownloadSnapshotSequences(remoteSnapshot) manager.SnapshotManager.DownloadSnapshotSequences(remoteSnapshot)
@@ -698,18 +698,41 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
return true return true
}) })
close(remoteListingChannel) close(remoteListingChannel)
} () }()
var localEntry *Entry var localEntry *Entry
localListingOK := true localListingOK := true
type hardLinkEntry struct { type hardLinkEntry struct {
entry *Entry entry *Entry
willDownload bool willExist bool
} }
var hardLinkTable []hardLinkEntry var hardLinkTable []hardLinkEntry
var hardLinks []*Entry var hardLinks []*Entry
restoreHardlink := func(entry *Entry, fullPath string) bool {
if entry.IsHardlinkRoot() {
hardLinkTable[len(hardLinkTable)-1].willExist = true
} else if entry.IsHardlinkedFrom() {
i, err := entry.GetHardlinkId()
if err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Decode error for hardlinked entry %s, %v", entry.Path, err)
return false
}
if !hardLinkTable[i].willExist {
hardLinkTable[i] = hardLinkEntry{entry, true}
} else {
sourcePath := joinPath(top, hardLinkTable[i].entry.Path)
LOG_INFO("RESTORE_HARDLINK", "Hard linking %s to %s", fullPath, sourcePath)
if err := MakeHardlink(sourcePath, fullPath); err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Failed to create hard link %s to %s %v", fullPath, sourcePath, err)
}
return true
}
}
return false
}
for remoteEntry := range remoteListingChannel { for remoteEntry := range remoteListingChannel {
if remoteEntry.IsHardlinkRoot() { if remoteEntry.IsHardlinkRoot() {
@@ -725,7 +748,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
for { for {
if localEntry == nil && localListingOK { if localEntry == nil && localListingOK {
localEntry, localListingOK = <- localListingChannel localEntry, localListingOK = <-localListingChannel
} }
if localEntry == nil { if localEntry == nil {
compareResult = 1 compareResult = 1
@@ -752,26 +775,39 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
} }
fullPath := joinPath(top, remoteEntry.Path) fullPath := joinPath(top, remoteEntry.Path)
if remoteEntry.IsLink() { if remoteEntry.IsLink() {
stat, err := os.Lstat(fullPath) if stat, _ := os.Lstat(fullPath); stat != nil {
if stat != nil {
if stat.Mode()&os.ModeSymlink != 0 { if stat.Mode()&os.ModeSymlink != 0 {
isRegular, link, err := Readlink(fullPath) isRegular, link, err := Readlink(fullPath)
if err == nil && link == remoteEntry.Link && !isRegular { if err == nil && link == remoteEntry.Link && !isRegular {
remoteEntry.RestoreMetadata(fullPath, nil, setOwner) remoteEntry.RestoreMetadata(fullPath, nil, setOwner)
if remoteEntry.IsHardlinkRoot() {
hardLinkTable[len(hardLinkTable)-1].willExist = true
}
continue continue
} }
} }
if !overwrite {
LOG_WERROR(allowFailures, "DOWNLOAD_OVERWRITE",
"File %s already exists. Please specify the -overwrite option to overwrite", remoteEntry.Path)
continue
}
os.Remove(fullPath) os.Remove(fullPath)
} }
err = os.Symlink(remoteEntry.Link, fullPath) if restoreHardlink(remoteEntry, fullPath) {
if err != nil { continue
}
if err := os.Symlink(remoteEntry.Link, fullPath); err != nil {
LOG_ERROR("RESTORE_SYMLINK", "Can't create symlink %s: %v", remoteEntry.Path, err) LOG_ERROR("RESTORE_SYMLINK", "Can't create symlink %s: %v", remoteEntry.Path, err)
return 0 return 0
} }
remoteEntry.RestoreMetadata(fullPath, nil, setOwner) remoteEntry.RestoreMetadata(fullPath, nil, setOwner)
LOG_TRACE("DOWNLOAD_DONE", "Symlink %s updated", remoteEntry.Path) LOG_TRACE("DOWNLOAD_DONE", "Symlink %s updated", remoteEntry.Path)
} else if remoteEntry.IsDir() { } else if remoteEntry.IsDir() {
@@ -793,22 +829,48 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
} }
remoteEntry.RestoreEarlyDirFlags(fullPath) remoteEntry.RestoreEarlyDirFlags(fullPath)
directoryEntries = append(directoryEntries, remoteEntry) directoryEntries = append(directoryEntries, remoteEntry)
} else if remoteEntry.IsSpecial() {
if stat, _ := os.Lstat(fullPath); stat != nil {
if remoteEntry.IsSameSpecial(stat) {
remoteEntry.RestoreMetadata(fullPath, nil, setOwner)
if remoteEntry.IsHardlinkRoot() {
hardLinkTable[len(hardLinkTable)-1].willExist = true
}
}
if !overwrite {
LOG_WERROR(allowFailures, "DOWNLOAD_OVERWRITE",
"File %s already exists. Please specify the -overwrite option to overwrite", remoteEntry.Path)
continue
}
os.Remove(fullPath)
}
if restoreHardlink(remoteEntry, fullPath) {
continue
}
if err := remoteEntry.RestoreSpecial(fullPath); err != nil {
LOG_ERROR("RESTORE_SPECIAL", "Unable to restore special file %s: %v", remoteEntry.Path, err)
return 0
}
remoteEntry.RestoreMetadata(fullPath, nil, setOwner)
} else { } else {
if remoteEntry.IsHardlinkRoot() { if remoteEntry.IsHardlinkRoot() {
hardLinkTable[len(hardLinkTable)-1] = hardLinkEntry{remoteEntry, true} hardLinkTable[len(hardLinkTable)-1].willExist = true
} else if remoteEntry.IsHardlinkedFrom() { } else if remoteEntry.IsHardlinkedFrom() {
i, err := strconv.ParseUint(remoteEntry.Link, 16, 64) i, err := remoteEntry.GetHardlinkId()
if err != nil { if err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Decode error in hardlink entry, expected hex int, got %s", remoteEntry.Link) LOG_ERROR("RESTORE_HARDLINK", "Decode error for hardlinked entry %s, %v", remoteEntry.Path, err)
return 0 return 0
} }
if !hardLinkTable[i].willDownload { if !hardLinkTable[i].willExist {
hardLinkTable[i] = hardLinkEntry{remoteEntry, true} hardLinkTable[i] = hardLinkEntry{remoteEntry, true}
} else { } else {
hardLinks = append(hardLinks, remoteEntry) hardLinks = append(hardLinks, remoteEntry)
continue continue
} }
} }
// We can't download files here since fileEntries needs to be sorted // We can't download files here since fileEntries needs to be sorted
fileEntries = append(fileEntries, remoteEntry) fileEntries = append(fileEntries, remoteEntry)
totalFileSize += remoteEntry.Size totalFileSize += remoteEntry.Size
@@ -820,7 +882,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
} }
for localListingOK { for localListingOK {
localEntry, localListingOK = <- localListingChannel localEntry, localListingOK = <-localListingChannel
if localEntry != nil { if localEntry != nil {
extraFiles = append(extraFiles, localEntry.Path) extraFiles = append(extraFiles, localEntry.Path)
} }
@@ -929,11 +991,32 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
} }
for _, linkEntry := range hardLinks { for _, linkEntry := range hardLinks {
i, _ := strconv.ParseUint(linkEntry.Link, 16, 64)
i, _ := linkEntry.GetHardlinkId()
sourcePath := joinPath(top, hardLinkTable[i].entry.Path) sourcePath := joinPath(top, hardLinkTable[i].entry.Path)
fullPath := joinPath(top, linkEntry.Path) fullPath := joinPath(top, linkEntry.Path)
LOG_INFO("RESTORE_HARDLINK", "Hard linking %s to %s", fullPath, sourcePath)
if err := os.Link(sourcePath, fullPath); err != nil { if stat, _ := os.Lstat(fullPath); stat != nil {
sourceStat, _ := os.Lstat(sourcePath)
if os.SameFile(stat, sourceStat) {
continue
}
if sourceStat == nil {
LOG_WERROR(allowFailures, "RESTORE_HARDLINK",
"Target %s for hardlink %s is missing", sourcePath, linkEntry.Path)
continue
}
if !overwrite {
LOG_WERROR(allowFailures, "DOWNLOAD_OVERWRITE",
"File %s already exists. Please specify the -overwrite option to overwrite", linkEntry.Path)
continue
}
os.Remove(fullPath)
}
LOG_DEBUG("RESTORE_HARDLINK", "Hard linking %s to %s", fullPath, sourcePath)
if err := MakeHardlink(sourcePath, fullPath); err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Failed to create hard link %s to %s", fullPath, sourcePath) LOG_ERROR("RESTORE_HARDLINK", "Failed to create hard link %s to %s", fullPath, sourcePath)
return 0 return 0
} }
@@ -1084,14 +1167,14 @@ func (manager *BackupManager) UploadSnapshot(chunkOperator *ChunkOperator, top s
encoder := msgpack.NewEncoder(buffer) encoder := msgpack.NewEncoder(buffer)
metadataChunkMaker := CreateMetaDataChunkMaker(manager.config, metadataChunkSize) metadataChunkMaker := CreateMetaDataChunkMaker(manager.config, metadataChunkSize)
var chunkHashes []string var chunkHashes []string
var chunkLengths []int var chunkLengths []int
lastChunk := -1 lastChunk := -1
lastEndChunk := 0 lastEndChunk := 0
type hardLinkEntry struct { type hardLinkEntry struct {
entry *Entry entry *Entry
startChunk int startChunk int
} }
var hardLinkTable []hardLinkEntry var hardLinkTable []hardLinkEntry
@@ -1123,10 +1206,10 @@ func (manager *BackupManager) UploadSnapshot(chunkOperator *ChunkOperator, top s
entry.StartChunk -= lastEndChunk entry.StartChunk -= lastEndChunk
lastEndChunk = entry.EndChunk lastEndChunk = entry.EndChunk
entry.EndChunk = delta entry.EndChunk = delta
} else if entry.IsHardlinkedFrom() { } else if entry.IsHardlinkedFrom() && !entry.IsLink() {
i, err := strconv.ParseUint(entry.Link, 16, 64) i, err := entry.GetHardlinkId()
if err != nil { if err != nil {
LOG_ERROR("SNAPSHOT_UPLOAD", "Decode error in hardlink entry, expected hex int, got %s", entry.Link) LOG_ERROR("SNAPSHOT_UPLOAD", "Decode error for hardlinked entry %s, %v", entry.Link, err)
return err return err
} }
@@ -1214,9 +1297,10 @@ func (manager *BackupManager) UploadSnapshot(chunkOperator *ChunkOperator, top s
// Restore downloads a file from the storage. If 'inPlace' is false, the download file is saved first to a temporary // Restore downloads a file from the storage. If 'inPlace' is false, the download file is saved first to a temporary
// file under the .duplicacy directory and then replaces the existing one. Otherwise, the existing file will be // file under the .duplicacy directory and then replaces the existing one. Otherwise, the existing file will be
// overwritten directly. // overwritten directly.
// Return: true, nil: Restored file; // Return: true, nil: Restored file;
// false, nil: Skipped file; //
// false, error: Failure to restore file (only if allowFailures == true) // false, nil: Skipped file;
// false, error: Failure to restore file (only if allowFailures == true)
func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chunkMaker *ChunkMaker, entry *Entry, top string, inPlace bool, overwrite bool, func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chunkMaker *ChunkMaker, entry *Entry, top string, inPlace bool, overwrite bool,
showStatistics bool, totalFileSize int64, downloadedFileSize int64, startTime int64, allowFailures bool) (bool, error) { showStatistics bool, totalFileSize int64, downloadedFileSize int64, startTime int64, allowFailures bool) (bool, error) {
@@ -1392,7 +1476,7 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
// fileHash != entry.Hash, warn/error depending on -overwrite option // fileHash != entry.Hash, warn/error depending on -overwrite option
if !overwrite && !isNewFile { if !overwrite && !isNewFile {
LOG_WERROR(allowFailures, "DOWNLOAD_OVERWRITE", LOG_WERROR(allowFailures, "DOWNLOAD_OVERWRITE",
"File %s already exists. Please specify the -overwrite option to overwrite", entry.Path) "File %s already exists. Please specify the -overwrite option to overwrite", entry.Path)
return false, fmt.Errorf("file exists") return false, fmt.Errorf("file exists")
} }
@@ -1639,7 +1723,7 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
if otherManager.config.DataShards != 0 && otherManager.config.ParityShards != 0 { if otherManager.config.DataShards != 0 && otherManager.config.ParityShards != 0 {
LOG_INFO("BACKUP_ERASURECODING", "Erasure coding is enabled for the destination storage with %d data shards and %d parity shards", LOG_INFO("BACKUP_ERASURECODING", "Erasure coding is enabled for the destination storage with %d data shards and %d parity shards",
otherManager.config.DataShards, otherManager.config.ParityShards) otherManager.config.DataShards, otherManager.config.ParityShards)
} }
if otherManager.config.rsaPublicKey != nil && len(otherManager.config.FileKey) > 0 { if otherManager.config.rsaPublicKey != nil && len(otherManager.config.FileKey) > 0 {
@@ -1740,15 +1824,15 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
LOG_TRACE("SNAPSHOT_COPY", "Copying snapshot %s at revision %d", snapshot.ID, snapshot.Revision) LOG_TRACE("SNAPSHOT_COPY", "Copying snapshot %s at revision %d", snapshot.ID, snapshot.Revision)
for _, chunkHash := range snapshot.FileSequence { for _, chunkHash := range snapshot.FileSequence {
chunks[chunkHash] = true // The chunk is a snapshot chunk chunks[chunkHash] = true // The chunk is a snapshot chunk
} }
for _, chunkHash := range snapshot.ChunkSequence { for _, chunkHash := range snapshot.ChunkSequence {
chunks[chunkHash] = true // The chunk is a snapshot chunk chunks[chunkHash] = true // The chunk is a snapshot chunk
} }
for _, chunkHash := range snapshot.LengthSequence { for _, chunkHash := range snapshot.LengthSequence {
chunks[chunkHash] = true // The chunk is a snapshot chunk chunks[chunkHash] = true // The chunk is a snapshot chunk
} }
description := manager.SnapshotManager.DownloadSequence(snapshot.ChunkSequence) description := manager.SnapshotManager.DownloadSequence(snapshot.ChunkSequence)
@@ -1761,7 +1845,7 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
for _, chunkHash := range snapshot.ChunkHashes { for _, chunkHash := range snapshot.ChunkHashes {
if _, found := chunks[chunkHash]; !found { if _, found := chunks[chunkHash]; !found {
chunks[chunkHash] = false // The chunk is a file chunk chunks[chunkHash] = false // The chunk is a file chunk
} }
} }
@@ -1793,7 +1877,7 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
} }
} }
LOG_INFO("SNAPSHOT_COPY", "Chunks to copy: %d, to skip: %d, total: %d", len(chunksToCopy), len(chunks) - len(chunksToCopy), len(chunks)) LOG_INFO("SNAPSHOT_COPY", "Chunks to copy: %d, to skip: %d, total: %d", len(chunksToCopy), len(chunks)-len(chunksToCopy), len(chunks))
chunkDownloader := CreateChunkOperator(manager.config, manager.storage, nil, false, false, downloadingThreads, false) chunkDownloader := CreateChunkOperator(manager.config, manager.storage, nil, false, false, downloadingThreads, false)
@@ -1802,7 +1886,7 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
copiedChunks := 0 copiedChunks := 0
chunkUploader := CreateChunkOperator(otherManager.config, otherManager.storage, nil, false, false, uploadingThreads, false) chunkUploader := CreateChunkOperator(otherManager.config, otherManager.storage, nil, false, false, uploadingThreads, false)
chunkUploader.UploadCompletionFunc = func(chunk *Chunk, chunkIndex int, skipped bool, chunkSize int, uploadSize int) { chunkUploader.UploadCompletionFunc = func(chunk *Chunk, chunkIndex int, skipped bool, chunkSize int, uploadSize int) {
action := "Skipped" action := "Skipped"
if !skipped { if !skipped {
copiedChunks++ copiedChunks++
@@ -1813,11 +1897,11 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
elapsedTime := time.Now().Sub(startTime).Seconds() elapsedTime := time.Now().Sub(startTime).Seconds()
speed := int64(float64(atomic.LoadInt64(&uploadedBytes)) / elapsedTime) speed := int64(float64(atomic.LoadInt64(&uploadedBytes)) / elapsedTime)
remainingTime := int64(float64(len(chunksToCopy) - chunkIndex - 1) / float64(chunkIndex + 1) * elapsedTime) remainingTime := int64(float64(len(chunksToCopy)-chunkIndex-1) / float64(chunkIndex+1) * elapsedTime)
percentage := float64(chunkIndex + 1) / float64(len(chunksToCopy)) * 100.0 percentage := float64(chunkIndex+1) / float64(len(chunksToCopy)) * 100.0
LOG_INFO("COPY_PROGRESS", "%s chunk %s (%d/%d) %sB/s %s %.1f%%", LOG_INFO("COPY_PROGRESS", "%s chunk %s (%d/%d) %sB/s %s %.1f%%",
action, chunk.GetID(), chunkIndex + 1, len(chunksToCopy), action, chunk.GetID(), chunkIndex+1, len(chunksToCopy),
PrettySize(speed), PrettyTime(remainingTime), percentage) PrettySize(speed), PrettyTime(remainingTime), percentage)
otherManager.config.PutChunk(chunk) otherManager.config.PutChunk(chunk)
} }
@@ -1840,7 +1924,7 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
chunkDownloader.Stop() chunkDownloader.Stop()
chunkUploader.Stop() chunkUploader.Stop()
LOG_INFO("SNAPSHOT_COPY", "Copied %d new chunks and skipped %d existing chunks", copiedChunks, len(chunks) - copiedChunks) LOG_INFO("SNAPSHOT_COPY", "Copied %d new chunks and skipped %d existing chunks", copiedChunks, len(chunks)-copiedChunks)
for _, snapshot := range snapshots { for _, snapshot := range snapshots {
if revisionMap[snapshot.ID][snapshot.Revision] == false { if revisionMap[snapshot.ID][snapshot.Revision] == false {

View File

@@ -8,6 +8,7 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -23,6 +24,11 @@ import (
"github.com/vmihailenco/msgpack" "github.com/vmihailenco/msgpack"
) )
const (
entrySymHardLinkRootChunkMarker = -72
entrySymHardLinkTargetChunkMarker = -73
)
// This is the hidden directory in the repository for storing various files. // This is the hidden directory in the repository for storing various files.
var DUPLICACY_DIRECTORY = ".duplicacy" var DUPLICACY_DIRECTORY = ".duplicacy"
var DUPLICACY_FILE = ".duplicacy" var DUPLICACY_FILE = ".duplicacy"
@@ -509,16 +515,36 @@ func (entry *Entry) IsLink() bool {
return entry.Mode&uint32(os.ModeSymlink) != 0 return entry.Mode&uint32(os.ModeSymlink) != 0
} }
func (entry *Entry) IsSpecial() bool {
return entry.Mode&uint32(os.ModeNamedPipe|os.ModeDevice|os.ModeCharDevice) != 0
}
func (entry *Entry) IsFileOrSpecial() bool {
return entry.Mode&uint32(os.ModeDir|os.ModeSymlink|os.ModeIrregular) == 0
}
func (entry *Entry) IsComplete() bool { func (entry *Entry) IsComplete() bool {
return entry.Size >= 0 return entry.Size >= 0
} }
func (entry *Entry) IsHardlinkedFrom() bool { func (entry *Entry) IsHardlinkedFrom() bool {
return entry.IsFile() && len(entry.Link) > 0 && entry.Link != "/" return (entry.IsFileOrSpecial() && len(entry.Link) > 0 && entry.Link != "/") || (entry.IsLink() && entry.StartChunk == entrySymHardLinkTargetChunkMarker)
} }
func (entry *Entry) IsHardlinkRoot() bool { func (entry *Entry) IsHardlinkRoot() bool {
return entry.IsFile() && entry.Link == "/" return (entry.IsFileOrSpecial() && entry.Link == "/") || (entry.IsLink() && entry.StartChunk == entrySymHardLinkRootChunkMarker)
}
func (entry *Entry) GetHardlinkId() (int, error) {
if entry.IsLink() {
if entry.StartChunk != entrySymHardLinkTargetChunkMarker {
return 0, errors.New("Symlink entry not marked as hardlinked")
}
return entry.StartOffset, nil
} else {
i, err := strconv.ParseUint(entry.Link, 16, 64)
return int(i), err
}
} }
func (entry *Entry) GetPermissions() os.FileMode { func (entry *Entry) GetPermissions() os.FileMode {
@@ -579,6 +605,10 @@ func (entry *Entry) RestoreMetadata(fullPath string, fileInfo *os.FileInfo, setO
} }
} }
if entry.Attributes != nil && len(*entry.Attributes) > 0 {
entry.SetAttributesToFile(fullPath)
}
// Only set the time if the file is not a symlink // Only set the time if the file is not a symlink
if !entry.IsLink() && (*fileInfo).ModTime().Unix() != entry.Time { if !entry.IsLink() && (*fileInfo).ModTime().Unix() != entry.Time {
modifiedTime := time.Unix(entry.Time, 0) modifiedTime := time.Unix(entry.Time, 0)
@@ -589,10 +619,6 @@ func (entry *Entry) RestoreMetadata(fullPath string, fileInfo *os.FileInfo, setO
} }
} }
if entry.Attributes != nil && len(*entry.Attributes) > 0 {
entry.SetAttributesToFile(fullPath)
}
return true return true
} }
@@ -781,10 +807,44 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string
if f.Name() == DUPLICACY_DIRECTORY { if f.Name() == DUPLICACY_DIRECTORY {
continue continue
} }
if f.Mode()&os.ModeSocket != 0 {
continue
}
entry := CreateEntryFromFileInfo(f, normalizedPath) entry := CreateEntryFromFileInfo(f, normalizedPath)
if len(patterns) > 0 && !MatchPath(entry.Path, patterns) { if len(patterns) > 0 && !MatchPath(entry.Path, patterns) {
continue continue
} }
var linkKey *listEntryLinkKey
if runtime.GOOS != "windows" && !entry.IsDir() {
if stat := f.Sys().(*syscall.Stat_t); stat != nil && stat.Nlink > 1 {
k := listEntryLinkKey{dev: uint64(stat.Dev), ino: uint64(stat.Ino)}
if linkIndex, seen := listingState.linkTable[k]; seen {
if linkIndex == -1 {
LOG_DEBUG("LIST_EXCLUDE", "%s is excluded by attribute (hardlink)", entry.Path)
continue
}
entry.Size = 0
if entry.IsLink() {
entry.StartChunk = entrySymHardLinkTargetChunkMarker
entry.StartOffset = linkIndex
} else {
entry.Link = strconv.FormatInt(int64(linkIndex), 16)
}
} else {
if entry.IsLink() {
entry.StartChunk = entrySymHardLinkRootChunkMarker
} else {
entry.Link = "/"
}
listingState.linkTable[k] = -1
linkKey = &k
}
}
}
if entry.IsLink() { if entry.IsLink() {
isRegular := false isRegular := false
isRegular, entry.Link, err = Readlink(joinPath(top, entry.Path)) isRegular, entry.Link, err = Readlink(joinPath(top, entry.Path))
@@ -815,29 +875,11 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string
} }
entry = newEntry entry = newEntry
} }
} } else if entry.IsSpecial() {
if !entry.ReadSpecial(f) {
if f.Mode()&(os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 { LOG_WARN("LIST_DEV", "Failed to save device node %s", entry.Path)
LOG_WARN("LIST_SKIP", "Skipped non-regular file %s", entry.Path) skippedFiles = append(skippedFiles, entry.Path)
skippedFiles = append(skippedFiles, entry.Path) continue
continue
}
var linkKey *listEntryLinkKey
if stat, ok := f.Sys().(*syscall.Stat_t); entry.IsFile() && ok && stat != nil && stat.Nlink > 1 {
k := listEntryLinkKey{dev: uint64(stat.Dev), ino: uint64(stat.Ino)}
if linkIndex, seen := listingState.linkTable[k]; seen {
if linkIndex == -1 {
LOG_DEBUG("LIST_EXCLUDE", "%s is excluded by attribute (hardlink)", entry.Path)
continue
}
entry.Size = 0
entry.Link = strconv.FormatInt(int64(linkIndex), 16)
} else {
entry.Link = "/"
listingState.linkTable[k] = -1
linkKey = &k
} }
} }

View File

@@ -111,12 +111,12 @@ func (entryList *EntryList)createOnDiskFile() error {
// Add an entry to the entry list // Add an entry to the entry list
func (entryList *EntryList)AddEntry(entry *Entry) error { func (entryList *EntryList)AddEntry(entry *Entry) error {
if !entry.IsDir() && !entry.IsLink() { if entry.IsFile() {
entryList.NumberOfEntries++ entryList.NumberOfEntries++
} }
if !entry.IsComplete() { if !entry.IsComplete() {
if entry.IsDir() || entry.IsLink() { if !entry.IsFile() {
entry.Size = 0 entry.Size = 0
} else { } else {
modifiedEntry := ModifiedEntry { modifiedEntry := ModifiedEntry {

View File

@@ -7,14 +7,15 @@ package duplicacy
import ( import (
"bufio" "bufio"
"crypto/sha256" "crypto/sha256"
"encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
"regexp" "regexp"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"runtime"
"github.com/gilbertchen/gopass" "github.com/gilbertchen/gopass"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
@@ -56,7 +57,7 @@ func IsEmptyFilter(pattern string) bool {
} }
func IsUnspecifiedFilter(pattern string) bool { func IsUnspecifiedFilter(pattern string) bool {
if pattern[0] != '+' && pattern[0] != '-' && !strings.HasPrefix(pattern, "i:") && !strings.HasPrefix(pattern, "e:") { if pattern[0] != '+' && pattern[0] != '-' && !strings.HasPrefix(pattern, "i:") && !strings.HasPrefix(pattern, "e:") {
return true return true
} else { } else {
return false return false
@@ -275,7 +276,6 @@ func SavePassword(preference Preference, passwordType string, password string) {
// The following code was modified from the online article 'Matching Wildcards: An Algorithm', by Kirk J. Krauss, // The following code was modified from the online article 'Matching Wildcards: An Algorithm', by Kirk J. Krauss,
// Dr. Dobb's, August 26, 2008. However, the version in the article doesn't handle cases like matching 'abcccd' // Dr. Dobb's, August 26, 2008. However, the version in the article doesn't handle cases like matching 'abcccd'
// against '*ccd', and the version here fixed that issue. // against '*ccd', and the version here fixed that issue.
//
func matchPattern(text string, pattern string) bool { func matchPattern(text string, pattern string) bool {
textLength := len(text) textLength := len(text)
@@ -469,8 +469,39 @@ func PrintMemoryUsage() {
runtime.ReadMemStats(&m) runtime.ReadMemStats(&m)
LOG_INFO("MEMORY_STATS", "Currently allocated: %s, total allocated: %s, system memory: %s, number of GCs: %d", LOG_INFO("MEMORY_STATS", "Currently allocated: %s, total allocated: %s, system memory: %s, number of GCs: %d",
PrettySize(int64(m.Alloc)), PrettySize(int64(m.TotalAlloc)), PrettySize(int64(m.Sys)), m.NumGC) PrettySize(int64(m.Alloc)), PrettySize(int64(m.TotalAlloc)), PrettySize(int64(m.Sys)), m.NumGC)
time.Sleep(time.Second) time.Sleep(time.Second)
} }
} }
func (entry *Entry) dump() map[string]interface{} {
object := make(map[string]interface{})
object["path"] = entry.Path
object["size"] = entry.Size
object["time"] = entry.Time
object["mode"] = entry.Mode
object["hash"] = entry.Hash
object["link"] = entry.Link
object["content"] = fmt.Sprintf("%d:%d:%d:%d",
entry.StartChunk, entry.StartOffset, entry.EndChunk, entry.EndOffset)
if entry.UID != -1 && entry.GID != -1 {
object["uid"] = entry.UID
object["gid"] = entry.GID
}
if entry.Attributes != nil && len(*entry.Attributes) > 0 {
object["attributes"] = entry.Attributes
}
return object
}
func (entry *Entry) dumpString() string {
data, _ := json.Marshal(entry.dump())
return string(data)
}

View File

@@ -1,53 +0,0 @@
// 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
}

View File

@@ -0,0 +1,94 @@
// 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 (
"bytes"
"encoding/binary"
"os"
"path/filepath"
"syscall"
"github.com/pkg/xattr"
)
const bsdFileFlagsKey = "\x00bf"
func (entry *Entry) ReadAttributes(top string) {
fullPath := filepath.Join(top, entry.Path)
fileInfo, err := os.Lstat(fullPath)
if err != nil {
return
}
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 err := entry.readFileFlags(fileInfo); err != nil {
LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err)
}
}
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) readFileFlags(fileInfo os.FileInfo) error {
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
}

View File

@@ -5,10 +5,29 @@
package duplicacy package duplicacy
import ( import (
"encoding/binary"
"os"
"strings" "strings"
"syscall"
) )
func excludedByAttribute(attributes map[string][]byte) bool { func excludedByAttribute(attributes map[string][]byte) bool {
value, ok := attributes["com.apple.metadata:com_apple_backup_excludeItem"] value, ok := attributes["com.apple.metadata:com_apple_backup_excludeItem"]
return ok && strings.Contains(string(value), "com.apple.backupd") return ok && strings.Contains(string(value), "com.apple.backupd")
} }
func (entry *Entry) restoreLateFileFlags(path string) error {
if entry.Attributes == nil {
return nil
}
if v, have := (*entry.Attributes)[bsdFileFlagsKey]; have {
f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_SYMLINK, 0)
if err != nil {
return err
}
err = syscall.Fchflags(int(f.Fd()), int(binary.LittleEndian.Uint32(v)))
f.Close()
return err
}
return nil
}

View File

@@ -5,10 +5,14 @@
package duplicacy package duplicacy
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"os" "os"
"path/filepath"
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/pkg/xattr"
) )
const ( const (
@@ -47,7 +51,116 @@ func ioctl(f *os.File, request uintptr, attrp *uint32) error {
return nil return nil
} }
func (entry *Entry) ReadFileFlags(f *os.File) error { type xattrHandle struct {
f *os.File
fullPath string
}
func (x xattrHandle) list() ([]string, error) {
if x.f != nil {
return xattr.FList(x.f)
} else {
return xattr.LList(x.fullPath)
}
}
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{}
}
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 (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)
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)
} else {
x.remove(name)
}
}
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()
}
func (entry *Entry) readFileFlags(f *os.File) error {
var flags uint32 var flags uint32
if err := ioctl(f, linux_FS_IOC_GETFLAGS, &flags); err != nil { if err := ioctl(f, linux_FS_IOC_GETFLAGS, &flags); err != nil {
return err return err
@@ -94,7 +207,7 @@ func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error {
return nil return nil
} }
func (entry *Entry) RestoreLateFileFlags(f *os.File) error { func (entry *Entry) restoreLateFileFlags(f *os.File) error {
if entry.Attributes == nil { if entry.Attributes == nil {
return nil return nil
} }

View File

@@ -2,18 +2,17 @@
// 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 !windows
// +build !windows // +build !windows
package duplicacy package duplicacy
import ( import (
"bytes"
"os" "os"
"path" "path"
"path/filepath"
"syscall" "syscall"
"github.com/pkg/xattr" "golang.org/x/sys/unix"
) )
func Readlink(path string) (isRegular bool, s string, err error) { func Readlink(path string) (isRegular bool, s string, err error) {
@@ -47,58 +46,50 @@ func SetOwner(fullPath string, entry *Entry, fileInfo *os.FileInfo) bool {
return true return true
} }
func (entry *Entry) ReadAttributes(top string) { func (entry *Entry) ReadSpecial(fileInfo os.FileInfo) bool {
fullPath := filepath.Join(top, entry.Path) if fileInfo.Mode()&(os.ModeDevice|os.ModeCharDevice) == 0 {
f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0) return true
if err != nil {
return
} }
attributes, _ := xattr.FList(f) stat := fileInfo.Sys().(*syscall.Stat_t)
if len(attributes) > 0 { if stat == nil {
entry.Attributes = &map[string][]byte{} return false
for _, name := range attributes {
attribute, err := xattr.Get(fullPath, name)
if err == nil {
(*entry.Attributes)[name] = attribute
}
}
} }
if err := entry.ReadFileFlags(f); err != nil { entry.Size = 0
LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err) rdev := uint64(stat.Rdev)
} entry.StartChunk = int(rdev & 0xFFFFFFFF)
f.Close() entry.StartOffset = int(rdev >> 32)
return true
} }
func (entry *Entry) SetAttributesToFile(fullPath string) { func (entry *Entry) GetRdev() uint64 {
f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0) return uint64(entry.StartChunk) | uint64(entry.StartOffset)<<32
if err != nil { }
return
}
names, _ := xattr.FList(f) func (entry *Entry) RestoreSpecial(fullPath string) error {
for _, name := range names { mode := entry.Mode & uint32(fileModeMask)
newAttribute, found := (*entry.Attributes)[name]
if found {
oldAttribute, _ := xattr.FGet(f, name)
if !bytes.Equal(oldAttribute, newAttribute) {
xattr.FSet(f, name, newAttribute)
}
delete(*entry.Attributes, name)
} else {
xattr.FRemove(f, name)
}
}
for name, attribute := range *entry.Attributes { if entry.Mode&uint32(os.ModeNamedPipe) != 0 {
if len(name) > 0 && name[0] == '\x00' { mode |= syscall.S_IFIFO
continue } else if entry.Mode&uint32(os.ModeCharDevice) != 0 {
} mode |= syscall.S_IFCHR
xattr.FSet(f, name, attribute) } else if entry.Mode&uint32(os.ModeDevice) != 0 {
mode |= syscall.S_IFBLK
} else {
return nil
} }
if err := entry.RestoreLateFileFlags(f); err != nil { return syscall.Mknod(fullPath, mode, int(entry.GetRdev()))
LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) }
func (entry *Entry) IsSameSpecial(fileInfo os.FileInfo) bool {
stat := fileInfo.Sys().(*syscall.Stat_t)
if stat == nil {
return false
} }
f.Close() return (uint32(fileInfo.Mode()) == entry.Mode) && (uint64(stat.Rdev) == entry.GetRdev())
}
func MakeHardlink(source string, target string) error {
return unix.Linkat(unix.AT_FDCWD, source, unix.AT_FDCWD, target, 0)
} }
func joinPath(components ...string) string { func joinPath(components ...string) string {

View File

@@ -117,6 +117,18 @@ func (entry *Entry) SetAttributesToFile(fullPath string) {
} }
func (entry *Entry) ReadDeviceNode(fileInfo os.FileInfo) bool {
return nil
}
func (entry *Entry) RestoreSpecial(fullPath string) error {
return nil
}
func MakeHardlink(source string, target string) error {
return os.Link(source, target)
}
func joinPath(components ...string) string { func joinPath(components ...string) string {
combinedPath := `\\?\` + filepath.Join(components...) combinedPath := `\\?\` + filepath.Join(components...)

View File

@@ -0,0 +1,30 @@
// 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
// +build freebsd netbsd
package duplicacy
import (
"bytes"
"encoding/binary"
"os"
"path/filepath"
"syscall"
"github.com/pkg/xattr"
)
func (entry *Entry) restoreLateFileFlags(path string) error {
if entry.Attributes == nil {
return nil
}
if v, have := (*entry.Attributes)[bsdFileFlagsKey]; have {
if _, _, errno := syscall.Syscall(syscall.SYS_LCHFLAGS, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), uintptr(v), 0); errno != 0 {
return os.NewSyscallError("lchflags", errno)
}
}
return nil
}