mirror of
https://github.com/jkl1337/duplicacy.git
synced 2026-01-02 11:44:45 -06:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e5cbc73b9 | ||
|
|
21b3d9e57f | ||
|
|
244b797a1c | ||
|
|
073292018c | ||
|
|
15f15aa2ca | ||
|
|
d8e13d8d85 | ||
|
|
bfb4b44c0a | ||
|
|
22a0b222db | ||
|
|
674d35e5ca | ||
|
|
a7d2a941be | ||
|
|
39d71a3256 | ||
|
|
9d10cc77fc |
@@ -1993,7 +1993,7 @@ func main() {
|
||||
app.Name = "duplicacy"
|
||||
app.HelpName = "duplicacy"
|
||||
app.Usage = "A new generation cloud backup tool based on lock-free deduplication"
|
||||
app.Version = "2.1.1" + " (" + GitCommit + ")"
|
||||
app.Version = "2.1.2" + " (" + GitCommit + ")"
|
||||
|
||||
// If the program is interrupted, call the RunAtError function.
|
||||
c := make(chan os.Signal, 1)
|
||||
|
||||
@@ -40,6 +40,7 @@ var B2AuthorizationURL = "https://api.backblazeb2.com/b2api/v1/b2_authorize_acco
|
||||
type B2Client struct {
|
||||
HTTPClient *http.Client
|
||||
AccountID string
|
||||
ApplicationKeyID string
|
||||
ApplicationKey string
|
||||
AuthorizationToken string
|
||||
APIURL string
|
||||
@@ -53,11 +54,11 @@ type B2Client struct {
|
||||
TestMode bool
|
||||
}
|
||||
|
||||
func NewB2Client(accountID string, applicationKey string) *B2Client {
|
||||
func NewB2Client(applicationKeyID string, applicationKey string) *B2Client {
|
||||
client := &B2Client{
|
||||
HTTPClient: http.DefaultClient,
|
||||
AccountID: accountID,
|
||||
ApplicationKey: applicationKey,
|
||||
HTTPClient: http.DefaultClient,
|
||||
ApplicationKeyID: applicationKeyID,
|
||||
ApplicationKey: applicationKey,
|
||||
}
|
||||
return client
|
||||
}
|
||||
@@ -119,7 +120,7 @@ func (client *B2Client) call(url string, method string, requestHeaders map[strin
|
||||
}
|
||||
|
||||
if url == B2AuthorizationURL {
|
||||
request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(client.AccountID+":"+client.ApplicationKey)))
|
||||
request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(client.ApplicationKeyID+":"+client.ApplicationKey)))
|
||||
} else {
|
||||
request.Header.Set("Authorization", client.AuthorizationToken)
|
||||
}
|
||||
@@ -225,6 +226,10 @@ func (client *B2Client) AuthorizeAccount() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// The account id may be different from the application key id so we're getting the account id from the returned
|
||||
// json object here, which is needed by the b2_list_buckets call.
|
||||
client.AccountID = output.AccountID
|
||||
|
||||
client.AuthorizationToken = output.AuthorizationToken
|
||||
client.APIURL = output.APIURL
|
||||
client.DownloadURL = output.DownloadURL
|
||||
@@ -233,7 +238,7 @@ func (client *B2Client) AuthorizeAccount() (err error) {
|
||||
}
|
||||
|
||||
type ListBucketOutput struct {
|
||||
AccoundID string
|
||||
AccountID string
|
||||
BucketID string
|
||||
BucketName string
|
||||
BucketType string
|
||||
|
||||
@@ -472,7 +472,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
|
||||
|
||||
uploadedModifiedFileSize := atomic.AddInt64(&uploadedModifiedFileSize, int64(chunkSize))
|
||||
|
||||
if IsTracing() || showStatistics {
|
||||
if (IsTracing() || showStatistics) && totalModifiedFileSize > 0 {
|
||||
now := time.Now().Unix()
|
||||
if now <= startUploadingTime {
|
||||
now = startUploadingTime + 1
|
||||
@@ -825,6 +825,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
|
||||
if stat.Mode()&os.ModeSymlink != 0 {
|
||||
isRegular, link, err := Readlink(fullPath)
|
||||
if err == nil && link == entry.Link && !isRegular {
|
||||
entry.RestoreMetadata(fullPath, nil, setOwner)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -837,6 +838,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
|
||||
LOG_ERROR("RESTORE_SYMLINK", "Can't create symlink %s: %v", entry.Path, err)
|
||||
return false
|
||||
}
|
||||
entry.RestoreMetadata(fullPath, nil, setOwner)
|
||||
LOG_TRACE("DOWNLOAD_DONE", "Symlink %s updated", entry.Path)
|
||||
} else if entry.IsDir() {
|
||||
stat, err := os.Stat(fullPath)
|
||||
@@ -1160,6 +1162,9 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
||||
lengthMap := make(map[string]int)
|
||||
var offset int64
|
||||
|
||||
// If the file is newly created (needed by sparse file optimization)
|
||||
isNewFile := false
|
||||
|
||||
existingFile, err = os.Open(fullPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -1194,6 +1199,7 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
||||
LOG_ERROR("DOWNLOAD_OPEN", "Can't reopen the initial file just created: %v", err)
|
||||
return false
|
||||
}
|
||||
isNewFile = true
|
||||
}
|
||||
} else {
|
||||
LOG_TRACE("DOWNLOAD_OPEN", "Can't open the existing file: %v", err)
|
||||
@@ -1206,6 +1212,9 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
||||
}
|
||||
}
|
||||
|
||||
// The key in this map is the number of zeroes. The value is the corresponding hash.
|
||||
knownHashes := make(map[int]string)
|
||||
|
||||
fileHash := ""
|
||||
if existingFile != nil {
|
||||
|
||||
@@ -1215,6 +1224,7 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
||||
fileHasher := manager.config.NewFileHasher()
|
||||
buffer := make([]byte, 64*1024)
|
||||
err = nil
|
||||
isSkipped := false
|
||||
// We set to read one more byte so the file hash will be different if the file to be restored is a
|
||||
// truncated portion of the existing file
|
||||
for i := entry.StartChunk; i <= entry.EndChunk+1; i++ {
|
||||
@@ -1230,6 +1240,28 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
||||
chunkSize = 1 // the size of extra chunk beyond EndChunk
|
||||
}
|
||||
count := 0
|
||||
|
||||
if isNewFile {
|
||||
if hash, found := knownHashes[chunkSize]; found {
|
||||
// We have read the same number of zeros before, so we just retrieve the hash from the map
|
||||
existingChunks = append(existingChunks, hash)
|
||||
existingLengths = append(existingLengths, chunkSize)
|
||||
offsetMap[hash] = offset
|
||||
lengthMap[hash] = chunkSize
|
||||
offset += int64(chunkSize)
|
||||
isSkipped = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if isSkipped {
|
||||
_, err := existingFile.Seek(offset, 0)
|
||||
if err != nil {
|
||||
LOG_ERROR("DOWNLOAD_SEEK", "Failed to seek to offset %d: %v", offset, err)
|
||||
}
|
||||
isSkipped = false
|
||||
}
|
||||
|
||||
for count < chunkSize {
|
||||
n := chunkSize - count
|
||||
if n > cap(buffer) {
|
||||
@@ -1256,12 +1288,16 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
||||
offsetMap[hash] = offset
|
||||
lengthMap[hash] = chunkSize
|
||||
offset += int64(chunkSize)
|
||||
if isNewFile {
|
||||
knownHashes[chunkSize] = hash
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fileHash = hex.EncodeToString(fileHasher.Sum(nil))
|
||||
} else {
|
||||
// If it is not inplace, we want to reuse any chunks in the existing file regardless their offets, so
|
||||
@@ -1288,6 +1324,7 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for i := entry.StartChunk; i <= entry.EndChunk; i++ {
|
||||
if _, found := offsetMap[chunkDownloader.taskList[i].chunkHash]; !found {
|
||||
chunkDownloader.taskList[i].needed = true
|
||||
|
||||
@@ -250,10 +250,7 @@ func (chunk *Chunk) Encrypt(encryptionKey []byte, derivationKey string) (err err
|
||||
// PKCS7 is used. Compressed chunk sizes leaks information about the original chunks so we want the padding sizes
|
||||
// to be the maximum allowed by PKCS7
|
||||
dataLength := encryptedBuffer.Len() - offset
|
||||
paddingLength := dataLength % 256
|
||||
if paddingLength == 0 {
|
||||
paddingLength = 256
|
||||
}
|
||||
paddingLength := 256 - dataLength % 256
|
||||
|
||||
encryptedBuffer.Write(bytes.Repeat([]byte{byte(paddingLength)}, paddingLength))
|
||||
encryptedBuffer.Write(bytes.Repeat([]byte{0}, gcm.Overhead()))
|
||||
|
||||
@@ -22,6 +22,8 @@ func TestChunk(t *testing.T) {
|
||||
config.CompressionLevel = DEFAULT_COMPRESSION_LEVEL
|
||||
maxSize := 1000000
|
||||
|
||||
remainderLength := -1
|
||||
|
||||
for i := 0; i < 500; i++ {
|
||||
|
||||
size := rand.Int() % maxSize
|
||||
@@ -44,6 +46,12 @@ func TestChunk(t *testing.T) {
|
||||
encryptedData := make([]byte, chunk.GetLength())
|
||||
copy(encryptedData, chunk.GetBytes())
|
||||
|
||||
if remainderLength == -1 {
|
||||
remainderLength = len(encryptedData) % 256
|
||||
} else if len(encryptedData) % 256 != remainderLength {
|
||||
t.Errorf("Incorrect padding size")
|
||||
}
|
||||
|
||||
chunk.Reset(false)
|
||||
chunk.Write(encryptedData)
|
||||
err = chunk.Decrypt(key, "")
|
||||
|
||||
@@ -292,7 +292,7 @@ func (entry *Entry) String(maxSizeDigits int) string {
|
||||
func (entry *Entry) RestoreMetadata(fullPath string, fileInfo *os.FileInfo, setOwner bool) bool {
|
||||
|
||||
if fileInfo == nil {
|
||||
stat, err := os.Stat(fullPath)
|
||||
stat, err := os.Lstat(fullPath)
|
||||
fileInfo = &stat
|
||||
if err != nil {
|
||||
LOG_ERROR("RESTORE_STAT", "Failed to retrieve the file info: %v", err)
|
||||
@@ -307,7 +307,8 @@ func (entry *Entry) RestoreMetadata(fullPath string, fileInfo *os.FileInfo, setO
|
||||
}
|
||||
}
|
||||
|
||||
if (*fileInfo).Mode()&fileModeMask != entry.GetPermissions() {
|
||||
// Only set the permission if the file is not a symlink
|
||||
if !entry.IsLink() && (*fileInfo).Mode() & fileModeMask != entry.GetPermissions() {
|
||||
err := os.Chmod(fullPath, entry.GetPermissions())
|
||||
if err != nil {
|
||||
LOG_ERROR("RESTORE_CHMOD", "Failed to set the file permissions: %v", err)
|
||||
@@ -315,7 +316,8 @@ func (entry *Entry) RestoreMetadata(fullPath string, fileInfo *os.FileInfo, setO
|
||||
}
|
||||
}
|
||||
|
||||
if (*fileInfo).ModTime().Unix() != entry.Time {
|
||||
// Only set the time if the file is not a symlink
|
||||
if !entry.IsLink() && (*fileInfo).ModTime().Unix() != entry.Time {
|
||||
modifiedTime := time.Unix(entry.Time, 0)
|
||||
err := os.Chtimes(fullPath, modifiedTime, modifiedTime)
|
||||
if err != nil {
|
||||
|
||||
@@ -390,7 +390,7 @@ func (storage *GCDStorage) ListFiles(threadIndex int, dir string) ([]string, []i
|
||||
subDirs = append(subDirs, file.Name + "/")
|
||||
}
|
||||
return subDirs, nil, nil
|
||||
} else if strings.HasPrefix(dir, "snapshots/") {
|
||||
} else if strings.HasPrefix(dir, "snapshots/") || strings.HasPrefix(dir, "benchmark") {
|
||||
pathID, err := storage.getIDFromPath(threadIndex, dir, false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -136,6 +136,16 @@ func keyringSet(key string, value string) bool {
|
||||
if value == "" {
|
||||
keyring[key] = nil
|
||||
} else {
|
||||
|
||||
// Check if the value to be set is the same as the existing one
|
||||
existingEncryptedValue := keyring[key]
|
||||
if len(existingEncryptedValue) > 0 {
|
||||
existingValue, err := keyringDecrypt(existingEncryptedValue)
|
||||
if err == nil && string(existingValue) == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
encryptedValue, err := keyringEncrypt([]byte(value))
|
||||
if err != nil {
|
||||
LOG_DEBUG("KEYRING_ENCRYPT", "Failed to encrypt the value: %v", err)
|
||||
|
||||
@@ -97,7 +97,7 @@ func (storage *OneDriveStorage) ListFiles(threadIndex int, dir string) ([]string
|
||||
}
|
||||
}
|
||||
return subDirs, nil, nil
|
||||
} else if strings.HasPrefix(dir, "snapshots/") {
|
||||
} else if strings.HasPrefix(dir, "snapshots/") || strings.HasPrefix(dir, "benchmark") {
|
||||
entries, err := storage.client.ListEntries(storage.storageDir + "/" + dir)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -170,6 +170,16 @@ func (collection *FossilCollection) IsEmpty() bool {
|
||||
return len(collection.Fossils) == 0 && len(collection.Temporaries) == 0
|
||||
}
|
||||
|
||||
// Calculates the number of days between two times ignoring the hours, minutes and seconds.
|
||||
func getDaysBetween(start int64, end int64) int {
|
||||
startTime := time.Unix(start, 0).In(time.Now().Location())
|
||||
endTime := time.Unix(end, 0).In(time.Now().Location())
|
||||
startDate := time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, startTime.Location())
|
||||
endDate := time.Date(endTime.Year(), endTime.Month(), endTime.Day(), 0, 0, 0, 0, endTime.Location())
|
||||
hours := int(endDate.Sub(startDate).Hours())
|
||||
return (hours + 1) / 24
|
||||
}
|
||||
|
||||
// SnapshotManager is mainly responsible for downloading, and deleting snapshots.
|
||||
type SnapshotManager struct {
|
||||
|
||||
@@ -679,6 +689,9 @@ func (manager *SnapshotManager) ListSnapshots(snapshotID string, revisionsToList
|
||||
for _, revision := range revisions {
|
||||
|
||||
snapshot := manager.DownloadSnapshot(snapshotID, revision)
|
||||
if tag != "" && snapshot.Tag != tag {
|
||||
continue
|
||||
}
|
||||
creationTime := time.Unix(snapshot.StartTime, 0).Format("2006-01-02 15:04")
|
||||
tagWithSpace := ""
|
||||
if len(snapshot.Tag) > 0 {
|
||||
@@ -687,15 +700,16 @@ func (manager *SnapshotManager) ListSnapshots(snapshotID string, revisionsToList
|
||||
LOG_INFO("SNAPSHOT_INFO", "Snapshot %s revision %d created at %s %s%s",
|
||||
snapshotID, revision, creationTime, tagWithSpace, snapshot.Options)
|
||||
|
||||
if tag != "" && snapshot.Tag != tag {
|
||||
continue
|
||||
}
|
||||
|
||||
if showFiles {
|
||||
manager.DownloadSnapshotFileSequence(snapshot, nil, false)
|
||||
}
|
||||
|
||||
if showFiles {
|
||||
|
||||
if snapshot.NumberOfFiles > 0 {
|
||||
LOG_INFO("SNAPSHOT_STATS", "Files: %d", snapshot.NumberOfFiles)
|
||||
}
|
||||
|
||||
maxSize := int64(9)
|
||||
maxSizeDigits := 1
|
||||
totalFiles := 0
|
||||
@@ -806,11 +820,28 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
|
||||
|
||||
for _, revision := range revisions {
|
||||
snapshot := manager.DownloadSnapshot(snapshotID, revision)
|
||||
snapshotMap[snapshotID] = append(snapshotMap[snapshotID], snapshot)
|
||||
|
||||
if tag != "" && snapshot.Tag != tag {
|
||||
continue
|
||||
}
|
||||
snapshotMap[snapshotID] = append(snapshotMap[snapshotID], snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
totalRevisions := 0
|
||||
for _, snapshotList := range snapshotMap {
|
||||
totalRevisions += len(snapshotList)
|
||||
}
|
||||
LOG_INFO("SNAPSHOT_CHECK", "%d snapshots and %d revisions", len(snapshotMap), totalRevisions)
|
||||
|
||||
var totalChunkSize int64
|
||||
for _, size := range chunkSizeMap {
|
||||
totalChunkSize += size
|
||||
}
|
||||
LOG_INFO("SNAPSHOT_CHECK", "Total chunk size is %s in %d chunks", PrettyNumber(totalChunkSize), len(chunkSizeMap))
|
||||
|
||||
for snapshotID, _ = range snapshotMap {
|
||||
|
||||
for _, snapshot := range snapshotMap[snapshotID] {
|
||||
|
||||
if checkFiles {
|
||||
manager.DownloadSnapshotContents(snapshot, nil, false)
|
||||
@@ -833,7 +864,7 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
|
||||
missingChunks += 1
|
||||
LOG_WARN("SNAPSHOT_VALIDATE",
|
||||
"Chunk %s referenced by snapshot %s at revision %d does not exist",
|
||||
chunkID, snapshotID, revision)
|
||||
chunkID, snapshotID, snapshot.Revision)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -848,7 +879,7 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
|
||||
missingChunks += 1
|
||||
LOG_WARN("SNAPSHOT_VALIDATE",
|
||||
"Chunk %s referenced by snapshot %s at revision %d does not exist",
|
||||
chunkID, snapshotID, revision)
|
||||
chunkID, snapshotID, snapshot.Revision)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -856,7 +887,7 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
|
||||
manager.resurrectChunk(chunkPath, chunkID)
|
||||
} else {
|
||||
LOG_WARN("SNAPSHOT_FOSSIL", "Chunk %s referenced by snapshot %s at revision %d "+
|
||||
"has been marked as a fossil", chunkID, snapshotID, revision)
|
||||
"has been marked as a fossil", chunkID, snapshotID, snapshot.Revision)
|
||||
}
|
||||
|
||||
chunkSizeMap[chunkID] = size
|
||||
@@ -879,11 +910,11 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
|
||||
|
||||
if missingChunks > 0 {
|
||||
LOG_WARN("SNAPSHOT_CHECK", "Some chunks referenced by snapshot %s at revision %d are missing",
|
||||
snapshotID, revision)
|
||||
snapshotID, snapshot.Revision)
|
||||
totalMissingChunks += missingChunks
|
||||
} else {
|
||||
LOG_INFO("SNAPSHOT_CHECK", "All chunks referenced by snapshot %s at revision %d exist",
|
||||
snapshotID, revision)
|
||||
snapshotID, snapshot.Revision)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1907,7 +1938,7 @@ func (manager *SnapshotManager) PruneSnapshots(selfID string, snapshotID string,
|
||||
|
||||
// Find out which retent policy applies based on the age.
|
||||
for i < len(retentionPolicies) &&
|
||||
int(now-snapshot.StartTime) < retentionPolicies[i].Age*secondsInDay {
|
||||
getDaysBetween(snapshot.StartTime, now) < retentionPolicies[i].Age {
|
||||
i++
|
||||
lastSnapshotTime = 0
|
||||
}
|
||||
@@ -1920,9 +1951,8 @@ func (manager *SnapshotManager) PruneSnapshots(selfID string, snapshotID string,
|
||||
snapshot.Flag = true
|
||||
toBeDeleted++
|
||||
} else if lastSnapshotTime != 0 &&
|
||||
int(snapshot.StartTime-lastSnapshotTime) < retentionPolicies[i].Interval*secondsInDay-600 {
|
||||
// Delete the snapshot if it is too close to the last kept one. Note that a tolerance of 10
|
||||
// minutes was subtracted from the interval.
|
||||
getDaysBetween(lastSnapshotTime, snapshot.StartTime) < retentionPolicies[i].Interval {
|
||||
// Delete the snapshot if it is too close to the last kept one.
|
||||
LOG_DEBUG("SNAPSHOT_DELETE", "Snapshot %s at revision %d to be deleted - older than %d days, less than %d days from previous",
|
||||
snapshot.ID, snapshot.Revision, retentionPolicies[i].Age, retentionPolicies[i].Interval)
|
||||
snapshot.Flag = true
|
||||
|
||||
@@ -35,7 +35,7 @@ func SetOwner(fullPath string, entry *Entry, fileInfo *os.FileInfo) bool {
|
||||
stat, ok := (*fileInfo).Sys().(*syscall.Stat_t)
|
||||
if ok && stat != nil && (int(stat.Uid) != entry.UID || int(stat.Gid) != entry.GID) {
|
||||
if entry.UID != -1 && entry.GID != -1 {
|
||||
err := os.Chown(fullPath, entry.UID, entry.GID)
|
||||
err := os.Lchown(fullPath, entry.UID, entry.GID)
|
||||
if err != nil {
|
||||
LOG_ERROR("RESTORE_CHOWN", "Failed to change uid or gid: %v", err)
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user