Compare commits

..

8 Commits

Author SHA1 Message Date
Gilbert Chen
175adb14cb Bump version to 2.7.2 2020-11-15 23:20:11 -05:00
Gilbert Chen
ae706e3dcf Update dependency (for gilbertchen/go-dropbox and github.com/pkg/xattr) 2020-11-15 23:19:20 -05:00
Gilbert Chen
5eed6c65f6 Validate the repository id for the init and add command
Only letter, numbers, dashes, and underscores are allowed.
2020-11-04 21:32:07 -05:00
Gilbert Chen
bec3a0edcd Fixed a bug that caused a fresh restore to fail without the -overwrite option
When restoring a file that doesn't exit locally, if the file is large (>100M)
Duplicacy will create an empty sparse file.  But this newly created file will
be mistaken for a local copy and hence the restore will fail with a message
suggesting the -overwrite option.
2020-11-03 10:57:47 -05:00
Gilbert Chen
b392302c06 Use github.com/pkg/xattr for reading/writing extended attributes.
The one previously used, github.com/redsift/xattr, is old and can only process
user-defined extended attributes, not system ones.
2020-10-16 21:16:05 -04:00
Gilbert Chen
7c36311aa9 Change snapshot source path from / to /System/Volumes/Data
Also use a regex to extract the snapshot date from tmutil output.
2020-10-11 15:23:09 -04:00
Gilbert Chen
7f834e84f6 Don't attemp to load verified_chunks when it doesn't exist. 2020-10-09 14:22:45 -04:00
Gilbert Chen
d7c1903d5a Skip chunks already verified in previous runs for check -chunks.
This is done by storing the list of verified chunks in a file
`.duplicacy/cache/<storage>/verified_chunks`.
2020-10-08 19:59:39 -04:00
7 changed files with 126 additions and 57 deletions

10
Gopkg.lock generated
View File

@@ -52,7 +52,7 @@
[[projects]] [[projects]]
name = "github.com/gilbertchen/go-dropbox" name = "github.com/gilbertchen/go-dropbox"
packages = ["."] packages = ["."]
revision = "0baa9015ac2547d8b69b2e88c709aa90cfb8fbc1" revision = "2233fa1dd846b3a3e8060b6c1ea12883deb9d288"
[[projects]] [[projects]]
name = "github.com/gilbertchen/go-ole" name = "github.com/gilbertchen/go-ole"
@@ -173,6 +173,12 @@
revision = "5616182052227b951e76d9c9b79a616c608bd91b" revision = "5616182052227b951e76d9c9b79a616c608bd91b"
version = "v1.11.0" version = "v1.11.0"
[[projects]]
name = "github.com/pkg/xattr"
packages = ["."]
revision = "dd870b5cfebab49617ea0c1da6176474e8a52bf4"
version = "v0.4.1"
[[projects]] [[projects]]
name = "github.com/satori/go.uuid" name = "github.com/satori/go.uuid"
packages = ["."] packages = ["."]
@@ -265,6 +271,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "0e6ea2be64dedc36cb9192f1d410917ea72896302011e55b6df5e4c00c1c2f1c" inputs-digest = "e46f7c2dac527af6d5a0d47f1444421a6738d28252eb5d6084fc1c65f2b41bd8"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@@ -47,7 +47,7 @@
[[constraint]] [[constraint]]
name = "github.com/gilbertchen/go-dropbox" name = "github.com/gilbertchen/go-dropbox"
revision = "0baa9015ac2547d8b69b2e88c709aa90cfb8fbc1" revision = "2233fa1dd846b3a3e8060b6c1ea12883deb9d288"
[[constraint]] [[constraint]]
name = "github.com/gilbertchen/go-ole" name = "github.com/gilbertchen/go-ole"

View File

@@ -274,6 +274,13 @@ func configRepository(context *cli.Context, init bool) {
} }
} }
snapshotIDRegex := regexp.MustCompile(`^[A-Za-z0-9_\-]+$`)
matched := snapshotIDRegex.FindStringSubmatch(snapshotID)
if matched == nil {
duplicacy.LOG_ERROR("PREFERENCE_INVALID", "'%s' is an invalid snapshot id", snapshotID)
return
}
var repository string var repository string
var err error var err error
@@ -2179,7 +2186,7 @@ func main() {
app.Name = "duplicacy" app.Name = "duplicacy"
app.HelpName = "duplicacy" app.HelpName = "duplicacy"
app.Usage = "A new generation cloud backup tool based on lock-free deduplication" app.Usage = "A new generation cloud backup tool based on lock-free deduplication"
app.Version = "2.7.1" + " (" + GitCommit + ")" app.Version = "2.7.2" + " (" + GitCommit + ")"
// If the program is interrupted, call the RunAtError function. // If the program is interrupted, call the RunAtError function.
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)

View File

@@ -1377,7 +1377,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 { 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")

View File

@@ -14,7 +14,6 @@ import (
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
"strings"
"syscall" "syscall"
"time" "time"
) )
@@ -77,19 +76,19 @@ func DeleteShadowCopy() {
err := exec.Command("/sbin/umount", "-f", snapshotPath).Run() err := exec.Command("/sbin/umount", "-f", snapshotPath).Run()
if err != nil { if err != nil {
LOG_ERROR("VSS_DELETE", "Error while unmounting snapshot") LOG_WARN("VSS_DELETE", "Error while unmounting snapshot: %v", err)
return return
} }
err = exec.Command("tmutil", "deletelocalsnapshots", snapshotDate).Run() err = exec.Command("tmutil", "deletelocalsnapshots", snapshotDate).Run()
if err != nil { if err != nil {
LOG_ERROR("VSS_DELETE", "Error while deleting local snapshot") LOG_WARN("VSS_DELETE", "Error while deleting local snapshot: %v", err)
return return
} }
err = os.RemoveAll(snapshotPath) err = os.RemoveAll(snapshotPath)
if err != nil { if err != nil {
LOG_ERROR("VSS_DELETE", "Error while deleting temporary mount directory") LOG_WARN("VSS_DELETE", "Error while deleting temporary mount directory: %v", err)
return return
} }
@@ -150,12 +149,13 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow
return top return top
} }
colonPos := strings.IndexByte(tmutilOutput, ':') snapshotDateRegex := regexp.MustCompile(`:\s+([0-9\-]+)`)
if colonPos < 0 { matched := snapshotDateRegex.FindStringSubmatch(tmutilOutput)
if matched == nil {
LOG_ERROR("VSS_CREATE", "Snapshot creation failed: %s", tmutilOutput) LOG_ERROR("VSS_CREATE", "Snapshot creation failed: %s", tmutilOutput)
return top return top
} }
snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:]) snapshotDate = matched[1]
tmutilOutput, err = CommandWithTimeout(timeoutInSeconds, "tmutil", "listlocalsnapshots", ".") tmutilOutput, err = CommandWithTimeout(timeoutInSeconds, "tmutil", "listlocalsnapshots", ".")
if err != nil { if err != nil {
@@ -164,17 +164,17 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow
} }
snapshotName := "com.apple.TimeMachine." + snapshotDate snapshotName := "com.apple.TimeMachine." + snapshotDate
r := regexp.MustCompile(`(?m)^(.+` + snapshotDate + `.*)$`) snapshotNameRegex := regexp.MustCompile(`(?m)^(.+` + snapshotDate + `.*)$`)
snapshotNames := r.FindStringSubmatch(tmutilOutput) matched = snapshotNameRegex.FindStringSubmatch(tmutilOutput)
if len(snapshotNames) > 0 { if len(matched) > 0 {
snapshotName = snapshotNames[0] snapshotName = matched[0]
} else { } else {
LOG_WARN("VSS_CREATE", "Error while using 'tmutil listlocalsnapshots' to find snapshot name. Will fallback to 'com.apple.TimeMachine.SNAPSHOT_DATE'") LOG_INFO("VSS_CREATE", "Can't find the snapshot name with 'tmutil listlocalsnapshots'; fallback to %s", snapshotName)
} }
// Mount snapshot as readonly and hide from GUI i.e. Finder // Mount snapshot as readonly and hide from GUI i.e. Finder
_, err = CommandWithTimeout(timeoutInSeconds, _, err = CommandWithTimeout(timeoutInSeconds,
"/sbin/mount", "-t", "apfs", "-o", "nobrowse,-r,-s="+snapshotName, "/", snapshotPath) "/sbin/mount", "-t", "apfs", "-o", "nobrowse,-r,-s="+snapshotName, "/System/Volumes/Data", snapshotPath)
if err != nil { if err != nil {
LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: %v", err) LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: %v", err)
return top return top

View File

@@ -1017,7 +1017,50 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
manager.ShowStatistics(snapshotMap, chunkSizeMap, chunkUniqueMap, chunkSnapshotMap) manager.ShowStatistics(snapshotMap, chunkSizeMap, chunkUniqueMap, chunkSnapshotMap)
} }
if checkChunks && !checkFiles { // Don't verify chunks with -files
if !checkChunks || checkFiles {
return true
}
// This contains chunks that have been verifed in previous checks and is loaded from
// .duplicacy/cache/storage/verified_chunks. Note that it contains the chunk ids not chunk
// hashes.
verifiedChunks := make(map[string]int64)
verifiedChunksFile := "verified_chunks"
manager.fileChunk.Reset(false)
err = manager.snapshotCache.DownloadFile(0, verifiedChunksFile, manager.fileChunk)
if err != nil {
if !os.IsNotExist(err) {
LOG_WARN("SNAPSHOT_VERIFY", "Failed to load the file containing verified chunks: %v", err)
}
} else {
err = json.Unmarshal(manager.fileChunk.GetBytes(), &verifiedChunks)
if err != nil {
LOG_WARN("SNAPSHOT_VERIFY", "Failed to parse the file containing verified chunks: %v", err)
}
}
numberOfVerifiedChunks := len(verifiedChunks)
saveVerifiedChunks := func() {
if len(verifiedChunks) > numberOfVerifiedChunks {
var description []byte
description, err = json.Marshal(verifiedChunks)
if err != nil {
LOG_WARN("SNAPSHOT_VERIFY", "Failed to create a json file for the set of verified chunks: %v", err)
} else {
err = manager.snapshotCache.UploadFile(0, verifiedChunksFile, description)
if err != nil {
LOG_WARN("SNAPSHOT_VERIFY", "Failed to save the verified chunks file: %v", err)
} else {
LOG_INFO("SNAPSHOT_VERIFY", "Added %d chunks to the list of verified chunks", len(verifiedChunks) - numberOfVerifiedChunks)
}
}
}
}
defer saveVerifiedChunks()
RunAtError = saveVerifiedChunks
manager.chunkDownloader.snapshotCache = nil manager.chunkDownloader.snapshotCache = nil
LOG_INFO("SNAPSHOT_VERIFY", "Verifying %d chunks", len(*allChunkHashes)) LOG_INFO("SNAPSHOT_VERIFY", "Verifying %d chunks", len(*allChunkHashes))
@@ -1028,7 +1071,15 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
// some metadata chunks so the index doesn't start with 0. // some metadata chunks so the index doesn't start with 0.
chunkIndex := -1 chunkIndex := -1
skippedChunks := 0
for chunkHash := range *allChunkHashes { for chunkHash := range *allChunkHashes {
if len(verifiedChunks) > 0 {
chunkID := manager.config.GetChunkIDFromHash(chunkHash)
if _, found := verifiedChunks[chunkID]; found {
skippedChunks++
continue
}
}
chunkHashes = append(chunkHashes, chunkHash) chunkHashes = append(chunkHashes, chunkHash)
if chunkIndex == -1 { if chunkIndex == -1 {
chunkIndex = manager.chunkDownloader.AddChunk(chunkHash) chunkIndex = manager.chunkDownloader.AddChunk(chunkHash)
@@ -1037,13 +1088,19 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
} }
} }
if skippedChunks > 0 {
LOG_INFO("SNAPSHOT_VERIFY", "Skipped %d chunks that have already been verified before", skippedChunks)
}
var downloadedChunkSize int64 var downloadedChunkSize int64
totalChunks := len(*allChunkHashes) totalChunks := len(chunkHashes)
for i := 0; i < totalChunks; i++ { for i := 0; i < totalChunks; i++ {
chunk := manager.chunkDownloader.WaitForChunk(i + chunkIndex) chunk := manager.chunkDownloader.WaitForChunk(i + chunkIndex)
chunkID := manager.config.GetChunkIDFromHash(chunkHashes[i])
if chunk.isBroken { if chunk.isBroken {
continue continue
} }
verifiedChunks[chunkID] = startTime.Unix()
downloadedChunkSize += int64(chunk.GetLength()) downloadedChunkSize += int64(chunk.GetLength())
elapsedTime := time.Now().Sub(startTime).Seconds() elapsedTime := time.Now().Sub(startTime).Seconds()
@@ -1051,15 +1108,13 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
remainingTime := int64(float64(totalChunks - i - 1) / float64(i + 1) * elapsedTime) remainingTime := int64(float64(totalChunks - i - 1) / float64(i + 1) * elapsedTime)
percentage := float64(i + 1) / float64(totalChunks) * 100.0 percentage := float64(i + 1) / float64(totalChunks) * 100.0
LOG_INFO("VERIFY_PROGRESS", "Verified chunk %s (%d/%d), %sB/s %s %.1f%%", LOG_INFO("VERIFY_PROGRESS", "Verified chunk %s (%d/%d), %sB/s %s %.1f%%",
manager.config.GetChunkIDFromHash(chunkHashes[i]), i + 1, totalChunks, chunkID, i + 1, totalChunks, PrettySize(speed), PrettyTime(remainingTime), percentage)
PrettySize(speed), PrettyTime(remainingTime), percentage)
} }
if manager.chunkDownloader.NumberOfFailedChunks > 0 { if manager.chunkDownloader.NumberOfFailedChunks > 0 {
LOG_ERROR("SNAPSHOT_VERIFY", "%d out of %d chunks are corrupted", manager.chunkDownloader.NumberOfFailedChunks, totalChunks) LOG_ERROR("SNAPSHOT_VERIFY", "%d out of %d chunks are corrupted", manager.chunkDownloader.NumberOfFailedChunks, len(*allChunkHashes))
} else { } else {
LOG_INFO("SNAPSHOT_VERIFY", "All %d chunks have been successfully verified", totalChunks) LOG_INFO("SNAPSHOT_VERIFY", "All %d chunks have been successfully verified", len(*allChunkHashes))
}
} }
return true return true
} }

View File

@@ -13,7 +13,7 @@ import (
"path/filepath" "path/filepath"
"syscall" "syscall"
"github.com/gilbertchen/xattr" "github.com/pkg/xattr"
) )
func Readlink(path string) (isRegular bool, s string, err error) { func Readlink(path string) (isRegular bool, s string, err error) {
@@ -50,11 +50,11 @@ func SetOwner(fullPath string, entry *Entry, fileInfo *os.FileInfo) bool {
func (entry *Entry) ReadAttributes(top string) { func (entry *Entry) ReadAttributes(top string) {
fullPath := filepath.Join(top, entry.Path) fullPath := filepath.Join(top, entry.Path)
attributes, _ := xattr.Listxattr(fullPath) attributes, _ := xattr.List(fullPath)
if len(attributes) > 0 { if len(attributes) > 0 {
entry.Attributes = make(map[string][]byte) entry.Attributes = make(map[string][]byte)
for _, name := range attributes { for _, name := range attributes {
attribute, err := xattr.Getxattr(fullPath, name) attribute, err := xattr.Get(fullPath, name)
if err == nil { if err == nil {
entry.Attributes[name] = attribute entry.Attributes[name] = attribute
} }
@@ -63,24 +63,25 @@ func (entry *Entry) ReadAttributes(top string) {
} }
func (entry *Entry) SetAttributesToFile(fullPath string) { func (entry *Entry) SetAttributesToFile(fullPath string) {
names, _ := xattr.Listxattr(fullPath) names, _ := xattr.List(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.Getxattr(fullPath, name) oldAttribute, _ := xattr.Get(fullPath, name)
if !bytes.Equal(oldAttribute, newAttribute) { if !bytes.Equal(oldAttribute, newAttribute) {
xattr.Setxattr(fullPath, name, newAttribute) xattr.Set(fullPath, name, newAttribute)
} }
delete(entry.Attributes, name) delete(entry.Attributes, name)
} else { } else {
xattr.Removexattr(fullPath, name) xattr.Remove(fullPath, name)
} }
} }
for name, attribute := range entry.Attributes { for name, attribute := range entry.Attributes {
xattr.Setxattr(fullPath, name, attribute) xattr.Set(fullPath, name, attribute)
} }
} }