mirror of
https://github.com/jkl1337/duplicacy.git
synced 2026-01-06 13:44:40 -06:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
808ae4eb75 | ||
|
|
6699e2f440 | ||
|
|
733b68be2c | ||
|
|
b61906c99e | ||
|
|
a0a07d18cc | ||
|
|
a6ce64e715 | ||
|
|
499b612a0d | ||
|
|
46ce0ba1fb | ||
|
|
cc88abd547 | ||
|
|
e888b6d7e5 | ||
|
|
d43fe1a282 | ||
|
|
504d07bd51 | ||
|
|
0abb4099f6 | ||
|
|
694494ea54 | ||
|
|
165152493c | ||
|
|
e02041f4ed | ||
|
|
a99f059b52 | ||
|
|
f022a6f684 | ||
|
|
791c61eecb | ||
|
|
6ad27adaea | ||
|
|
9abfbe1ee0 | ||
|
|
b32c3b2cd5 | ||
|
|
9baafdafa2 | ||
|
|
ca7d927840 | ||
|
|
0ca9cd476e | ||
|
|
abf9a94fc9 | ||
|
|
9a0d60ca84 | ||
|
|
90833f9d86 | ||
|
|
58387c0951 | ||
|
|
81bb188211 | ||
|
|
5821cad8c5 | ||
|
|
662805fbbd |
@@ -14,3 +14,4 @@ Duplicacy is based on the following open source projects:
|
|||||||
|https://github.com/pcwizz/xattr | BSD-2-Clause |
|
|https://github.com/pcwizz/xattr | BSD-2-Clause |
|
||||||
|https://github.com/minio/blake2b-simd | Apache-2.0 |
|
|https://github.com/minio/blake2b-simd | Apache-2.0 |
|
||||||
|https://github.com/go-ole/go-ole | MIT |
|
|https://github.com/go-ole/go-ole | MIT |
|
||||||
|
https://github.com/ncw/swift | MIT |
|
||||||
|
|||||||
6
Gopkg.lock
generated
6
Gopkg.lock
generated
@@ -153,8 +153,8 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/pkg/sftp"
|
name = "github.com/pkg/sftp"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "98203f5a8333288eb3163b7c667d4260fe1333e9"
|
revision = "3edd153f213d8d4191a0ee4577c61cca19436632"
|
||||||
version = "1.0.0"
|
version = "v1.10.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/satori/go.uuid"
|
name = "github.com/satori/go.uuid"
|
||||||
@@ -225,6 +225,6 @@
|
|||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "eff5ae2d9507f0d62cd2e5bdedebb5c59d64f70f476b087c01c35d4a5e1be72d"
|
inputs-digest = "8636a9db1eb54be5374f9914687693122efdde511f11c47d10c22f9e245e7f70"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/pkg/sftp"
|
name = "github.com/pkg/sftp"
|
||||||
version = "1.0.0"
|
version = "1.10.1"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
|||||||
@@ -201,13 +201,24 @@ func runScript(context *cli.Context, storageName string, phase string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
duplicacy.LOG_WARN("SCRIPT_ERROR", "Failed to run script: %v", err)
|
duplicacy.LOG_ERROR("SCRIPT_ERROR", "Failed to run %s script: %v", script, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadRSAPrivateKey(keyFile string, preference *duplicacy.Preference, backupManager *duplicacy.BackupManager, resetPasswords bool) {
|
||||||
|
if keyFile == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := fmt.Sprintf("Enter the passphrase for %s:", keyFile)
|
||||||
|
passphrase := duplicacy.GetPassword(*preference, "rsa_passphrase", prompt, false, resetPasswords)
|
||||||
|
backupManager.LoadRSAPrivateKey(keyFile, passphrase)
|
||||||
|
duplicacy.SavePassword(*preference, "rsa_passphrase", passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
func initRepository(context *cli.Context) {
|
func initRepository(context *cli.Context) {
|
||||||
configRepository(context, true)
|
configRepository(context, true)
|
||||||
}
|
}
|
||||||
@@ -319,6 +330,11 @@ func configRepository(context *cli.Context, init bool) {
|
|||||||
if preference.Encrypted {
|
if preference.Encrypted {
|
||||||
prompt := fmt.Sprintf("Enter storage password for %s:", preference.StorageURL)
|
prompt := fmt.Sprintf("Enter storage password for %s:", preference.StorageURL)
|
||||||
storagePassword = duplicacy.GetPassword(preference, "password", prompt, false, true)
|
storagePassword = duplicacy.GetPassword(preference, "password", prompt, false, true)
|
||||||
|
} else {
|
||||||
|
if context.String("key") != "" {
|
||||||
|
duplicacy.LOG_ERROR("STORAGE_CONFIG", "RSA encryption can't be enabled with an unencrypted storage")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
existingConfig, _, err := duplicacy.DownloadConfig(storage, storagePassword)
|
existingConfig, _, err := duplicacy.DownloadConfig(storage, storagePassword)
|
||||||
@@ -434,7 +450,7 @@ func configRepository(context *cli.Context, init bool) {
|
|||||||
iterations = duplicacy.CONFIG_DEFAULT_ITERATIONS
|
iterations = duplicacy.CONFIG_DEFAULT_ITERATIONS
|
||||||
}
|
}
|
||||||
duplicacy.ConfigStorage(storage, iterations, compressionLevel, averageChunkSize, maximumChunkSize,
|
duplicacy.ConfigStorage(storage, iterations, compressionLevel, averageChunkSize, maximumChunkSize,
|
||||||
minimumChunkSize, storagePassword, otherConfig, bitCopy)
|
minimumChunkSize, storagePassword, otherConfig, bitCopy, context.String("key"))
|
||||||
}
|
}
|
||||||
|
|
||||||
duplicacy.Preferences = append(duplicacy.Preferences, preference)
|
duplicacy.Preferences = append(duplicacy.Preferences, preference)
|
||||||
@@ -532,7 +548,13 @@ func setPreference(context *cli.Context) {
|
|||||||
newPreference.DoNotSavePassword = triBool.IsTrue()
|
newPreference.DoNotSavePassword = triBool.IsTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
newPreference.NobackupFile = context.String("nobackup-file")
|
if context.String("nobackup-file") != "" {
|
||||||
|
newPreference.NobackupFile = context.String("nobackup-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if context.String("filters") != "" {
|
||||||
|
newPreference.FiltersFile = context.String("filters")
|
||||||
|
}
|
||||||
|
|
||||||
key := context.String("key")
|
key := context.String("key")
|
||||||
value := context.String("value")
|
value := context.String("value")
|
||||||
@@ -715,7 +737,7 @@ func backupRepository(context *cli.Context) {
|
|||||||
uploadRateLimit := context.Int("limit-rate")
|
uploadRateLimit := context.Int("limit-rate")
|
||||||
enumOnly := context.Bool("enum-only")
|
enumOnly := context.Bool("enum-only")
|
||||||
storage.SetRateLimits(0, uploadRateLimit)
|
storage.SetRateLimits(0, uploadRateLimit)
|
||||||
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile)
|
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile, preference.FiltersFile)
|
||||||
duplicacy.SavePassword(*preference, "password", password)
|
duplicacy.SavePassword(*preference, "password", password)
|
||||||
|
|
||||||
backupManager.SetupSnapshotCache(preference.Name)
|
backupManager.SetupSnapshotCache(preference.Name)
|
||||||
@@ -783,10 +805,8 @@ func restoreRepository(context *cli.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
patterns = append(patterns, pattern)
|
patterns = append(patterns, pattern)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
patterns = duplicacy.ProcessFilterLines(patterns, make([]string, 0))
|
patterns = duplicacy.ProcessFilterLines(patterns, make([]string, 0))
|
||||||
|
|
||||||
duplicacy.LOG_DEBUG("REGEX_DEBUG", "There are %d compiled regular expressions stored", len(duplicacy.RegexMap))
|
duplicacy.LOG_DEBUG("REGEX_DEBUG", "There are %d compiled regular expressions stored", len(duplicacy.RegexMap))
|
||||||
@@ -794,9 +814,11 @@ func restoreRepository(context *cli.Context) {
|
|||||||
duplicacy.LOG_INFO("SNAPSHOT_FILTER", "Loaded %d include/exclude pattern(s)", len(patterns))
|
duplicacy.LOG_INFO("SNAPSHOT_FILTER", "Loaded %d include/exclude pattern(s)", len(patterns))
|
||||||
|
|
||||||
storage.SetRateLimits(context.Int("limit-rate"), 0)
|
storage.SetRateLimits(context.Int("limit-rate"), 0)
|
||||||
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile)
|
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile, preference.FiltersFile)
|
||||||
duplicacy.SavePassword(*preference, "password", password)
|
duplicacy.SavePassword(*preference, "password", password)
|
||||||
|
|
||||||
|
loadRSAPrivateKey(context.String("key"), preference, backupManager, false)
|
||||||
|
|
||||||
backupManager.SetupSnapshotCache(preference.Name)
|
backupManager.SetupSnapshotCache(preference.Name)
|
||||||
backupManager.Restore(repository, revision, true, quickMode, threads, overwrite, deleteMode, setOwner, showStatistics, patterns)
|
backupManager.Restore(repository, revision, true, quickMode, threads, overwrite, deleteMode, setOwner, showStatistics, patterns)
|
||||||
|
|
||||||
@@ -834,7 +856,7 @@ func listSnapshots(context *cli.Context) {
|
|||||||
tag := context.String("t")
|
tag := context.String("t")
|
||||||
revisions := getRevisions(context)
|
revisions := getRevisions(context)
|
||||||
|
|
||||||
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile)
|
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, "", "")
|
||||||
duplicacy.SavePassword(*preference, "password", password)
|
duplicacy.SavePassword(*preference, "password", password)
|
||||||
|
|
||||||
id := preference.SnapshotID
|
id := preference.SnapshotID
|
||||||
@@ -847,6 +869,9 @@ func listSnapshots(context *cli.Context) {
|
|||||||
showFiles := context.Bool("files")
|
showFiles := context.Bool("files")
|
||||||
showChunks := context.Bool("chunks")
|
showChunks := context.Bool("chunks")
|
||||||
|
|
||||||
|
// list doesn't need to decrypt file chunks; but we need -key here so we can reset the passphrase for the private key
|
||||||
|
loadRSAPrivateKey(context.String("key"), preference, backupManager, resetPassword)
|
||||||
|
|
||||||
backupManager.SetupSnapshotCache(preference.Name)
|
backupManager.SetupSnapshotCache(preference.Name)
|
||||||
backupManager.SnapshotManager.ListSnapshots(id, revisions, tag, showFiles, showChunks)
|
backupManager.SnapshotManager.ListSnapshots(id, revisions, tag, showFiles, showChunks)
|
||||||
|
|
||||||
@@ -882,9 +907,11 @@ func checkSnapshots(context *cli.Context) {
|
|||||||
tag := context.String("t")
|
tag := context.String("t")
|
||||||
revisions := getRevisions(context)
|
revisions := getRevisions(context)
|
||||||
|
|
||||||
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile)
|
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, "", "")
|
||||||
duplicacy.SavePassword(*preference, "password", password)
|
duplicacy.SavePassword(*preference, "password", password)
|
||||||
|
|
||||||
|
loadRSAPrivateKey(context.String("key"), preference, backupManager, false)
|
||||||
|
|
||||||
id := preference.SnapshotID
|
id := preference.SnapshotID
|
||||||
if context.Bool("all") {
|
if context.Bool("all") {
|
||||||
id = ""
|
id = ""
|
||||||
@@ -937,9 +964,11 @@ func printFile(context *cli.Context) {
|
|||||||
snapshotID = context.String("id")
|
snapshotID = context.String("id")
|
||||||
}
|
}
|
||||||
|
|
||||||
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile)
|
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, "", "")
|
||||||
duplicacy.SavePassword(*preference, "password", password)
|
duplicacy.SavePassword(*preference, "password", password)
|
||||||
|
|
||||||
|
loadRSAPrivateKey(context.String("key"), preference, backupManager, false)
|
||||||
|
|
||||||
backupManager.SetupSnapshotCache(preference.Name)
|
backupManager.SetupSnapshotCache(preference.Name)
|
||||||
|
|
||||||
file := ""
|
file := ""
|
||||||
@@ -993,11 +1022,13 @@ func diff(context *cli.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compareByHash := context.Bool("hash")
|
compareByHash := context.Bool("hash")
|
||||||
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile)
|
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, "", "")
|
||||||
duplicacy.SavePassword(*preference, "password", password)
|
duplicacy.SavePassword(*preference, "password", password)
|
||||||
|
|
||||||
|
loadRSAPrivateKey(context.String("key"), preference, backupManager, false)
|
||||||
|
|
||||||
backupManager.SetupSnapshotCache(preference.Name)
|
backupManager.SetupSnapshotCache(preference.Name)
|
||||||
backupManager.SnapshotManager.Diff(repository, snapshotID, revisions, path, compareByHash, preference.NobackupFile)
|
backupManager.SnapshotManager.Diff(repository, snapshotID, revisions, path, compareByHash, preference.NobackupFile, preference.FiltersFile)
|
||||||
|
|
||||||
runScript(context, preference.Name, "post")
|
runScript(context, preference.Name, "post")
|
||||||
}
|
}
|
||||||
@@ -1036,7 +1067,7 @@ func showHistory(context *cli.Context) {
|
|||||||
|
|
||||||
revisions := getRevisions(context)
|
revisions := getRevisions(context)
|
||||||
showLocalHash := context.Bool("hash")
|
showLocalHash := context.Bool("hash")
|
||||||
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile)
|
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, "", "")
|
||||||
duplicacy.SavePassword(*preference, "password", password)
|
duplicacy.SavePassword(*preference, "password", password)
|
||||||
|
|
||||||
backupManager.SetupSnapshotCache(preference.Name)
|
backupManager.SetupSnapshotCache(preference.Name)
|
||||||
@@ -1099,7 +1130,7 @@ func pruneSnapshots(context *cli.Context) {
|
|||||||
os.Exit(ArgumentExitCode)
|
os.Exit(ArgumentExitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile)
|
backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, "", "")
|
||||||
duplicacy.SavePassword(*preference, "password", password)
|
duplicacy.SavePassword(*preference, "password", password)
|
||||||
|
|
||||||
backupManager.SetupSnapshotCache(preference.Name)
|
backupManager.SetupSnapshotCache(preference.Name)
|
||||||
@@ -1139,10 +1170,12 @@ func copySnapshots(context *cli.Context) {
|
|||||||
sourcePassword = duplicacy.GetPassword(*source, "password", "Enter source storage password:", false, false)
|
sourcePassword = duplicacy.GetPassword(*source, "password", "Enter source storage password:", false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceManager := duplicacy.CreateBackupManager(source.SnapshotID, sourceStorage, repository, sourcePassword, source.NobackupFile)
|
sourceManager := duplicacy.CreateBackupManager(source.SnapshotID, sourceStorage, repository, sourcePassword, "", "")
|
||||||
sourceManager.SetupSnapshotCache(source.Name)
|
sourceManager.SetupSnapshotCache(source.Name)
|
||||||
duplicacy.SavePassword(*source, "password", sourcePassword)
|
duplicacy.SavePassword(*source, "password", sourcePassword)
|
||||||
|
|
||||||
|
loadRSAPrivateKey(context.String("key"), source, sourceManager, false)
|
||||||
|
|
||||||
_, destination := getRepositoryPreference(context, context.String("to"))
|
_, destination := getRepositoryPreference(context, context.String("to"))
|
||||||
|
|
||||||
if destination.Name == source.Name {
|
if destination.Name == source.Name {
|
||||||
@@ -1172,7 +1205,7 @@ func copySnapshots(context *cli.Context) {
|
|||||||
destinationStorage.SetRateLimits(0, context.Int("upload-limit-rate"))
|
destinationStorage.SetRateLimits(0, context.Int("upload-limit-rate"))
|
||||||
|
|
||||||
destinationManager := duplicacy.CreateBackupManager(destination.SnapshotID, destinationStorage, repository,
|
destinationManager := duplicacy.CreateBackupManager(destination.SnapshotID, destinationStorage, repository,
|
||||||
destinationPassword, destination.NobackupFile)
|
destinationPassword, "", "")
|
||||||
duplicacy.SavePassword(*destination, "password", destinationPassword)
|
duplicacy.SavePassword(*destination, "password", destinationPassword)
|
||||||
destinationManager.SetupSnapshotCache(destination.Name)
|
destinationManager.SetupSnapshotCache(destination.Name)
|
||||||
|
|
||||||
@@ -1350,6 +1383,11 @@ func main() {
|
|||||||
Usage: "initialize a new repository at the specified path rather than the current working directory",
|
Usage: "initialize a new repository at the specified path rather than the current working directory",
|
||||||
Argument: "<path>",
|
Argument: "<path>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "the RSA public key to encrypt file chunks",
|
||||||
|
Argument: "<public key>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "Initialize the storage if necessary and the current directory as the repository",
|
Usage: "Initialize the storage if necessary and the current directory as the repository",
|
||||||
ArgsUsage: "<snapshot id> <storage url>",
|
ArgsUsage: "<snapshot id> <storage url>",
|
||||||
@@ -1457,6 +1495,11 @@ func main() {
|
|||||||
Usage: "restore from the specified storage instead of the default one",
|
Usage: "restore from the specified storage instead of the default one",
|
||||||
Argument: "<storage name>",
|
Argument: "<storage name>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "the RSA private key to decrypt file chunks",
|
||||||
|
Argument: "<private key>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "Restore the repository to a previously saved snapshot",
|
Usage: "Restore the repository to a previously saved snapshot",
|
||||||
ArgsUsage: "[--] [pattern] ...",
|
ArgsUsage: "[--] [pattern] ...",
|
||||||
@@ -1502,6 +1545,11 @@ func main() {
|
|||||||
Usage: "retrieve snapshots from the specified storage",
|
Usage: "retrieve snapshots from the specified storage",
|
||||||
Argument: "<storage name>",
|
Argument: "<storage name>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "the RSA private key to decrypt file chunks",
|
||||||
|
Argument: "<private key>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "List snapshots",
|
Usage: "List snapshots",
|
||||||
ArgsUsage: " ",
|
ArgsUsage: " ",
|
||||||
@@ -1554,6 +1602,11 @@ func main() {
|
|||||||
Usage: "retrieve snapshots from the specified storage",
|
Usage: "retrieve snapshots from the specified storage",
|
||||||
Argument: "<storage name>",
|
Argument: "<storage name>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "the RSA private key to decrypt file chunks",
|
||||||
|
Argument: "<private key>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "Check the integrity of snapshots",
|
Usage: "Check the integrity of snapshots",
|
||||||
ArgsUsage: " ",
|
ArgsUsage: " ",
|
||||||
@@ -1577,6 +1630,11 @@ func main() {
|
|||||||
Usage: "retrieve the file from the specified storage",
|
Usage: "retrieve the file from the specified storage",
|
||||||
Argument: "<storage name>",
|
Argument: "<storage name>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "the RSA private key to decrypt file chunks",
|
||||||
|
Argument: "<private key>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "Print to stdout the specified file, or the snapshot content if no file is specified",
|
Usage: "Print to stdout the specified file, or the snapshot content if no file is specified",
|
||||||
ArgsUsage: "[<file>]",
|
ArgsUsage: "[<file>]",
|
||||||
@@ -1605,6 +1663,11 @@ func main() {
|
|||||||
Usage: "retrieve files from the specified storage",
|
Usage: "retrieve files from the specified storage",
|
||||||
Argument: "<storage name>",
|
Argument: "<storage name>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "the RSA private key to decrypt file chunks",
|
||||||
|
Argument: "<private key>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "Compare two snapshots or two revisions of a file",
|
Usage: "Compare two snapshots or two revisions of a file",
|
||||||
ArgsUsage: "[<file>]",
|
ArgsUsage: "[<file>]",
|
||||||
@@ -1769,6 +1832,11 @@ func main() {
|
|||||||
Usage: "specify the path of the repository (instead of the current working directory)",
|
Usage: "specify the path of the repository (instead of the current working directory)",
|
||||||
Argument: "<path>",
|
Argument: "<path>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "the RSA public key to encrypt file chunks",
|
||||||
|
Argument: "<public key>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "Add an additional storage to be used for the existing repository",
|
Usage: "Add an additional storage to be used for the existing repository",
|
||||||
ArgsUsage: "<storage name> <snapshot id> <storage url>",
|
ArgsUsage: "<storage name> <snapshot id> <storage url>",
|
||||||
@@ -1821,6 +1889,11 @@ func main() {
|
|||||||
Usage: "use the specified storage instead of the default one",
|
Usage: "use the specified storage instead of the default one",
|
||||||
Argument: "<storage name>",
|
Argument: "<storage name>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "filters",
|
||||||
|
Usage: "specify the path of the filters file containing include/exclude patterns",
|
||||||
|
Argument: "<file path>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "Change the options for the default or specified storage",
|
Usage: "Change the options for the default or specified storage",
|
||||||
ArgsUsage: " ",
|
ArgsUsage: " ",
|
||||||
@@ -1867,6 +1940,11 @@ func main() {
|
|||||||
Usage: "number of uploading threads",
|
Usage: "number of uploading threads",
|
||||||
Argument: "<n>",
|
Argument: "<n>",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Usage: "the RSA private key to decrypt file chunks from the source storage",
|
||||||
|
Argument: "<public key>",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Usage: "Copy snapshots between compatible storages",
|
Usage: "Copy snapshots between compatible storages",
|
||||||
ArgsUsage: " ",
|
ArgsUsage: " ",
|
||||||
@@ -1981,7 +2059,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.2.2" + " (" + GitCommit + ")"
|
app.Version = "2.4.1" + " (" + 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)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func B2Escape(path string) string {
|
|||||||
return strings.Join(components, "/")
|
return strings.Join(components, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewB2Client(applicationKeyID string, applicationKey string, storageDir string, threads int) *B2Client {
|
func NewB2Client(applicationKeyID string, applicationKey string, downloadURL string, storageDir string, threads int) *B2Client {
|
||||||
|
|
||||||
for storageDir != "" && storageDir[0] == '/' {
|
for storageDir != "" && storageDir[0] == '/' {
|
||||||
storageDir = storageDir[1:]
|
storageDir = storageDir[1:]
|
||||||
@@ -85,7 +85,7 @@ func NewB2Client(applicationKeyID string, applicationKey string, storageDir stri
|
|||||||
storageDir += "/"
|
storageDir += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
maximumRetries := 10
|
maximumRetries := 15
|
||||||
if value, found := os.LookupEnv("DUPLICACY_B2_RETRIES"); found && value != "" {
|
if value, found := os.LookupEnv("DUPLICACY_B2_RETRIES"); found && value != "" {
|
||||||
maximumRetries, _ = strconv.Atoi(value)
|
maximumRetries, _ = strconv.Atoi(value)
|
||||||
LOG_INFO("B2_RETRIES", "Setting maximum retries for B2 to %d", maximumRetries)
|
LOG_INFO("B2_RETRIES", "Setting maximum retries for B2 to %d", maximumRetries)
|
||||||
@@ -95,6 +95,7 @@ func NewB2Client(applicationKeyID string, applicationKey string, storageDir stri
|
|||||||
HTTPClient: http.DefaultClient,
|
HTTPClient: http.DefaultClient,
|
||||||
ApplicationKeyID: applicationKeyID,
|
ApplicationKeyID: applicationKeyID,
|
||||||
ApplicationKey: applicationKey,
|
ApplicationKey: applicationKey,
|
||||||
|
DownloadURL: downloadURL,
|
||||||
StorageDir: storageDir,
|
StorageDir: storageDir,
|
||||||
UploadURLs: make([]string, threads),
|
UploadURLs: make([]string, threads),
|
||||||
UploadTokens: make([]string, threads),
|
UploadTokens: make([]string, threads),
|
||||||
@@ -325,7 +326,10 @@ func (client *B2Client) AuthorizeAccount(threadIndex int) (err error, allowed bo
|
|||||||
|
|
||||||
client.AuthorizationToken = output.AuthorizationToken
|
client.AuthorizationToken = output.AuthorizationToken
|
||||||
client.APIURL = output.APIURL
|
client.APIURL = output.APIURL
|
||||||
client.DownloadURL = output.DownloadURL
|
if client.DownloadURL == "" {
|
||||||
|
client.DownloadURL = output.DownloadURL
|
||||||
|
}
|
||||||
|
LOG_INFO("BACKBLAZE_URL", "download URL is: %s", client.DownloadURL)
|
||||||
client.IsAuthorized = true
|
client.IsAuthorized = true
|
||||||
|
|
||||||
client.LastAuthorizationTime = time.Now().Unix()
|
client.LastAuthorizationTime = time.Now().Unix()
|
||||||
@@ -344,6 +348,7 @@ func (client *B2Client) FindBucket(bucketName string) (err error) {
|
|||||||
|
|
||||||
input := make(map[string]string)
|
input := make(map[string]string)
|
||||||
input["accountId"] = client.AccountID
|
input["accountId"] = client.AccountID
|
||||||
|
input["bucketName"] = bucketName
|
||||||
|
|
||||||
url := client.getAPIURL() + "/b2api/v1/b2_list_buckets"
|
url := client.getAPIURL() + "/b2api/v1/b2_list_buckets"
|
||||||
|
|
||||||
@@ -412,16 +417,16 @@ func (client *B2Client) ListFileNames(threadIndex int, startFileName string, sin
|
|||||||
input["prefix"] = client.StorageDir
|
input["prefix"] = client.StorageDir
|
||||||
|
|
||||||
for {
|
for {
|
||||||
url := client.getAPIURL() + "/b2api/v1/b2_list_file_names"
|
apiURL := client.getAPIURL() + "/b2api/v1/b2_list_file_names"
|
||||||
requestHeaders := map[string]string{}
|
requestHeaders := map[string]string{}
|
||||||
requestMethod := http.MethodPost
|
requestMethod := http.MethodPost
|
||||||
var requestInput interface{}
|
var requestInput interface{}
|
||||||
requestInput = input
|
requestInput = input
|
||||||
if includeVersions {
|
if includeVersions {
|
||||||
url = client.getAPIURL() + "/b2api/v1/b2_list_file_versions"
|
apiURL = client.getAPIURL() + "/b2api/v1/b2_list_file_versions"
|
||||||
} else if singleFile {
|
} else if singleFile {
|
||||||
// handle a single file with no versions as a special case to download the last byte of the file
|
// handle a single file with no versions as a special case to download the last byte of the file
|
||||||
url = client.getDownloadURL() + "/file/" + client.BucketName + "/" + B2Escape(client.StorageDir + startFileName)
|
apiURL = client.getDownloadURL() + "/file/" + client.BucketName + "/" + B2Escape(client.StorageDir + startFileName)
|
||||||
// requesting byte -1 works for empty files where 0-0 fails with a 416 error
|
// requesting byte -1 works for empty files where 0-0 fails with a 416 error
|
||||||
requestHeaders["Range"] = "bytes=-1"
|
requestHeaders["Range"] = "bytes=-1"
|
||||||
// HEAD request
|
// HEAD request
|
||||||
@@ -431,7 +436,7 @@ func (client *B2Client) ListFileNames(threadIndex int, startFileName string, sin
|
|||||||
var readCloser io.ReadCloser
|
var readCloser io.ReadCloser
|
||||||
var responseHeader http.Header
|
var responseHeader http.Header
|
||||||
var err error
|
var err error
|
||||||
readCloser, responseHeader, _, err = client.call(threadIndex, url, requestMethod, requestHeaders, requestInput)
|
readCloser, responseHeader, _, err = client.call(threadIndex, apiURL, requestMethod, requestHeaders, requestInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -444,7 +449,7 @@ func (client *B2Client) ListFileNames(threadIndex int, startFileName string, sin
|
|||||||
|
|
||||||
if singleFile && !includeVersions {
|
if singleFile && !includeVersions {
|
||||||
if responseHeader == nil {
|
if responseHeader == nil {
|
||||||
LOG_DEBUG("BACKBLAZE_LIST", "%s did not return headers", url)
|
LOG_DEBUG("BACKBLAZE_LIST", "%s did not return headers", apiURL)
|
||||||
return []*B2Entry{}, nil
|
return []*B2Entry{}, nil
|
||||||
}
|
}
|
||||||
requiredHeaders := []string{
|
requiredHeaders := []string{
|
||||||
@@ -458,11 +463,17 @@ func (client *B2Client) ListFileNames(threadIndex int, startFileName string, sin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(missingKeys) > 0 {
|
if len(missingKeys) > 0 {
|
||||||
return nil, fmt.Errorf("%s missing headers: %s", url, missingKeys)
|
return nil, fmt.Errorf("%s missing headers: %s", apiURL, missingKeys)
|
||||||
}
|
}
|
||||||
// construct the B2Entry from the response headers of the download request
|
// construct the B2Entry from the response headers of the download request
|
||||||
fileID := responseHeader.Get("x-bz-file-id")
|
fileID := responseHeader.Get("x-bz-file-id")
|
||||||
fileName := responseHeader.Get("x-bz-file-name")
|
fileName := responseHeader.Get("x-bz-file-name")
|
||||||
|
unescapedFileName, err := url.QueryUnescape(fileName)
|
||||||
|
if err == nil {
|
||||||
|
fileName = unescapedFileName
|
||||||
|
} else {
|
||||||
|
LOG_WARN("BACKBLAZE_UNESCAPE", "Failed to unescape the file name %s", fileName)
|
||||||
|
}
|
||||||
fileAction := "upload"
|
fileAction := "upload"
|
||||||
// byte range that is returned: "bytes #-#/#
|
// byte range that is returned: "bytes #-#/#
|
||||||
rangeString := responseHeader.Get("Content-Range")
|
rangeString := responseHeader.Get("Content-Range")
|
||||||
@@ -475,10 +486,10 @@ func (client *B2Client) ListFileNames(threadIndex int, startFileName string, sin
|
|||||||
// this should only execute if the requested file is empty and the range request didn't result in a Content-Range header
|
// this should only execute if the requested file is empty and the range request didn't result in a Content-Range header
|
||||||
fileSize, _ = strconv.ParseInt(lengthString, 0, 64)
|
fileSize, _ = strconv.ParseInt(lengthString, 0, 64)
|
||||||
if fileSize != 0 {
|
if fileSize != 0 {
|
||||||
return nil, fmt.Errorf("%s returned non-zero file length", url)
|
return nil, fmt.Errorf("%s returned non-zero file length", apiURL)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("could not parse headers returned by %s", url)
|
return nil, fmt.Errorf("could not parse headers returned by %s", apiURL)
|
||||||
}
|
}
|
||||||
fileUploadTimestamp, _ := strconv.ParseInt(responseHeader.Get("X-Bz-Upload-Timestamp"), 0, 64)
|
fileUploadTimestamp, _ := strconv.ParseInt(responseHeader.Get("X-Bz-Upload-Timestamp"), 0, 64)
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func createB2ClientForTest(t *testing.T) (*B2Client, string) {
|
|||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewB2Client(b2["account"], b2["key"], b2["directory"], 1), b2["bucket"]
|
return NewB2Client(b2["account"], b2["key"], "", b2["directory"], 1), b2["bucket"]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ type B2Storage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateB2Storage creates a B2 storage object.
|
// CreateB2Storage creates a B2 storage object.
|
||||||
func CreateB2Storage(accountID string, applicationKey string, bucket string, storageDir string, threads int) (storage *B2Storage, err error) {
|
func CreateB2Storage(accountID string, applicationKey string, downloadURL string, bucket string, storageDir string, threads int) (storage *B2Storage, err error) {
|
||||||
|
|
||||||
client := NewB2Client(accountID, applicationKey, storageDir, threads)
|
client := NewB2Client(accountID, applicationKey, downloadURL, storageDir, threads)
|
||||||
|
|
||||||
err, _ = client.AuthorizeAccount(0)
|
err, _ = client.AuthorizeAccount(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -204,7 +204,6 @@ func (storage *B2Storage) GetFileInfo(threadIndex int, filePath string) (exist b
|
|||||||
// DownloadFile reads the file at 'filePath' into the chunk.
|
// DownloadFile reads the file at 'filePath' into the chunk.
|
||||||
func (storage *B2Storage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
|
func (storage *B2Storage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
|
||||||
|
|
||||||
filePath = strings.Replace(filePath, " ", "%20", -1)
|
|
||||||
readCloser, _, err := storage.client.DownloadFile(threadIndex, filePath)
|
readCloser, _, err := storage.client.DownloadFile(threadIndex, filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -218,7 +217,6 @@ func (storage *B2Storage) DownloadFile(threadIndex int, filePath string, chunk *
|
|||||||
|
|
||||||
// UploadFile writes 'content' to the file at 'filePath'.
|
// UploadFile writes 'content' to the file at 'filePath'.
|
||||||
func (storage *B2Storage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {
|
func (storage *B2Storage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {
|
||||||
filePath = strings.Replace(filePath, " ", "%20", -1)
|
|
||||||
return storage.client.UploadFile(threadIndex, filePath, content, storage.UploadRateLimit/storage.client.Threads)
|
return storage.client.UploadFile(threadIndex, filePath, content, storage.UploadRateLimit/storage.client.Threads)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *BackupManager) SetDryRun(dryRun bool) {
|
func (manager *BackupManager) SetDryRun(dryRun bool) {
|
||||||
@@ -44,7 +45,7 @@ func (manager *BackupManager) SetDryRun(dryRun bool) {
|
|||||||
// CreateBackupManager creates a backup manager using the specified 'storage'. 'snapshotID' is a unique id to
|
// CreateBackupManager creates a backup manager using the specified 'storage'. 'snapshotID' is a unique id to
|
||||||
// identify snapshots created for this repository. 'top' is the top directory of the repository. 'password' is the
|
// identify snapshots created for this repository. 'top' is the top directory of the repository. 'password' is the
|
||||||
// master key which can be nil if encryption is not enabled.
|
// master key which can be nil if encryption is not enabled.
|
||||||
func CreateBackupManager(snapshotID string, storage Storage, top string, password string, nobackupFile string) *BackupManager {
|
func CreateBackupManager(snapshotID string, storage Storage, top string, password string, nobackupFile string, filtersFile string) *BackupManager {
|
||||||
|
|
||||||
config, _, err := DownloadConfig(storage, password)
|
config, _, err := DownloadConfig(storage, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -67,6 +68,7 @@ func CreateBackupManager(snapshotID string, storage Storage, top string, passwor
|
|||||||
config: config,
|
config: config,
|
||||||
|
|
||||||
nobackupFile: nobackupFile,
|
nobackupFile: nobackupFile,
|
||||||
|
filtersFile: filtersFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
if IsDebugging() {
|
if IsDebugging() {
|
||||||
@@ -76,6 +78,11 @@ func CreateBackupManager(snapshotID string, storage Storage, top string, passwor
|
|||||||
return backupManager
|
return backupManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadRSAPrivateKey loads the specifed private key file for decrypting file chunks
|
||||||
|
func (manager *BackupManager) LoadRSAPrivateKey(keyFile string, passphrase string) {
|
||||||
|
manager.config.loadRSAPrivateKey(keyFile, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
// SetupSnapshotCache creates the snapshot cache, which is merely a local storage under the default .duplicacy
|
// SetupSnapshotCache creates the snapshot cache, which is merely a local storage under the default .duplicacy
|
||||||
// directory
|
// directory
|
||||||
func (manager *BackupManager) SetupSnapshotCache(storageName string) bool {
|
func (manager *BackupManager) SetupSnapshotCache(storageName string) bool {
|
||||||
@@ -103,6 +110,7 @@ func (manager *BackupManager) SetupSnapshotCache(storageName string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// setEntryContent sets the 4 content pointers for each entry in 'entries'. 'offset' indicates the value
|
// setEntryContent sets the 4 content pointers for each entry in 'entries'. 'offset' indicates the value
|
||||||
// to be added to the StartChunk and EndChunk points, used when intending to append 'entries' to the
|
// to be added to the StartChunk and EndChunk points, used when intending to append 'entries' to the
|
||||||
// original unchanged entry list.
|
// original unchanged entry list.
|
||||||
@@ -176,6 +184,10 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
|
|||||||
|
|
||||||
LOG_DEBUG("BACKUP_PARAMETERS", "top: %s, quick: %t, tag: %s", top, quickMode, tag)
|
LOG_DEBUG("BACKUP_PARAMETERS", "top: %s, quick: %t, tag: %s", top, quickMode, tag)
|
||||||
|
|
||||||
|
if manager.config.rsaPublicKey != nil && len(manager.config.FileKey) > 0 {
|
||||||
|
LOG_INFO("BACKUP_KEY", "RSA encryption is enabled" )
|
||||||
|
}
|
||||||
|
|
||||||
remoteSnapshot := manager.SnapshotManager.downloadLatestSnapshot(manager.snapshotID)
|
remoteSnapshot := manager.SnapshotManager.downloadLatestSnapshot(manager.snapshotID)
|
||||||
if remoteSnapshot == nil {
|
if remoteSnapshot == nil {
|
||||||
remoteSnapshot = CreateEmptySnapshot(manager.snapshotID)
|
remoteSnapshot = CreateEmptySnapshot(manager.snapshotID)
|
||||||
@@ -188,7 +200,8 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
|
|||||||
defer DeleteShadowCopy()
|
defer DeleteShadowCopy()
|
||||||
|
|
||||||
LOG_INFO("BACKUP_INDEXING", "Indexing %s", top)
|
LOG_INFO("BACKUP_INDEXING", "Indexing %s", top)
|
||||||
localSnapshot, skippedDirectories, skippedFiles, err := CreateSnapshotFromDirectory(manager.snapshotID, shadowTop, manager.nobackupFile)
|
localSnapshot, skippedDirectories, skippedFiles, err := CreateSnapshotFromDirectory(manager.snapshotID, shadowTop,
|
||||||
|
manager.nobackupFile, manager.filtersFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("SNAPSHOT_LIST", "Failed to list the directory %s: %v", top, err)
|
LOG_ERROR("SNAPSHOT_LIST", "Failed to list the directory %s: %v", top, err)
|
||||||
return false
|
return false
|
||||||
@@ -760,7 +773,8 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
|
|||||||
remoteSnapshot := manager.SnapshotManager.DownloadSnapshot(manager.snapshotID, revision)
|
remoteSnapshot := manager.SnapshotManager.DownloadSnapshot(manager.snapshotID, revision)
|
||||||
manager.SnapshotManager.DownloadSnapshotContents(remoteSnapshot, patterns, true)
|
manager.SnapshotManager.DownloadSnapshotContents(remoteSnapshot, patterns, true)
|
||||||
|
|
||||||
localSnapshot, _, _, err := CreateSnapshotFromDirectory(manager.snapshotID, top, manager.nobackupFile)
|
localSnapshot, _, _, err := CreateSnapshotFromDirectory(manager.snapshotID, top, manager.nobackupFile,
|
||||||
|
manager.filtersFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("SNAPSHOT_LIST", "Failed to list the repository: %v", err)
|
LOG_ERROR("SNAPSHOT_LIST", "Failed to list the repository: %v", err)
|
||||||
return false
|
return false
|
||||||
@@ -1612,6 +1626,9 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These two maps store hashes of chunks in the source and destination storages, respectively. Note that
|
||||||
|
// the value of 'chunks' is used to indicated if the chunk is a snapshot chunk, while the value of 'otherChunks'
|
||||||
|
// is not used.
|
||||||
chunks := make(map[string]bool)
|
chunks := make(map[string]bool)
|
||||||
otherChunks := make(map[string]bool)
|
otherChunks := make(map[string]bool)
|
||||||
|
|
||||||
@@ -1624,21 +1641,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 {
|
||||||
if _, found := chunks[chunkHash]; !found {
|
chunks[chunkHash] = true // The chunk is a snapshot chunk
|
||||||
chunks[chunkHash] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, chunkHash := range snapshot.ChunkSequence {
|
for _, chunkHash := range snapshot.ChunkSequence {
|
||||||
if _, found := chunks[chunkHash]; !found {
|
chunks[chunkHash] = true // The chunk is a snapshot chunk
|
||||||
chunks[chunkHash] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, chunkHash := range snapshot.LengthSequence {
|
for _, chunkHash := range snapshot.LengthSequence {
|
||||||
if _, found := chunks[chunkHash]; !found {
|
chunks[chunkHash] = true // The chunk is a snapshot chunk
|
||||||
chunks[chunkHash] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
description := manager.SnapshotManager.DownloadSequence(snapshot.ChunkSequence)
|
description := manager.SnapshotManager.DownloadSequence(snapshot.ChunkSequence)
|
||||||
@@ -1651,9 +1662,11 @@ 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] = true
|
chunks[chunkHash] = false // The chunk is a file chunk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
snapshot.ChunkHashes = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
otherChunkFiles, otherChunkSizes := otherManager.SnapshotManager.ListAllFiles(otherManager.storage, "chunks/")
|
otherChunkFiles, otherChunkSizes := otherManager.SnapshotManager.ListAllFiles(otherManager.storage, "chunks/")
|
||||||
@@ -1705,7 +1718,7 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
|
|||||||
totalSkipped := 0
|
totalSkipped := 0
|
||||||
chunkIndex := 0
|
chunkIndex := 0
|
||||||
|
|
||||||
for chunkHash := range chunks {
|
for chunkHash, isSnapshot := range chunks {
|
||||||
chunkIndex++
|
chunkIndex++
|
||||||
chunkID := manager.config.GetChunkIDFromHash(chunkHash)
|
chunkID := manager.config.GetChunkIDFromHash(chunkHash)
|
||||||
newChunkID := otherManager.config.GetChunkIDFromHash(chunkHash)
|
newChunkID := otherManager.config.GetChunkIDFromHash(chunkHash)
|
||||||
@@ -1716,6 +1729,7 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho
|
|||||||
newChunk := otherManager.config.GetChunk()
|
newChunk := otherManager.config.GetChunk()
|
||||||
newChunk.Reset(true)
|
newChunk.Reset(true)
|
||||||
newChunk.Write(chunk.GetBytes())
|
newChunk.Write(chunk.GetBytes())
|
||||||
|
newChunk.isSnapshot = isSnapshot
|
||||||
chunkUploader.StartChunk(newChunk, chunkIndex)
|
chunkUploader.StartChunk(newChunk, chunkIndex)
|
||||||
totalCopied++
|
totalCopied++
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -227,11 +227,11 @@ func TestBackupManager(t *testing.T) {
|
|||||||
|
|
||||||
time.Sleep(time.Duration(delay) * time.Second)
|
time.Sleep(time.Duration(delay) * time.Second)
|
||||||
if testFixedChunkSize {
|
if testFixedChunkSize {
|
||||||
if !ConfigStorage(storage, 16384, 100, 64*1024, 64*1024, 64*1024, password, nil, false) {
|
if !ConfigStorage(storage, 16384, 100, 64*1024, 64*1024, 64*1024, password, nil, false, "") {
|
||||||
t.Errorf("Failed to initialize the storage")
|
t.Errorf("Failed to initialize the storage")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !ConfigStorage(storage, 16384, 100, 64*1024, 256*1024, 16*1024, password, nil, false) {
|
if !ConfigStorage(storage, 16384, 100, 64*1024, 256*1024, 16*1024, password, nil, false, "") {
|
||||||
t.Errorf("Failed to initialize the storage")
|
t.Errorf("Failed to initialize the storage")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ func TestBackupManager(t *testing.T) {
|
|||||||
time.Sleep(time.Duration(delay) * time.Second)
|
time.Sleep(time.Duration(delay) * time.Second)
|
||||||
|
|
||||||
SetDuplicacyPreferencePath(testDir + "/repository1/.duplicacy")
|
SetDuplicacyPreferencePath(testDir + "/repository1/.duplicacy")
|
||||||
backupManager := CreateBackupManager("host1", storage, testDir, password, "")
|
backupManager := CreateBackupManager("host1", storage, testDir, password, "", "")
|
||||||
backupManager.SetupSnapshotCache("default")
|
backupManager.SetupSnapshotCache("default")
|
||||||
|
|
||||||
SetDuplicacyPreferencePath(testDir + "/repository1/.duplicacy")
|
SetDuplicacyPreferencePath(testDir + "/repository1/.duplicacy")
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func benchmarkSplit(reader *bytes.Reader, fileSize int64, chunkSize int, compres
|
|||||||
if encryption {
|
if encryption {
|
||||||
key = "0123456789abcdef0123456789abcdef"
|
key = "0123456789abcdef0123456789abcdef"
|
||||||
}
|
}
|
||||||
err := chunk.Encrypt([]byte(key), "")
|
err := chunk.Encrypt([]byte(key), "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("BENCHMARK_ENCRYPT", "Failed to encrypt the chunk: %v", err)
|
LOG_ERROR("BENCHMARK_ENCRYPT", "Failed to encrypt the chunk: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"compress/zlib"
|
"compress/zlib"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
@@ -60,11 +62,17 @@ type Chunk struct {
|
|||||||
|
|
||||||
config *Config // Every chunk is associated with a Config object. Which hashing algorithm to use is determined
|
config *Config // Every chunk is associated with a Config object. Which hashing algorithm to use is determined
|
||||||
// by the config
|
// by the config
|
||||||
|
|
||||||
|
isSnapshot bool // Indicates if the chunk is a snapshot chunk (instead of a file chunk). This is only used by RSA
|
||||||
|
// encryption, where a snapshot chunk is not encrypted by RSA
|
||||||
}
|
}
|
||||||
|
|
||||||
// Magic word to identify a duplicacy format encrypted file, plus a version number.
|
// Magic word to identify a duplicacy format encrypted file, plus a version number.
|
||||||
var ENCRYPTION_HEADER = "duplicacy\000"
|
var ENCRYPTION_HEADER = "duplicacy\000"
|
||||||
|
|
||||||
|
// RSA encrypted chunks start with "duplicacy\002"
|
||||||
|
var ENCRYPTION_VERSION_RSA byte = 2
|
||||||
|
|
||||||
// CreateChunk creates a new chunk.
|
// CreateChunk creates a new chunk.
|
||||||
func CreateChunk(config *Config, bufferNeeded bool) *Chunk {
|
func CreateChunk(config *Config, bufferNeeded bool) *Chunk {
|
||||||
|
|
||||||
@@ -113,6 +121,7 @@ func (chunk *Chunk) Reset(hashNeeded bool) {
|
|||||||
chunk.hash = nil
|
chunk.hash = nil
|
||||||
chunk.id = ""
|
chunk.id = ""
|
||||||
chunk.size = 0
|
chunk.size = 0
|
||||||
|
chunk.isSnapshot = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write implements the Writer interface.
|
// Write implements the Writer interface.
|
||||||
@@ -170,7 +179,7 @@ func (chunk *Chunk) VerifyID() {
|
|||||||
|
|
||||||
// Encrypt encrypts the plain data stored in the chunk buffer. If derivationKey is not nil, the actual
|
// Encrypt encrypts the plain data stored in the chunk buffer. If derivationKey is not nil, the actual
|
||||||
// encryption key will be HMAC-SHA256(encryptionKey, derivationKey).
|
// encryption key will be HMAC-SHA256(encryptionKey, derivationKey).
|
||||||
func (chunk *Chunk) Encrypt(encryptionKey []byte, derivationKey string) (err error) {
|
func (chunk *Chunk) Encrypt(encryptionKey []byte, derivationKey string, isSnapshot bool) (err error) {
|
||||||
|
|
||||||
var aesBlock cipher.Block
|
var aesBlock cipher.Block
|
||||||
var gcm cipher.AEAD
|
var gcm cipher.AEAD
|
||||||
@@ -186,8 +195,17 @@ func (chunk *Chunk) Encrypt(encryptionKey []byte, derivationKey string) (err err
|
|||||||
if len(encryptionKey) > 0 {
|
if len(encryptionKey) > 0 {
|
||||||
|
|
||||||
key := encryptionKey
|
key := encryptionKey
|
||||||
|
usingRSA := false
|
||||||
if len(derivationKey) > 0 {
|
// Enable RSA encryption only when the chunk is not a snapshot chunk
|
||||||
|
if chunk.config.rsaPublicKey != nil && !isSnapshot && !chunk.isSnapshot {
|
||||||
|
randomKey := make([]byte, 32)
|
||||||
|
_, err := rand.Read(randomKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key = randomKey
|
||||||
|
usingRSA = true
|
||||||
|
} else if len(derivationKey) > 0 {
|
||||||
hasher := chunk.config.NewKeyedHasher([]byte(derivationKey))
|
hasher := chunk.config.NewKeyedHasher([]byte(derivationKey))
|
||||||
hasher.Write(encryptionKey)
|
hasher.Write(encryptionKey)
|
||||||
key = hasher.Sum(nil)
|
key = hasher.Sum(nil)
|
||||||
@@ -204,7 +222,21 @@ func (chunk *Chunk) Encrypt(encryptionKey []byte, derivationKey string) (err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start with the magic number and the version number.
|
// Start with the magic number and the version number.
|
||||||
encryptedBuffer.Write([]byte(ENCRYPTION_HEADER))
|
if usingRSA {
|
||||||
|
// RSA encryption starts "duplicacy\002"
|
||||||
|
encryptedBuffer.Write([]byte(ENCRYPTION_HEADER)[:len(ENCRYPTION_HEADER) - 1])
|
||||||
|
encryptedBuffer.Write([]byte{ENCRYPTION_VERSION_RSA})
|
||||||
|
|
||||||
|
// Then the encrypted key
|
||||||
|
encryptedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, chunk.config.rsaPublicKey, key, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
binary.Write(encryptedBuffer, binary.LittleEndian, uint16(len(encryptedKey)))
|
||||||
|
encryptedBuffer.Write(encryptedKey)
|
||||||
|
} else {
|
||||||
|
encryptedBuffer.Write([]byte(ENCRYPTION_HEADER))
|
||||||
|
}
|
||||||
|
|
||||||
// Followed by the nonce
|
// Followed by the nonce
|
||||||
nonce = make([]byte, gcm.NonceSize())
|
nonce = make([]byte, gcm.NonceSize())
|
||||||
@@ -214,7 +246,6 @@ func (chunk *Chunk) Encrypt(encryptionKey []byte, derivationKey string) (err err
|
|||||||
}
|
}
|
||||||
encryptedBuffer.Write(nonce)
|
encryptedBuffer.Write(nonce)
|
||||||
offset = encryptedBuffer.Len()
|
offset = encryptedBuffer.Len()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// offset is either 0 or the length of header + nonce
|
// offset is either 0 or the length of header + nonce
|
||||||
@@ -291,6 +322,7 @@ func (chunk *Chunk) Decrypt(encryptionKey []byte, derivationKey string) (err err
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
chunk.buffer, encryptedBuffer = encryptedBuffer, chunk.buffer
|
chunk.buffer, encryptedBuffer = encryptedBuffer, chunk.buffer
|
||||||
|
headerLength := len(ENCRYPTION_HEADER)
|
||||||
|
|
||||||
if len(encryptionKey) > 0 {
|
if len(encryptionKey) > 0 {
|
||||||
|
|
||||||
@@ -308,6 +340,41 @@ func (chunk *Chunk) Decrypt(encryptionKey []byte, derivationKey string) (err err
|
|||||||
key = hasher.Sum(nil)
|
key = hasher.Sum(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(encryptedBuffer.Bytes()) < headerLength + 12 {
|
||||||
|
return fmt.Errorf("No enough encrypted data (%d bytes) provided", len(encryptedBuffer.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(encryptedBuffer.Bytes()[:headerLength-1]) != ENCRYPTION_HEADER[:headerLength-1] {
|
||||||
|
return fmt.Errorf("The storage doesn't seem to be encrypted")
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionVersion := encryptedBuffer.Bytes()[headerLength-1]
|
||||||
|
if encryptionVersion != 0 && encryptionVersion != ENCRYPTION_VERSION_RSA {
|
||||||
|
return fmt.Errorf("Unsupported encryption version %d", encryptionVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encryptionVersion == ENCRYPTION_VERSION_RSA {
|
||||||
|
if chunk.config.rsaPrivateKey == nil {
|
||||||
|
LOG_ERROR("CHUNK_DECRYPT", "An RSA private key is required to decrypt the chunk")
|
||||||
|
return fmt.Errorf("An RSA private key is required to decrypt the chunk")
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedKeyLength := binary.LittleEndian.Uint16(encryptedBuffer.Bytes()[headerLength:headerLength+2])
|
||||||
|
|
||||||
|
if len(encryptedBuffer.Bytes()) < headerLength + 14 + int(encryptedKeyLength) {
|
||||||
|
return fmt.Errorf("No enough encrypted data (%d bytes) provided", len(encryptedBuffer.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedKey := encryptedBuffer.Bytes()[headerLength + 2:headerLength + 2 + int(encryptedKeyLength)]
|
||||||
|
headerLength += 2 + int(encryptedKeyLength)
|
||||||
|
|
||||||
|
decryptedKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, chunk.config.rsaPrivateKey, encryptedKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key = decryptedKey
|
||||||
|
}
|
||||||
|
|
||||||
aesBlock, err := aes.NewCipher(key)
|
aesBlock, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -318,21 +385,7 @@ func (chunk *Chunk) Decrypt(encryptionKey []byte, derivationKey string) (err err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
headerLength := len(ENCRYPTION_HEADER)
|
|
||||||
offset = headerLength + gcm.NonceSize()
|
offset = headerLength + gcm.NonceSize()
|
||||||
|
|
||||||
if len(encryptedBuffer.Bytes()) < offset {
|
|
||||||
return fmt.Errorf("No enough encrypted data (%d bytes) provided", len(encryptedBuffer.Bytes()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(encryptedBuffer.Bytes()[:headerLength-1]) != ENCRYPTION_HEADER[:headerLength-1] {
|
|
||||||
return fmt.Errorf("The storage doesn't seem to be encrypted")
|
|
||||||
}
|
|
||||||
|
|
||||||
if encryptedBuffer.Bytes()[headerLength-1] != 0 {
|
|
||||||
return fmt.Errorf("Unsupported encryption version %d", encryptedBuffer.Bytes()[headerLength-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce := encryptedBuffer.Bytes()[headerLength:offset]
|
nonce := encryptedBuffer.Bytes()[headerLength:offset]
|
||||||
|
|
||||||
decryptedBytes, err := gcm.Open(encryptedBuffer.Bytes()[:offset], nonce,
|
decryptedBytes, err := gcm.Open(encryptedBuffer.Bytes()[:offset], nonce,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package duplicacy
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
crypto_rand "crypto/rand"
|
crypto_rand "crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -22,6 +23,15 @@ func TestChunk(t *testing.T) {
|
|||||||
config.CompressionLevel = DEFAULT_COMPRESSION_LEVEL
|
config.CompressionLevel = DEFAULT_COMPRESSION_LEVEL
|
||||||
maxSize := 1000000
|
maxSize := 1000000
|
||||||
|
|
||||||
|
if testRSAEncryption {
|
||||||
|
privateKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to generate a random private key: %v", err)
|
||||||
|
}
|
||||||
|
config.rsaPrivateKey = privateKey
|
||||||
|
config.rsaPublicKey = privateKey.Public().(*rsa.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
remainderLength := -1
|
remainderLength := -1
|
||||||
|
|
||||||
for i := 0; i < 500; i++ {
|
for i := 0; i < 500; i++ {
|
||||||
@@ -37,7 +47,7 @@ func TestChunk(t *testing.T) {
|
|||||||
hash := chunk.GetHash()
|
hash := chunk.GetHash()
|
||||||
id := chunk.GetID()
|
id := chunk.GetID()
|
||||||
|
|
||||||
err := chunk.Encrypt(key, "")
|
err := chunk.Encrypt(key, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed to encrypt the data: %v", err)
|
t.Errorf("Failed to encrypt the data: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ func (uploader *ChunkUploader) Upload(threadIndex int, task ChunkUploadTask) boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the chunk only after we know that it must be uploaded.
|
// Encrypt the chunk only after we know that it must be uploaded.
|
||||||
err = chunk.Encrypt(uploader.config.ChunkKey, chunk.GetHash())
|
err = chunk.Encrypt(uploader.config.ChunkKey, chunk.GetHash(), uploader.snapshotCache != nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("UPLOAD_CHUNK", "Failed to encrypt the chunk %s: %v", chunkID, err)
|
LOG_ERROR("UPLOAD_CHUNK", "Failed to encrypt the chunk %s: %v", chunkID, err)
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -9,15 +9,20 @@ import (
|
|||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"io/ioutil"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
blake2 "github.com/minio/blake2b-simd"
|
blake2 "github.com/minio/blake2b-simd"
|
||||||
)
|
)
|
||||||
@@ -65,6 +70,10 @@ type Config struct {
|
|||||||
// for encrypting a non-chunk file
|
// for encrypting a non-chunk file
|
||||||
FileKey []byte `json:"-"`
|
FileKey []byte `json:"-"`
|
||||||
|
|
||||||
|
// for RSA encryption
|
||||||
|
rsaPrivateKey *rsa.PrivateKey
|
||||||
|
rsaPublicKey *rsa.PublicKey
|
||||||
|
|
||||||
chunkPool chan *Chunk
|
chunkPool chan *Chunk
|
||||||
numberOfChunks int32
|
numberOfChunks int32
|
||||||
dryRun bool
|
dryRun bool
|
||||||
@@ -80,10 +89,15 @@ type jsonableConfig struct {
|
|||||||
IDKey string `json:"id-key"`
|
IDKey string `json:"id-key"`
|
||||||
ChunkKey string `json:"chunk-key"`
|
ChunkKey string `json:"chunk-key"`
|
||||||
FileKey string `json:"file-key"`
|
FileKey string `json:"file-key"`
|
||||||
|
RSAPublicKey string `json:"rsa-public-key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) MarshalJSON() ([]byte, error) {
|
func (config *Config) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
|
publicKey := []byte {}
|
||||||
|
if config.rsaPublicKey != nil {
|
||||||
|
publicKey, _ = x509.MarshalPKIXPublicKey(config.rsaPublicKey)
|
||||||
|
}
|
||||||
return json.Marshal(&jsonableConfig{
|
return json.Marshal(&jsonableConfig{
|
||||||
aliasedConfig: (*aliasedConfig)(config),
|
aliasedConfig: (*aliasedConfig)(config),
|
||||||
ChunkSeed: hex.EncodeToString(config.ChunkSeed),
|
ChunkSeed: hex.EncodeToString(config.ChunkSeed),
|
||||||
@@ -91,6 +105,7 @@ func (config *Config) MarshalJSON() ([]byte, error) {
|
|||||||
IDKey: hex.EncodeToString(config.IDKey),
|
IDKey: hex.EncodeToString(config.IDKey),
|
||||||
ChunkKey: hex.EncodeToString(config.ChunkKey),
|
ChunkKey: hex.EncodeToString(config.ChunkKey),
|
||||||
FileKey: hex.EncodeToString(config.FileKey),
|
FileKey: hex.EncodeToString(config.FileKey),
|
||||||
|
RSAPublicKey: hex.EncodeToString(publicKey),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +135,19 @@ func (config *Config) UnmarshalJSON(description []byte) (err error) {
|
|||||||
return fmt.Errorf("Invalid representation of the file key in the config")
|
return fmt.Errorf("Invalid representation of the file key in the config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if publicKey, err := hex.DecodeString(aliased.RSAPublicKey); err != nil {
|
||||||
|
return fmt.Errorf("Invalid hex encoding of the RSA public key in the config")
|
||||||
|
} else if len(publicKey) > 0 {
|
||||||
|
parsedKey, err := x509.ParsePKIXPublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Invalid RSA public key in the config: %v", err)
|
||||||
|
}
|
||||||
|
config.rsaPublicKey = parsedKey.(*rsa.PublicKey)
|
||||||
|
if config.rsaPublicKey == nil {
|
||||||
|
return fmt.Errorf("Unsupported public key type %s in the config", reflect.TypeOf(parsedKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +168,29 @@ func (config *Config) Print() {
|
|||||||
LOG_INFO("CONFIG_INFO", "Maximum chunk size: %d", config.MaximumChunkSize)
|
LOG_INFO("CONFIG_INFO", "Maximum chunk size: %d", config.MaximumChunkSize)
|
||||||
LOG_INFO("CONFIG_INFO", "Minimum chunk size: %d", config.MinimumChunkSize)
|
LOG_INFO("CONFIG_INFO", "Minimum chunk size: %d", config.MinimumChunkSize)
|
||||||
LOG_INFO("CONFIG_INFO", "Chunk seed: %x", config.ChunkSeed)
|
LOG_INFO("CONFIG_INFO", "Chunk seed: %x", config.ChunkSeed)
|
||||||
|
|
||||||
|
LOG_TRACE("CONFIG_INFO", "Hash key: %x", config.HashKey)
|
||||||
|
LOG_TRACE("CONFIG_INFO", "ID key: %x", config.IDKey)
|
||||||
|
|
||||||
|
if len(config.ChunkKey) >= 0 {
|
||||||
|
LOG_TRACE("CONFIG_INFO", "File chunks are encrypted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.FileKey) >= 0 {
|
||||||
|
LOG_TRACE("CONFIG_INFO", "Metadata chunks are encrypted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.rsaPublicKey != nil {
|
||||||
|
pkisPublicKey, _ := x509.MarshalPKIXPublicKey(config.rsaPublicKey)
|
||||||
|
|
||||||
|
publicKey := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: pkisPublicKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
LOG_TRACE("CONFIG_INFO", "RSA public key: %s", publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateConfigFromParameters(compressionLevel int, averageChunkSize int, maximumChunkSize int, mininumChunkSize int,
|
func CreateConfigFromParameters(compressionLevel int, averageChunkSize int, maximumChunkSize int, mininumChunkSize int,
|
||||||
@@ -430,7 +481,7 @@ func UploadConfig(storage Storage, config *Config, password string, iterations i
|
|||||||
|
|
||||||
if len(password) > 0 {
|
if len(password) > 0 {
|
||||||
// Encrypt the config file with masterKey. If masterKey is nil then no encryption is performed.
|
// Encrypt the config file with masterKey. If masterKey is nil then no encryption is performed.
|
||||||
err = chunk.Encrypt(masterKey, "")
|
err = chunk.Encrypt(masterKey, "", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("CONFIG_CREATE", "Failed to create the config file: %v", err)
|
LOG_ERROR("CONFIG_CREATE", "Failed to create the config file: %v", err)
|
||||||
return false
|
return false
|
||||||
@@ -477,7 +528,7 @@ func UploadConfig(storage Storage, config *Config, password string, iterations i
|
|||||||
// it simply creates a file named 'config' that stores various parameters as well as a set of keys if encryption
|
// it simply creates a file named 'config' that stores various parameters as well as a set of keys if encryption
|
||||||
// is enabled.
|
// is enabled.
|
||||||
func ConfigStorage(storage Storage, iterations int, compressionLevel int, averageChunkSize int, maximumChunkSize int,
|
func ConfigStorage(storage Storage, iterations int, compressionLevel int, averageChunkSize int, maximumChunkSize int,
|
||||||
minimumChunkSize int, password string, copyFrom *Config, bitCopy bool) bool {
|
minimumChunkSize int, password string, copyFrom *Config, bitCopy bool, keyFile string) bool {
|
||||||
|
|
||||||
exist, _, _, err := storage.GetFileInfo(0, "config")
|
exist, _, _, err := storage.GetFileInfo(0, "config")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -496,5 +547,113 @@ func ConfigStorage(storage Storage, iterations int, compressionLevel int, averag
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if keyFile != "" {
|
||||||
|
config.loadRSAPublicKey(keyFile)
|
||||||
|
}
|
||||||
return UploadConfig(storage, config, password, iterations)
|
return UploadConfig(storage, config, password, iterations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) loadRSAPublicKey(keyFile string) {
|
||||||
|
encodedKey, err := ioutil.ReadFile(keyFile)
|
||||||
|
if err != nil {
|
||||||
|
LOG_ERROR("BACKUP_KEY", "Failed to read the public key file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedKey, _ := pem.Decode(encodedKey)
|
||||||
|
if decodedKey == nil {
|
||||||
|
LOG_ERROR("RSA_PUBLIC", "unrecognized public key in %s", keyFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if decodedKey.Type != "PUBLIC KEY" {
|
||||||
|
LOG_ERROR("RSA_PUBLIC", "Unsupported public key type %s in %s", decodedKey.Type, keyFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedKey, err := x509.ParsePKIXPublicKey(decodedKey.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
LOG_ERROR("RSA_PUBLIC", "Failed to parse the public key in %s: %v", keyFile, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key, ok := parsedKey.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
LOG_ERROR("RSA_PUBLIC", "Unsupported public key type %s in %s", reflect.TypeOf(parsedKey), keyFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.rsaPublicKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadRSAPrivateKey loads the specifed private key file for decrypting file chunks
|
||||||
|
func (config *Config) loadRSAPrivateKey(keyFile string, passphrase string) {
|
||||||
|
|
||||||
|
if config.rsaPublicKey == nil {
|
||||||
|
LOG_ERROR("RSA_PUBLIC", "The storage was not encrypted by an RSA key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedKey, err := ioutil.ReadFile(keyFile)
|
||||||
|
if err != nil {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "Failed to read the private key file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedKey, _ := pem.Decode(encodedKey)
|
||||||
|
if decodedKey == nil {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "unrecognized private key in %s", keyFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if decodedKey.Type != "RSA PRIVATE KEY" {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "Unsupported private key type %s in %s", decodedKey.Type, keyFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var decodedKeyBytes []byte
|
||||||
|
if passphrase != "" {
|
||||||
|
decodedKeyBytes, err = x509.DecryptPEMBlock(decodedKey, []byte(passphrase))
|
||||||
|
} else {
|
||||||
|
decodedKeyBytes = decodedKey.Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedKey interface{}
|
||||||
|
if parsedKey, err = x509.ParsePKCS1PrivateKey(decodedKeyBytes); err != nil {
|
||||||
|
if parsedKey, err = x509.ParsePKCS8PrivateKey(decodedKeyBytes); err != nil {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "Failed to parse the private key in %s: %v", keyFile, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key, ok := parsedKey.(*rsa.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "Unsupported private key type %s in %s", reflect.TypeOf(parsedKey), keyFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]byte, 32)
|
||||||
|
_, err = rand.Read(data)
|
||||||
|
if err != nil {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "Failed to generate random data for testing the private key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now test if the private key matches the public key
|
||||||
|
encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, config.rsaPublicKey, data, nil)
|
||||||
|
if err != nil {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "Failed to encrypt random data with the public key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, key, encryptedData, nil)
|
||||||
|
if err != nil {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "Incorrect private key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(data, decryptedData) {
|
||||||
|
LOG_ERROR("RSA_PRIVATE", "Decrypted data do not match the original data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.rsaPrivateKey = key
|
||||||
|
}
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ func ListEntries(top string, path string, fileList *[]*Entry, patterns []string,
|
|||||||
}
|
}
|
||||||
if entry.IsLink() {
|
if entry.IsLink() {
|
||||||
isRegular := false
|
isRegular := false
|
||||||
isRegular, entry.Link, err = Readlink(filepath.Join(top, entry.Path))
|
isRegular, entry.Link, err = Readlink(joinPath(top, entry.Path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_WARN("LIST_LINK", "Failed to read the symlink %s: %v", entry.Path, err)
|
LOG_WARN("LIST_LINK", "Failed to read the symlink %s: %v", entry.Path, err)
|
||||||
skippedFiles = append(skippedFiles, entry.Path)
|
skippedFiles = append(skippedFiles, entry.Path)
|
||||||
@@ -500,7 +500,7 @@ func ListEntries(top string, path string, fileList *[]*Entry, patterns []string,
|
|||||||
if isRegular {
|
if isRegular {
|
||||||
entry.Mode ^= uint32(os.ModeSymlink)
|
entry.Mode ^= uint32(os.ModeSymlink)
|
||||||
} else if path == "" && (filepath.IsAbs(entry.Link) || filepath.HasPrefix(entry.Link, `\\`)) && !strings.HasPrefix(entry.Link, normalizedTop) {
|
} else if path == "" && (filepath.IsAbs(entry.Link) || filepath.HasPrefix(entry.Link, `\\`)) && !strings.HasPrefix(entry.Link, normalizedTop) {
|
||||||
stat, err := os.Stat(filepath.Join(top, entry.Path))
|
stat, err := os.Stat(joinPath(top, entry.Path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_WARN("LIST_LINK", "Failed to read the symlink: %v", err)
|
LOG_WARN("LIST_LINK", "Failed to read the symlink: %v", err)
|
||||||
skippedFiles = append(skippedFiles, entry.Path)
|
skippedFiles = append(skippedFiles, entry.Path)
|
||||||
@@ -513,6 +513,9 @@ func ListEntries(top string, path string, fileList *[]*Entry, patterns []string,
|
|||||||
// path from f.Name(); note that a "/" is append assuming a symbolic link is always a directory
|
// path from f.Name(); note that a "/" is append assuming a symbolic link is always a directory
|
||||||
newEntry.Path = filepath.Join(normalizedPath, f.Name()) + "/"
|
newEntry.Path = filepath.Join(normalizedPath, f.Name()) + "/"
|
||||||
}
|
}
|
||||||
|
if len(patterns) > 0 && !MatchPath(newEntry.Path, patterns) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
entry = newEntry
|
entry = newEntry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Preference struct {
|
|||||||
DoNotSavePassword bool `json:"no_save_password"`
|
DoNotSavePassword bool `json:"no_save_password"`
|
||||||
NobackupFile string `json:"nobackup_file"`
|
NobackupFile string `json:"nobackup_file"`
|
||||||
Keys map[string]string `json:"keys"`
|
Keys map[string]string `json:"keys"`
|
||||||
|
FiltersFile string `json:"filters"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var preferencePath string
|
var preferencePath string
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func CreateSFTPStorage(server string, port int, username string, storageDir stri
|
|||||||
storageDir: storageDir,
|
storageDir: storageDir,
|
||||||
minimumNesting: minimumNesting,
|
minimumNesting: minimumNesting,
|
||||||
numberOfThreads: threads,
|
numberOfThreads: threads,
|
||||||
numberOfTries: 6,
|
numberOfTries: 8,
|
||||||
serverAddress: serverAddress,
|
serverAddress: serverAddress,
|
||||||
sftpConfig: sftpConfig,
|
sftpConfig: sftpConfig,
|
||||||
}
|
}
|
||||||
@@ -129,22 +129,19 @@ func (storage *SFTPStorage) retry(f func () error) error {
|
|||||||
delay *= 2
|
delay *= 2
|
||||||
|
|
||||||
storage.clientLock.Lock()
|
storage.clientLock.Lock()
|
||||||
if storage.client != nil {
|
|
||||||
storage.client.Close()
|
|
||||||
storage.client = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
connection, err := ssh.Dial("tcp", storage.serverAddress, storage.sftpConfig)
|
connection, err := ssh.Dial("tcp", storage.serverAddress, storage.sftpConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
LOG_WARN("SFT_RECONNECT", "Failed to connect to %s: %v; retrying", storage.serverAddress, err)
|
||||||
storage.clientLock.Unlock()
|
storage.clientLock.Unlock()
|
||||||
return err
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := sftp.NewClient(connection)
|
client, err := sftp.NewClient(connection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
LOG_WARN("SFT_RECONNECT", "Failed to create a new SFTP client to %s: %v; retrying", storage.serverAddress, err)
|
||||||
connection.Close()
|
connection.Close()
|
||||||
storage.clientLock.Unlock()
|
storage.clientLock.Unlock()
|
||||||
return err
|
continue
|
||||||
}
|
}
|
||||||
storage.client = client
|
storage.client = client
|
||||||
storage.clientLock.Unlock()
|
storage.clientLock.Unlock()
|
||||||
@@ -275,36 +272,19 @@ func (storage *SFTPStorage) UploadFile(threadIndex int, filePath string, content
|
|||||||
fullPath := path.Join(storage.storageDir, filePath)
|
fullPath := path.Join(storage.storageDir, filePath)
|
||||||
|
|
||||||
dirs := strings.Split(filePath, "/")
|
dirs := strings.Split(filePath, "/")
|
||||||
if len(dirs) > 1 {
|
fullDir := path.Dir(fullPath)
|
||||||
fullDir := path.Dir(fullPath)
|
return storage.retry(func() error {
|
||||||
err = storage.retry(func() error {
|
|
||||||
_, err := storage.getSFTPClient().Stat(fullDir)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// The error may be caused by a non-existent fullDir, or a broken connection. In either case,
|
|
||||||
// we just assume it is the former because there isn't a way to tell which is the case.
|
|
||||||
for i := range dirs[1 : len(dirs)-1] {
|
|
||||||
subDir := path.Join(storage.storageDir, path.Join(dirs[0:i+2]...))
|
|
||||||
// We don't check the error; just keep going blindly but always store the last err
|
|
||||||
err = storage.getSFTPClient().Mkdir(subDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is an error creating the dirs, we check fullDir one more time, because another thread
|
if len(dirs) > 1 {
|
||||||
// may happen to create the same fullDir ahead of this thread
|
_, err := storage.getSFTPClient().Stat(fullDir)
|
||||||
if err != nil {
|
if os.IsNotExist(err) {
|
||||||
err = storage.retry(func() error {
|
for i := range dirs[1 : len(dirs)-1] {
|
||||||
_, err := storage.getSFTPClient().Stat(fullDir)
|
subDir := path.Join(storage.storageDir, path.Join(dirs[0:i+2]...))
|
||||||
return err
|
// We don't check the error; just keep going blindly
|
||||||
})
|
storage.getSFTPClient().Mkdir(subDir)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return storage.retry(func() error {
|
|
||||||
|
|
||||||
letters := "abcdefghijklmnopqrstuvwxyz"
|
letters := "abcdefghijklmnopqrstuvwxyz"
|
||||||
suffix := make([]byte, 8)
|
suffix := make([]byte, 8)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -123,11 +124,11 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow
|
|||||||
}
|
}
|
||||||
deviceIdRepository, err := GetPathDeviceId(top)
|
deviceIdRepository, err := GetPathDeviceId(top)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("VSS_INIT", "Unable to get device ID of path: ", top)
|
LOG_ERROR("VSS_INIT", "Unable to get device ID of path: %s", top)
|
||||||
return top
|
return top
|
||||||
}
|
}
|
||||||
if deviceIdLocal != deviceIdRepository {
|
if deviceIdLocal != deviceIdRepository {
|
||||||
LOG_WARN("VSS_PATH", "VSS not supported for non-local repository path: ", top)
|
LOG_WARN("VSS_PATH", "VSS not supported for non-local repository path: %s", top)
|
||||||
return top
|
return top
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,22 +146,37 @@ func CreateShadowCopy(top string, shadowCopy bool, timeoutInSeconds int) (shadow
|
|||||||
// Use tmutil to create snapshot
|
// Use tmutil to create snapshot
|
||||||
tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot")
|
tmutilOutput, err := CommandWithTimeout(timeoutInSeconds, "tmutil", "snapshot")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("VSS_CREATE", "Error while calling tmutil: ", err)
|
LOG_ERROR("VSS_CREATE", "Error while calling tmutil: %v", err)
|
||||||
return top
|
return top
|
||||||
}
|
}
|
||||||
|
|
||||||
colonPos := strings.IndexByte(tmutilOutput, ':')
|
colonPos := strings.IndexByte(tmutilOutput, ':')
|
||||||
if colonPos < 0 {
|
if colonPos < 0 {
|
||||||
LOG_ERROR("VSS_CREATE", "Snapshot creation failed: ", tmutilOutput)
|
LOG_ERROR("VSS_CREATE", "Snapshot creation failed: %s", tmutilOutput)
|
||||||
return top
|
return top
|
||||||
}
|
}
|
||||||
snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:])
|
snapshotDate = strings.TrimSpace(tmutilOutput[colonPos+1:])
|
||||||
|
|
||||||
|
tmutilOutput, err = CommandWithTimeout(timeoutInSeconds, "tmutil", "listlocalsnapshots", ".")
|
||||||
|
if err != nil {
|
||||||
|
LOG_ERROR("VSS_CREATE", "Error while calling 'tmutil listlocalsnapshots': %v", err)
|
||||||
|
return top
|
||||||
|
}
|
||||||
|
snapshotName := "com.apple.TimeMachine." + snapshotDate
|
||||||
|
|
||||||
|
r := regexp.MustCompile(`(?m)^(.+` + snapshotDate + `.*)$`)
|
||||||
|
snapshotNames := r.FindStringSubmatch(tmutilOutput)
|
||||||
|
if len(snapshotNames) > 0 {
|
||||||
|
snapshotName = snapshotNames[0]
|
||||||
|
} else {
|
||||||
|
LOG_WARN("VSS_CREATE", "Error while using 'tmutil listlocalsnapshots' to find snapshot name. Will fallback to 'com.apple.TimeMachine.SNAPSHOT_DATE'")
|
||||||
|
}
|
||||||
|
|
||||||
// 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=com.apple.TimeMachine."+snapshotDate, "/", snapshotPath)
|
"/sbin/mount", "-t", "apfs", "-o", "nobrowse,-r,-s="+snapshotName, "/", snapshotPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: ", err)
|
LOG_ERROR("VSS_CREATE", "Error while mounting snapshot: %v", err)
|
||||||
return top
|
return top
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func CreateEmptySnapshot(id string) (snapshto *Snapshot) {
|
|||||||
|
|
||||||
// CreateSnapshotFromDirectory creates a snapshot from the local directory 'top'. Only 'Files'
|
// CreateSnapshotFromDirectory creates a snapshot from the local directory 'top'. Only 'Files'
|
||||||
// will be constructed, while 'ChunkHashes' and 'ChunkLengths' can only be populated after uploading.
|
// will be constructed, while 'ChunkHashes' and 'ChunkLengths' can only be populated after uploading.
|
||||||
func CreateSnapshotFromDirectory(id string, top string, nobackupFile string) (snapshot *Snapshot, skippedDirectories []string,
|
func CreateSnapshotFromDirectory(id string, top string, nobackupFile string, filtersFile string) (snapshot *Snapshot, skippedDirectories []string,
|
||||||
skippedFiles []string, err error) {
|
skippedFiles []string, err error) {
|
||||||
|
|
||||||
snapshot = &Snapshot{
|
snapshot = &Snapshot{
|
||||||
@@ -69,7 +69,10 @@ func CreateSnapshotFromDirectory(id string, top string, nobackupFile string) (sn
|
|||||||
|
|
||||||
var patterns []string
|
var patterns []string
|
||||||
|
|
||||||
patterns = ProcessFilters()
|
if filtersFile == "" {
|
||||||
|
filtersFile = joinPath(GetDuplicacyPreferencePath(), "filters")
|
||||||
|
}
|
||||||
|
patterns = ProcessFilters(filtersFile)
|
||||||
|
|
||||||
directories := make([]*Entry, 0, 256)
|
directories := make([]*Entry, 0, 256)
|
||||||
directories = append(directories, CreateEntry("", 0, 0, 0))
|
directories = append(directories, CreateEntry("", 0, 0, 0))
|
||||||
@@ -121,8 +124,8 @@ func AppendPattern(patterns []string, new_pattern string) (new_patterns []string
|
|||||||
new_patterns = append(patterns, new_pattern)
|
new_patterns = append(patterns, new_pattern)
|
||||||
return new_patterns
|
return new_patterns
|
||||||
}
|
}
|
||||||
func ProcessFilters() (patterns []string) {
|
func ProcessFilters(filtersFile string) (patterns []string) {
|
||||||
patterns = ProcessFilterFile(joinPath(GetDuplicacyPreferencePath(), "filters"), make([]string, 0))
|
patterns = ProcessFilterFile(filtersFile, make([]string, 0))
|
||||||
|
|
||||||
LOG_DEBUG("REGEX_DEBUG", "There are %d compiled regular expressions stored", len(RegexMap))
|
LOG_DEBUG("REGEX_DEBUG", "There are %d compiled regular expressions stored", len(RegexMap))
|
||||||
|
|
||||||
|
|||||||
@@ -759,8 +759,8 @@ func (manager *SnapshotManager) ListSnapshots(snapshotID string, revisionsToList
|
|||||||
func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToCheck []int, tag string, showStatistics bool, showTabular bool,
|
func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToCheck []int, tag string, showStatistics bool, showTabular bool,
|
||||||
checkFiles bool, searchFossils bool, resurrect bool) bool {
|
checkFiles bool, searchFossils bool, resurrect bool) bool {
|
||||||
|
|
||||||
LOG_DEBUG("LIST_PARAMETERS", "id: %s, revisions: %v, tag: %s, showStatistics: %t, checkFiles: %t, searchFossils: %t, resurrect: %t",
|
LOG_DEBUG("LIST_PARAMETERS", "id: %s, revisions: %v, tag: %s, showStatistics: %t, showTabular: %t, checkFiles: %t, searchFossils: %t, resurrect: %t",
|
||||||
snapshotID, revisionsToCheck, tag, showStatistics, checkFiles, searchFossils, resurrect)
|
snapshotID, revisionsToCheck, tag, showStatistics, showTabular, checkFiles, searchFossils, resurrect)
|
||||||
|
|
||||||
snapshotMap := make(map[string][]*Snapshot)
|
snapshotMap := make(map[string][]*Snapshot)
|
||||||
var err error
|
var err error
|
||||||
@@ -790,7 +790,7 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
|
|||||||
chunkSizeMap[chunk] = allSizes[i]
|
chunkSizeMap[chunk] = allSizes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
if snapshotID == "" || showStatistics {
|
if snapshotID == "" || showStatistics || showTabular {
|
||||||
snapshotIDs, err := manager.ListSnapshotIDs()
|
snapshotIDs, err := manager.ListSnapshotIDs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("SNAPSHOT_LIST", "Failed to list all snapshots: %v", err)
|
LOG_ERROR("SNAPSHOT_LIST", "Failed to list all snapshots: %v", err)
|
||||||
@@ -810,7 +810,7 @@ func (manager *SnapshotManager) CheckSnapshots(snapshotID string, revisionsToChe
|
|||||||
for snapshotID = range snapshotMap {
|
for snapshotID = range snapshotMap {
|
||||||
|
|
||||||
revisions := revisionsToCheck
|
revisions := revisionsToCheck
|
||||||
if len(revisions) == 0 || showStatistics {
|
if len(revisions) == 0 || showStatistics || showTabular {
|
||||||
revisions, err = manager.ListSnapshotRevisions(snapshotID)
|
revisions, err = manager.ListSnapshotRevisions(snapshotID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("SNAPSHOT_LIST", "Failed to list all revisions for snapshot %s: %v", snapshotID, err)
|
LOG_ERROR("SNAPSHOT_LIST", "Failed to list all revisions for snapshot %s: %v", snapshotID, err)
|
||||||
@@ -1299,7 +1299,7 @@ func (manager *SnapshotManager) PrintFile(snapshotID string, revision int, path
|
|||||||
|
|
||||||
// Diff compares two snapshots, or two revision of a file if the file argument is given.
|
// Diff compares two snapshots, or two revision of a file if the file argument is given.
|
||||||
func (manager *SnapshotManager) Diff(top string, snapshotID string, revisions []int,
|
func (manager *SnapshotManager) Diff(top string, snapshotID string, revisions []int,
|
||||||
filePath string, compareByHash bool, nobackupFile string) bool {
|
filePath string, compareByHash bool, nobackupFile string, filtersFile string) bool {
|
||||||
|
|
||||||
LOG_DEBUG("DIFF_PARAMETERS", "top: %s, id: %s, revision: %v, path: %s, compareByHash: %t",
|
LOG_DEBUG("DIFF_PARAMETERS", "top: %s, id: %s, revision: %v, path: %s, compareByHash: %t",
|
||||||
top, snapshotID, revisions, filePath, compareByHash)
|
top, snapshotID, revisions, filePath, compareByHash)
|
||||||
@@ -1312,7 +1312,7 @@ func (manager *SnapshotManager) Diff(top string, snapshotID string, revisions []
|
|||||||
if len(revisions) <= 1 {
|
if len(revisions) <= 1 {
|
||||||
// Only scan the repository if filePath is not provided
|
// Only scan the repository if filePath is not provided
|
||||||
if len(filePath) == 0 {
|
if len(filePath) == 0 {
|
||||||
rightSnapshot, _, _, err = CreateSnapshotFromDirectory(snapshotID, top, nobackupFile)
|
rightSnapshot, _, _, err = CreateSnapshotFromDirectory(snapshotID, top, nobackupFile, filtersFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("SNAPSHOT_LIST", "Failed to list the directory %s: %v", top, err)
|
LOG_ERROR("SNAPSHOT_LIST", "Failed to list the directory %s: %v", top, err)
|
||||||
return false
|
return false
|
||||||
@@ -1858,7 +1858,7 @@ func (manager *SnapshotManager) PruneSnapshots(selfID string, snapshotID string,
|
|||||||
if _, found := newChunks[chunk]; found {
|
if _, found := newChunks[chunk]; found {
|
||||||
// The fossil is referenced so it can't be deleted.
|
// The fossil is referenced so it can't be deleted.
|
||||||
if dryRun {
|
if dryRun {
|
||||||
LOG_INFO("FOSSIL_RESURRECT", "Fossil %s would be resurrected: %v", chunk)
|
LOG_INFO("FOSSIL_RESURRECT", "Fossil %s would be resurrected", chunk)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2466,7 +2466,7 @@ func (manager *SnapshotManager) UploadFile(path string, derivationKey string, co
|
|||||||
derivationKey = derivationKey[len(derivationKey)-64:]
|
derivationKey = derivationKey[len(derivationKey)-64:]
|
||||||
}
|
}
|
||||||
|
|
||||||
err := manager.fileChunk.Encrypt(manager.config.FileKey, derivationKey)
|
err := manager.fileChunk.Encrypt(manager.config.FileKey, derivationKey, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("UPLOAD_File", "Failed to encrypt the file %s: %v", path, err)
|
LOG_ERROR("UPLOAD_File", "Failed to encrypt the file %s: %v", path, err)
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -531,7 +531,25 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
|
|||||||
accountID := GetPassword(preference, "b2_id", "Enter Backblaze account or application id:", true, resetPassword)
|
accountID := GetPassword(preference, "b2_id", "Enter Backblaze account or application id:", true, resetPassword)
|
||||||
applicationKey := GetPassword(preference, "b2_key", "Enter corresponding Backblaze application key:", true, resetPassword)
|
applicationKey := GetPassword(preference, "b2_key", "Enter corresponding Backblaze application key:", true, resetPassword)
|
||||||
|
|
||||||
b2Storage, err := CreateB2Storage(accountID, applicationKey, bucket, storageDir, threads)
|
b2Storage, err := CreateB2Storage(accountID, applicationKey, "", bucket, storageDir, threads)
|
||||||
|
if err != nil {
|
||||||
|
LOG_ERROR("STORAGE_CREATE", "Failed to load the Backblaze B2 storage at %s: %v", storageURL, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
SavePassword(preference, "b2_id", accountID)
|
||||||
|
SavePassword(preference, "b2_key", applicationKey)
|
||||||
|
return b2Storage
|
||||||
|
} else if matched[1] == "b2-custom" {
|
||||||
|
b2customUrlRegex := regexp.MustCompile(`^b2-custom://([^/]+)/([^/]+)(/(.+))?`)
|
||||||
|
matched := b2customUrlRegex.FindStringSubmatch(storageURL)
|
||||||
|
downloadURL := "https://" + matched[1]
|
||||||
|
bucket := matched[2]
|
||||||
|
storageDir := matched[4]
|
||||||
|
|
||||||
|
accountID := GetPassword(preference, "b2_id", "Enter Backblaze account or application id:", true, resetPassword)
|
||||||
|
applicationKey := GetPassword(preference, "b2_key", "Enter corresponding Backblaze application key:", true, resetPassword)
|
||||||
|
|
||||||
|
b2Storage, err := CreateB2Storage(accountID, applicationKey, downloadURL, bucket, storageDir, threads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LOG_ERROR("STORAGE_CREATE", "Failed to load the Backblaze B2 storage at %s: %v", storageURL, err)
|
LOG_ERROR("STORAGE_CREATE", "Failed to load the Backblaze B2 storage at %s: %v", storageURL, err)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ var testRateLimit int
|
|||||||
var testQuickMode bool
|
var testQuickMode bool
|
||||||
var testThreads int
|
var testThreads int
|
||||||
var testFixedChunkSize bool
|
var testFixedChunkSize bool
|
||||||
|
var testRSAEncryption bool
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.StringVar(&testStorageName, "storage", "", "the test storage to use")
|
flag.StringVar(&testStorageName, "storage", "", "the test storage to use")
|
||||||
@@ -34,6 +35,7 @@ func init() {
|
|||||||
flag.BoolVar(&testQuickMode, "quick", false, "quick test")
|
flag.BoolVar(&testQuickMode, "quick", false, "quick test")
|
||||||
flag.IntVar(&testThreads, "threads", 1, "number of downloading/uploading threads")
|
flag.IntVar(&testThreads, "threads", 1, "number of downloading/uploading threads")
|
||||||
flag.BoolVar(&testFixedChunkSize, "fixed-chunk-size", false, "fixed chunk size")
|
flag.BoolVar(&testFixedChunkSize, "fixed-chunk-size", false, "fixed chunk size")
|
||||||
|
flag.BoolVar(&testRSAEncryption, "rsa", false, "enable RSA encryption")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +109,7 @@ func loadStorage(localStoragePath string, threads int) (Storage, error) {
|
|||||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||||
return storage, err
|
return storage, err
|
||||||
} else if testStorageName == "b2" {
|
} else if testStorageName == "b2" {
|
||||||
storage, err := CreateB2Storage(config["account"], config["key"], config["bucket"], config["directory"], threads)
|
storage, err := CreateB2Storage(config["account"], config["key"], "", config["bucket"], config["directory"], threads)
|
||||||
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
storage.SetDefaultNestingLevels([]int{2, 3}, 2)
|
||||||
return storage, err
|
return storage, err
|
||||||
} else if testStorageName == "gcs-s3" {
|
} else if testStorageName == "gcs-s3" {
|
||||||
|
|||||||
Reference in New Issue
Block a user