Compare commits

..

1 Commits

Author SHA1 Message Date
b2749b6e20 a 2023-10-03 22:08:48 -05:00
9 changed files with 124 additions and 344 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,41 +698,18 @@ 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
willExist bool willDownload 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() {
@@ -748,7 +725,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
@@ -775,16 +752,12 @@ 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() {
if stat, _ := os.Lstat(fullPath); stat != nil { if stat, _ := os.Lstat(fullPath); 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
} }
} }
@@ -798,16 +771,11 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
os.Remove(fullPath) os.Remove(fullPath)
} }
if restoreHardlink(remoteEntry, fullPath) {
continue
}
if err := os.Symlink(remoteEntry.Link, fullPath); err != nil { 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() {
@@ -829,48 +797,22 @@ 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].willExist = true hardLinkTable[len(hardLinkTable)-1] = hardLinkEntry{remoteEntry, true}
} else if remoteEntry.IsHardlinkedFrom() { } else if remoteEntry.IsHardlinkedFrom() {
i, err := remoteEntry.GetHardlinkId() i, err := strconv.ParseUint(remoteEntry.Link, 16, 64)
if err != nil { if err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Decode error for hardlinked entry %s, %v", remoteEntry.Path, err) LOG_ERROR("RESTORE_HARDLINK", "Decode error in hardlink entry, expected hex int, got %s", remoteEntry.Link)
return 0 return 0
} }
if !hardLinkTable[i].willExist { if !hardLinkTable[i].willDownload {
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
@@ -882,7 +824,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)
} }
@@ -991,32 +933,11 @@ 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 stat, _ := os.Lstat(fullPath); stat != nil { if err := os.Link(sourcePath, fullPath); err != 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
} }
@@ -1167,14 +1088,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
@@ -1206,10 +1127,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() && !entry.IsLink() { } else if entry.IsHardlinkedFrom() {
i, err := entry.GetHardlinkId() i, err := strconv.ParseUint(entry.Link, 16, 64)
if err != nil { if err != nil {
LOG_ERROR("SNAPSHOT_UPLOAD", "Decode error for hardlinked entry %s, %v", entry.Link, err) LOG_ERROR("SNAPSHOT_UPLOAD", "Decode error in hardlink entry, expected hex int, got %s", entry.Link)
return err return err
} }
@@ -1298,9 +1219,8 @@ func (manager *BackupManager) UploadSnapshot(chunkOperator *ChunkOperator, top s
// 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, nil: Skipped file; // false, error: Failure to restore file (only if allowFailures == true)
// 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) {
@@ -1476,7 +1396,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")
} }
@@ -1723,7 +1643,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 {
@@ -1824,15 +1744,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)
@@ -1845,7 +1765,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
} }
} }
@@ -1877,7 +1797,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)
@@ -1886,7 +1806,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++
@@ -1897,11 +1817,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)
} }
@@ -1924,7 +1844,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,7 +8,6 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -24,11 +23,6 @@ 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"
@@ -515,36 +509,16 @@ 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.IsFileOrSpecial() && len(entry.Link) > 0 && entry.Link != "/") || (entry.IsLink() && entry.StartChunk == entrySymHardLinkTargetChunkMarker) return entry.IsFile() && len(entry.Link) > 0 && entry.Link != "/"
} }
func (entry *Entry) IsHardlinkRoot() bool { func (entry *Entry) IsHardlinkRoot() bool {
return (entry.IsFileOrSpecial() && entry.Link == "/") || (entry.IsLink() && entry.StartChunk == entrySymHardLinkRootChunkMarker) return entry.IsFile() && entry.Link == "/"
}
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 {
@@ -605,10 +579,6 @@ 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)
@@ -619,6 +589,10 @@ 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
} }
@@ -807,44 +781,10 @@ 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))
@@ -875,11 +815,29 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string
} }
entry = newEntry entry = newEntry
} }
} else if entry.IsSpecial() { }
if !entry.ReadSpecial(f) {
LOG_WARN("LIST_DEV", "Failed to save device node %s", entry.Path) if f.Mode()&(os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
skippedFiles = append(skippedFiles, entry.Path) LOG_WARN("LIST_SKIP", "Skipped non-regular file %s", entry.Path)
continue skippedFiles = append(skippedFiles, entry.Path)
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.IsFile() { if !entry.IsDir() && !entry.IsLink() {
entryList.NumberOfEntries++ entryList.NumberOfEntries++
} }
if !entry.IsComplete() { if !entry.IsComplete() {
if !entry.IsFile() { if entry.IsDir() || entry.IsLink() {
entry.Size = 0 entry.Size = 0
} else { } else {
modifiedEntry := ModifiedEntry { modifiedEntry := ModifiedEntry {

View File

@@ -7,15 +7,14 @@ 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"
@@ -57,7 +56,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
@@ -276,6 +275,7 @@ 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,39 +469,8 @@ 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

@@ -8,10 +8,10 @@
package duplicacy package duplicacy
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"os" "os"
"path/filepath" "path/filepath"
"bytes"
"syscall" "syscall"
"github.com/pkg/xattr" "github.com/pkg/xattr"
@@ -25,16 +25,13 @@ func (entry *Entry) ReadAttributes(top string) {
if err != nil { if err != nil {
return return
} }
attributes, _ := xattr.LList(fullPath)
if !entry.IsSpecial() { if len(attributes) > 0 {
attributes, _ := xattr.LList(fullPath) entry.Attributes = &map[string][]byte{}
if len(attributes) > 0 { for _, name := range attributes {
entry.Attributes = &map[string][]byte{} attribute, err := xattr.LGet(fullPath, name)
for _, name := range attributes { if err == nil {
attribute, err := xattr.LGet(fullPath, name) (*entry.Attributes)[name] = attribute
if err == nil {
(*entry.Attributes)[name] = attribute
}
} }
} }
} }
@@ -44,27 +41,25 @@ func (entry *Entry) ReadAttributes(top string) {
} }
func (entry *Entry) SetAttributesToFile(fullPath string) { func (entry *Entry) SetAttributesToFile(fullPath string) {
if !entry.IsSpecial() { names, _ := xattr.LList(fullPath)
names, _ := xattr.LList(fullPath) for _, name := range names {
for _, name := range names { newAttribute, found := (*entry.Attributes)[name]
newAttribute, found := (*entry.Attributes)[name] if found {
if found { oldAttribute, _ := xattr.LGet(fullPath, name)
oldAttribute, _ := xattr.LGet(fullPath, name) if !bytes.Equal(oldAttribute, newAttribute) {
if !bytes.Equal(oldAttribute, newAttribute) { xattr.LSet(fullPath, name, newAttribute)
xattr.LSet(fullPath, name, newAttribute)
}
delete(*entry.Attributes, name)
} else {
xattr.LRemove(fullPath, name)
} }
delete(*entry.Attributes, name)
} else {
xattr.LRemove(fullPath, name)
} }
}
for name, attribute := range *entry.Attributes { for name, attribute := range *entry.Attributes {
if len(name) > 0 && name[0] == '\x00' { if len(name) > 0 && name[0] == '\x00' {
continue continue
}
xattr.LSet(fullPath, name, attribute)
} }
xattr.LSet(fullPath, name, attribute)
} }
if err := entry.restoreLateFileFlags(fullPath); err != nil { if err := entry.restoreLateFileFlags(fullPath); err != nil {
LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err)
@@ -92,3 +87,4 @@ func (entry *Entry) RestoreEarlyDirFlags(path string) error {
func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error { func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error {
return nil return nil
} }

View File

@@ -5,10 +5,10 @@
package duplicacy package duplicacy
import ( import (
"encoding/binary"
"os" "os"
"strings"
"syscall" "syscall"
"strings"
"encoding/binary"
) )
func excludedByAttribute(attributes map[string][]byte) bool { func excludedByAttribute(attributes map[string][]byte) bool {

View File

@@ -8,9 +8,9 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"os" "os"
"path/filepath"
"syscall" "syscall"
"unsafe" "unsafe"
"path/filepath"
"github.com/pkg/xattr" "github.com/pkg/xattr"
) )
@@ -52,7 +52,7 @@ func ioctl(f *os.File, request uintptr, attrp *uint32) error {
} }
type xattrHandle struct { type xattrHandle struct {
f *os.File f *os.File
fullPath string fullPath string
} }
@@ -84,17 +84,15 @@ func (x xattrHandle) remove(name string) error {
if x.f != nil { if x.f != nil {
return xattr.FRemove(x.f, name) return xattr.FRemove(x.f, name)
} else { } else {
return xattr.LRemove(x.fullPath, name) return xattr.LSet(x.fullPath, name)
} }
} }
func (entry *Entry) ReadAttributes(top string) { func (entry *Entry) ReadAttributes(top string) {
fullPath := filepath.Join(top, entry.Path) x := xattrHandle{nil, filepath.Join(top, entry.Path)}
x := xattrHandle{nil, fullPath}
if !entry.IsLink() { if !entry.IsLink() {
var err error x.f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0)
x.f, err = os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0)
if err != nil { if err != nil {
// FIXME: We really should return errors for failure to read // FIXME: We really should return errors for failure to read
return return
@@ -107,7 +105,7 @@ func (entry *Entry) ReadAttributes(top string) {
entry.Attributes = &map[string][]byte{} entry.Attributes = &map[string][]byte{}
} }
for _, name := range attributes { for _, name := range attributes {
attribute, err := x.get(name) attribute, err := x.get(f, name)
if err == nil { if err == nil {
(*entry.Attributes)[name] = attribute (*entry.Attributes)[name] = attribute
} }
@@ -118,14 +116,13 @@ func (entry *Entry) ReadAttributes(top string) {
LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err) LOG_INFO("ATTR_BACKUP", "Could not backup flags for file %s: %v", fullPath, err)
} }
} }
x.f.Close() f.Close()
} }
func (entry *Entry) SetAttributesToFile(fullPath string) { func (entry *Entry) SetAttributesToFile(fullPath string) {
x := xattrHandle{nil, fullPath} x := xattrHandle{nil, fullPath}
if !entry.IsLink() { if !entry.IsLink() {
var err error x.f, err := os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0)
x.f, err = os.OpenFile(fullPath, os.O_RDONLY|syscall.O_NOFOLLOW, 0)
if err != nil { if err != nil {
return return
} }
@@ -153,11 +150,11 @@ func (entry *Entry) SetAttributesToFile(fullPath string) {
x.set(name, attribute) x.set(name, attribute)
} }
if entry.IsFile() || entry.IsDir() { if entry.IsFile() || entry.IsDir() {
if err := entry.restoreLateFileFlags(x.f); err != nil { if err := entry.restoreLateFileFlags(f); err != nil {
LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err) LOG_DEBUG("ATTR_RESTORE", "Could not restore flags for file %s: %v", fullPath, err)
} }
} }
x.f.Close() f.Close()
} }
func (entry *Entry) readFileFlags(f *os.File) error { func (entry *Entry) readFileFlags(f *os.File) error {

View File

@@ -11,8 +11,6 @@ import (
"os" "os"
"path" "path"
"syscall" "syscall"
"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) {
@@ -46,52 +44,6 @@ func SetOwner(fullPath string, entry *Entry, fileInfo *os.FileInfo) bool {
return true return true
} }
func (entry *Entry) ReadSpecial(fileInfo os.FileInfo) bool {
if fileInfo.Mode()&(os.ModeDevice|os.ModeCharDevice) == 0 {
return true
}
stat := fileInfo.Sys().(*syscall.Stat_t)
if stat == nil {
return false
}
entry.Size = 0
rdev := uint64(stat.Rdev)
entry.StartChunk = int(rdev & 0xFFFFFFFF)
entry.StartOffset = int(rdev >> 32)
return true
}
func (entry *Entry) GetRdev() uint64 {
return uint64(entry.StartChunk) | uint64(entry.StartOffset)<<32
}
func (entry *Entry) RestoreSpecial(fullPath string) error {
mode := entry.Mode & uint32(fileModeMask)
if entry.Mode&uint32(os.ModeNamedPipe) != 0 {
mode |= syscall.S_IFIFO
} else if entry.Mode&uint32(os.ModeCharDevice) != 0 {
mode |= syscall.S_IFCHR
} else if entry.Mode&uint32(os.ModeDevice) != 0 {
mode |= syscall.S_IFBLK
} else {
return nil
}
return syscall.Mknod(fullPath, mode, int(entry.GetRdev()))
}
func (entry *Entry) IsSameSpecial(fileInfo os.FileInfo) bool {
stat := fileInfo.Sys().(*syscall.Stat_t)
if stat == nil {
return false
}
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 {
return path.Join(components...) return path.Join(components...)
} }

View File

@@ -117,18 +117,6 @@ 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...)