Compare commits

...

49 Commits

Author SHA1 Message Date
gilbertchen
18964e89a1 Add missing dependencies 2017-07-21 15:29:48 -04:00
Gilbert Chen
2d1ea86d8e Calculate file hash during in-place restore 2017-07-21 15:19:11 -04:00
Gilbert Chen
d881ac9169 Sparse file support: create an empty sparse file for in-place restore 2017-07-20 23:08:55 -04:00
Gilbert Chen
1aee9bd6ef Retain the error message in the SFTP rename error 2017-07-20 10:19:11 -04:00
Gilbert Chen
f3447bb611 Improve OneDrive backend by retrying on more errors 2017-07-19 23:39:54 -04:00
Gilbert Chen
9be4927c87 Set preference path before backup and restore 2017-07-19 15:15:19 -04:00
Gilbert Chen
a0fcb8802b Fixed typos 2017-07-17 22:35:12 -04:00
gilbertchen
58cfeec6ab Update doc for include/exclude patterns 2017-07-17 13:15:58 -04:00
Gilbert Chen
0d442e736d ChunkMaker should return the chunk once it is filled 2017-07-14 23:56:40 -04:00
Gilbert Chen
b32bda162d Fixed #25: don't error out when a file can't be found in a revision 2017-07-14 15:23:00 -04:00
gilbertchen
e6767bfad4 Update LICENSE.md 2017-07-13 23:42:58 -04:00
gilbertchen
0b9e23fcd8 Update README.md 2017-07-13 23:42:28 -04:00
Gilbert Chen
7f04a79111 Replace Fair Source 5 with our own free-for-personal-use license 2017-07-13 23:33:14 -04:00
Gilbert Chen
211c6867d3 Bump version to 2.0.6 2017-07-13 15:04:56 -04:00
Gilbert Chen
4a31fcfb68 Set file sizes to -1 before creating the file reader 2017-07-13 15:00:27 -04:00
Gilbert Chen
6a4b1f2a3f Add minios to test storages 2017-07-13 14:06:36 -04:00
Gilbert Chen
483ae5e6eb Add minios:// for minio servers with SSL support 2017-07-12 21:09:19 -04:00
Gilbert Chen
f8d879d414 Add a s3c backend to support s3 compatible storages that require V2 Signing 2017-07-11 21:27:20 -04:00
Gilbert Chen
c2120ad3d5 Merge branch 'master' of https://github.com/gilbertchen/duplicacy 2017-07-11 13:45:14 -04:00
Gilbert Chen
f8764a5a79 Make the S3 backend compatible with minio 2017-07-11 13:45:06 -04:00
gilbertchen
736b4da0c3 Update README.md 2017-07-08 21:05:24 -04:00
Gilbert Chen
0aa122609a Fixed #82: force in-place mode with a non-default preference path 2017-07-07 22:21:43 -04:00
gilbertchen
18462cf585 Update README.md 2017-07-07 20:49:53 -04:00
gilbertchen
e06283f0b3 Update GUIDE.md 2017-07-07 20:24:16 -04:00
Gilbert Chen
b4f3142275 Fixed #86: increase timeouts to handle overloaded Hubic servers 2017-07-07 20:08:09 -04:00
Gilbert Chen
cdd1f26079 Merge branch 'master' of https://github.com/gilbertchen/duplicacy 2017-07-07 17:23:12 -04:00
Gilbert Chen
199e312bea Fixed a build error in TestUploaderAndDownloader 2017-07-07 17:22:59 -04:00
gilbertchen
88141216e9 Update GUIDE.md 2017-07-07 15:32:03 -04:00
gilbertchen
f9ede565ff Update README.md 2017-07-07 15:02:08 -04:00
gilbertchen
93a61a6e49 Update README.md 2017-07-07 15:01:10 -04:00
gilbertchen
7d31199631 Update README.md 2017-07-07 14:58:43 -04:00
gilbertchen
f2451911f2 Update README.md 2017-07-07 14:58:10 -04:00
gilbertchen
ac655c8780 Update README.md 2017-07-07 14:57:40 -04:00
gilbertchen
c31d2a30d9 Update README.md 2017-07-07 14:54:36 -04:00
gilbertchen
83da36cae0 Update README.md 2017-07-07 14:54:03 -04:00
gilbertchen
96e2f78096 Update README.md 2017-07-07 14:45:59 -04:00
gilbertchen
593b409329 Update README.md 2017-07-07 14:44:20 -04:00
gilbertchen
5334f45998 Update README.md 2017-07-07 14:43:07 -04:00
gilbertchen
b56baa80c3 Update README.md 2017-07-07 14:42:07 -04:00
gilbertchen
74ab8d8c23 Update README.md 2017-07-07 14:38:37 -04:00
gilbertchen
a7613ab7d9 Update README.md 2017-07-07 14:34:11 -04:00
Gilbert Chen
65127c7ab7 Fixed incorrect restore percentage: should use chunk sizes instead of file sizes 2017-07-07 12:53:56 -04:00
Gilbert Chen
09f695b3e1 Verify chunk id for snapshot chunks 2017-07-07 12:08:38 -04:00
Gilbert Chen
2908b807b9 Fixed incorrect stats during backup; also check in files missing from last commit 2017-07-06 23:12:22 -04:00
Gilbert Chen
ba3702647b Fixed #90: unprocessed files may leak into incomplete snapshot leading to incorrect file size 2017-07-06 22:06:51 -04:00
Gilbert Chen
0a149cd509 Bump version to 2.0.5 2017-07-04 15:17:27 -04:00
Gilbert Chen
2cbb72c2d0 Handle 3xx status codes from B2 2017-07-04 15:14:15 -04:00
Gilbert Chen
12134ea6ad Fixed #83: don't pass unchanged files to the chunk downloader 2017-07-04 14:50:34 -04:00
Gilbert Chen
4291bc775b Retry on authentication error for Google Drive 2017-07-02 20:49:56 -04:00
56 changed files with 756 additions and 253 deletions

View File

@@ -1,4 +1,4 @@
Duplicacy is based on the following open source project:
Duplicacy is based on the following open source projects:
| Projects | License |
|--------|:-------:|
@@ -8,7 +8,9 @@ Duplicacy is based on the following open source project:
|https://github.com/Azure/azure-sdk-for-go | Apache-2.0 |
|https://github.com/tj/go-dropbox | MIT |
|https://github.com/aws/aws-sdk-go | Apache-2.0 |
|https://github.com/goamz/goamz | LGPL with static link exception |
|https://github.com/howeyc/gopass | ISC |
|https://github.com/tmc/keyring | ISC |
|https://github.com/pcwizz/xattr | BSD-2-Clause |
|https://github.com/minio/blake2b-simd | Apache-2.0 |
|https://github.com/go-ole/go-ole | MIT |

View File

@@ -456,7 +456,7 @@ destination storage and is required.
## Include/Exclude Patterns
An include pattern starts with +, and an exclude pattern starts with -. Patterns may contain wildcard characters such as * and ? with their normal meaning.
An include pattern starts with +, and an exclude pattern starts with -. Patterns may contain wildcard characters * which matches a path string of any length, and ? matches a single character. Note that both * and ? will match any character including the path separator /.
When matching a path against a list of patterns, the path is compared with the part after + or -, one pattern at a time. Therefore, the order of the patterns is significant. If a match with an include pattern is found, the path is said to be included without further comparisons. If a match with an exclude pattern is found, the path is said to be excluded without further comparison. If a match is not found, the path will be excluded if all patterns are include patterns, but included otherwise.
@@ -500,6 +500,16 @@ Duplicacy will attempt to retrieve in three ways the storage password and the st
Note that the passwords stored in the environment variable and the preference need to be in plaintext and thus are insecure and should be avoided whenever possible.
## Cache
Duplicacy maintains a local cache under the `.duplicacy/cache` folder in the repository. Only snapshot chunks may be stored in this local cache, and file chunks are never cached.
At the end of a backup operation, Duplicacy will clean up the local cache in such a way that only chunks composing the snapshot file from the last backup will stay in the cache. All other chunks will be removed from the cache. However, if the *prune* command has been run before (which will leave a the `.duplicacy/collection` folder in the repository, then the *backup* command won't perform any cache cleanup and instead defer that to the *prune* command.
At the end of a prune operation, Duplicacy will remove all chunks from the local cache except those composing the snapshot file from the last backup (those that would be kept by the *backup* command), as well as chunks that contain information about chunks referenced by *all* backups from *all* repositories connected to the same storage url.
Other commands, such as *list*, *check*, does not clean up the local cache at all, so the local cache may keep growing if many of these commands run consecutively. However, once a *backup* or a *prune* command is invoked, the local cache should shrink to its normal size.
## Scripts
You can instruct Duplicacy to run a script before or after executing a command. For example, if you create a bash script with the name *pre-prune* under the *.duplicacy/scripts* directory, this bash script will be run before the *prune* command starts. A script named *post-prune* will be run after the *prune* command finishes. This rule applies to all commands except *init*.

View File

@@ -1,20 +1,5 @@
Copyright © 2017 Acrosync LLC
Licensor: Acrosync LLC
Software: Dulicacy
Use Limitation: 5 users
License Grant. Licensor hereby grants to each recipient of the Software (“you”) a non-exclusive, non-transferable, royalty-free and fully-paid-up license, under all of the Licensors copyright and patent rights, to use, copy, distribute, prepare derivative works of, publicly perform and display the Software, subject to the Use Limitation and the conditions set forth below.
Use Limitation. The license granted above allows use by up to the number of users per entity set forth above (the “Use Limitation”). For determining the number of users, “you” includes all affiliates, meaning legal entities controlling, controlled by, or under common control with you. If you exceed the Use Limitation, your use is subject to payment of Licensors then-current list price for licenses.
Conditions. Redistribution in source code or other forms must include a copy of this license document to be provided in a reasonable manner. Any redistribution of the Software is only allowed subject to this license.
Trademarks. This license does not grant you any right in the trademarks, service marks, brand names or logos of Licensor.
DISCLAIMER. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OR CONDITION, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. LICENSORS HEREBY DISCLAIM ALL LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE.
Termination. If you violate the terms of this license, your rights will terminate automatically and will not be reinstated without the prior written consent of Licensor. Any such termination will not affect the right of others who may have received copies of the Software from you.
* Free for personal use or commercial trial
* Non-trial commercial use requires per-user licenses available from [duplicacy.com](https://duplicacy.com/customer) at a cost of $20 per year
* Modification and redistribution are permitted, but commercial use of derivative works is subject to the same requirements of this license

View File

@@ -28,6 +28,9 @@ The [design document](https://github.com/gilbertchen/duplicacy-cli/blob/master/D
## Getting Started
<details>
<summary>Installation</summary>
Duplicacy is written in Go. You can run the following command to build the executable (which will be created under `$GOPATH/bin`):
```
@@ -36,6 +39,11 @@ go get -u github.com/gilbertchen/duplicacy/...
You can also visit the [releases page](https://github.com/gilbertchen/duplicacy-cli/releases/latest) to download the pre-built binary suitable for your platform..
</details>
<details>
<summary>Commands</summary>
Once you have the Duplicacy executable on your path, you can change to the directory that you want to back up (called *repository*) and run the *init* command:
```
@@ -51,8 +59,16 @@ You can now create snapshots of the repository by invoking the *backup* command.
$ duplicacy backup -stats
```
The *restore* command rolls back the repository to a previous revision:
```sh
$ duplicacy restore -r 1
```
Duplicacy provides a set of commands, such as list, check, diff, cat history, to manage snapshots:
```makefile
$ duplicacy list # List all snapshots
$ duplicacy check # Check integrity of snapshots
@@ -61,10 +77,6 @@ $ duplicacy cat # Print a file in a snapshot
$ duplicacy history # Show how a file changes over time
```
The *restore* command rolls back the repository to a previous revision:
```sh
$ duplicacy restore -r 1
```
The *prune* command removes snapshots by revisions, or tags, or retention policies:
@@ -102,21 +114,26 @@ $ duplicacy copy -r 1 -to s3 # Copy snapshot at revision 1 to the s3 storage
$ duplicacy copy -to s3 # Copy every snapshot to the s3 storage
```
</details>
The [User Guide](https://github.com/gilbertchen/duplicacy-cli/blob/master/GUIDE.md) contains a complete reference to
all commands and other features of Duplicacy.
## Storages
Duplicacy currently supports local file storage, SFTP, and 5 cloud storage providers.
Duplicacy currently supports local file storage, SFTP, and many cloud storage providers.
#### Local disk
<details> <summary>Local disk</summary>
```
Storage URL: /path/to/storage (on Linux or Mac OS X)
C:\path\to\storage (on Windows)
```
</details>
#### SFTP
<details> <summary>SFTP</summary>
```
Storage URL: sftp://username@server/path/to/storage
@@ -124,7 +141,9 @@ Storage URL: sftp://username@server/path/to/storage
Login methods include password authentication and public key authentication. Due to a limitation of the underlying Go SSH library, the key pair for public key authentication must be generated without a passphrase. To work with a key that has a passphrase, you can set up SSH agent forwarding which is also supported by Duplicacy.
#### Dropbox
</details>
<details> <summary>Dropbox</summary>
```
Storage URL: dropbox://path/to/storage
@@ -138,7 +157,9 @@ For Duplicacy to access your Dropbox storage, you must provide an access token t
Dropbox has two advantages over other cloud providers. First, if you are already a paid user then to use the unused space as the backup storage is basically free. Second, unlike other providers Dropbox does not charge bandwidth or API usage fees.
#### Amazon S3
</details>
<details> <summary>Amazon S3</summary>
```
Storage URL: s3://amazon.com/bucket/path/to/storage (default region is us-east-1)
@@ -148,7 +169,9 @@ Storage URL: s3://amazon.com/bucket/path/to/storage (default region is us-east-
You'll need to input an access key and a secret key to access your Amazon S3 storage.
#### Google Cloud Storage
</details>
<details> <summary>Google Cloud Storage</summary>
```
Storage URL: gcs://bucket/path/to/storage
@@ -158,7 +181,9 @@ Starting from version 2.0.0, a new Google Cloud Storage backend is added which i
You can also use the s3 protocol to access Google Cloud Storage. To do this, you must enable the [s3 interoperability](https://cloud.google.com/storage/docs/migrating#migration-simple) in your Google Cloud Storage settings and set the storage url as `s3://storage.googleapis.com/bucket/path/to/storage`.
#### Microsoft Azure
</details>
<details> <summary>Microsoft Azure</summary>
```
Storage URL: azure://account/container
@@ -166,7 +191,9 @@ Storage URL: azure://account/container
You'll need to input the access key once prompted.
#### Backblaze
</details>
<details> <summary>Backblaze B2</summary>
```
Storage URL: b2://bucket
@@ -176,7 +203,9 @@ You'll need to input the account id and application key.
Backblaze's B2 storage is not only the least expensive (at 0.5 cent per GB per month), but also the fastest. We have been working closely with their developers to leverage the full potentials provided by the B2 API in order to maximize the transfer speed.
#### Google Drive
</details>
<details> <summary>Google Drive</summary>
```
Storage URL: gcd://path/to/storage
@@ -185,7 +214,9 @@ Storage URL: gcd://path/to/storage
To use Google Drive as the storage, you first need to download a token file from https://duplicacy.com/gcd_start by
authorizing Duplicacy to access your Google Drive, and then enter the path to this token file to Duplicacy when prompted.
#### Microsoft OneDrive
</details>
<details> <summary>Microsoft OneDrive</summary>
```
Storage URL: one://path/to/storage
@@ -194,7 +225,9 @@ Storage URL: one://path/to/storage
To use Microsoft OneDrive as the storage, you first need to download a token file from https://duplicacy.com/one_start by
authorizing Duplicacy to access your OneDrive, and then enter the path to this token file to Duplicacy when prompted.
#### Hubic
</details>
<details> <summary>Hubic</summary>
```
Storage URL: hubic://path/to/storage
@@ -205,8 +238,9 @@ authorizing Duplicacy to access your Hubic drive, and then enter the path to thi
Hubic offers the most free space (25GB) of all major cloud providers and there is no bandwidth charge (same as Google Drive and OneDrive), so it may be worth a try.
</details>
## Comparison with Other Backup Tools
## Feature Comparison with Other Backup Tools
[duplicity](http://duplicity.nongnu.org) works by applying the rsync algorithm (or more specific, the [librsync](https://github.com/librsync/librsync) library)
to find the differences from previous backups and only then uploading the differences. It is the only existing backup tool with extensive cloud support -- the [long list](http://duplicity.nongnu.org/duplicity.1.html#sect7) of storage backends covers almost every cloud provider one can think of. However, duplicity's biggest flaw lies in its incremental model -- a chain of dependent backups starts with a full backup followed by a number of incremental ones, and ends when another full backup is uploaded. Deleting one backup will render useless all the subsequent backups on the same chain. Periodic full backups are required, in order to make previous backups disposable.
@@ -242,9 +276,31 @@ The following table compares the feature lists of all these backup tools:
| Snapshot Migration | No | No | No | No | No | **Yes** |
## Performance Comparison with Other Backup Tools
Duplicacy is not only more feature-rich but also faster than other backup tools. The following table lists the running times in seconds of backing up the [Linux code base](https://github.com/torvalds/linux) using Duplicacy and 3 other tools. Clearly Duplicacy is the fastest by a significant margin.
| | Duplicacy | restic | Attic | duplicity |
|:------------------:|:----------------:|:----------:|:----------:|:-----------:|
| Initial backup | 13.7 | 20.7 | 26.9 | 44.2 |
| 2nd backup | 4.8 | 8.0 | 15.4 | 19.5 |
| 3rd backup | 6.9 | 11.9 | 19.6 | 29.8 |
| 4th backup | 3.3 | 7.0 | 13.7 | 18.6 |
| 5th backup | 9.9 | 11.4 | 19.9 | 28.0 |
| 6th backup | 3.8 | 8.0 | 16.8 | 22.0 |
| 7th backup | 5.1 | 7.8 | 14.3 | 21.6 |
| 8th backup | 9.5 | 13.5 | 18.3 | 35.0 |
| 9th backup | 4.3 | 9.0 | 15.7 | 24.9 |
| 10th backup | 7.9 | 20.2 | 32.2 | 35.0 |
| 11th backup | 4.6 | 9.1 | 16.8 | 28.1 |
| 12th backup | 7.4 | 12.0 | 21.7 | 37.4 |
For more details and other speed comparison results, please visit https://github.com/gilbertchen/benchmarking. There you can also find test scripts that you can use to run your own experiments.
## License
Duplicacy CLI is released under the [Fair Source 5 License](https://fair.io), which means it is free for individual users or any company or organization with less than 5 users. If your company or organization has 5 or more users, then a license for the actual number of users must be purchased from [duplicacy.com](https://duplicacy.com/customer).
A user is defined as the owner of any files to be backed up by Duplicacy. If you are an IT administrator who uses Duplicacy to back up files for your colleagues, then each colleague will be counted in the user limit permitted by the license.
* Free for personal use or commercial trial
* Non-trial commercial use requires per-user licenses available from [duplicacy.com](https://duplicacy.com/customer) at a cost of $20 per year
* Modification and redistribution are permitted, but commercial use of derivative works is subject to the same requirements of this license

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package main
@@ -1683,7 +1683,7 @@ func main() {
app.Name = "duplicacy"
app.HelpName = "duplicacy"
app.Usage = "A new generation cloud backup tool based on lock-free deduplication"
app.Version = "2.0.4"
app.Version = "2.0.6"
// If the program is interrupted, call the RunAtError function.
c := make(chan os.Signal, 1)

View File

@@ -6,32 +6,28 @@
fixture
pushd ${TEST_REPO}
${DUPLICACY} init integration-tests $TEST_STORAGE -c 4k
${DUPLICACY} init integration-tests $TEST_STORAGE -c 4
# Create 10 20k files
add_file file1 20000
add_file file2 20000
add_file file3 20000
add_file file4 20000
add_file file5 20000
add_file file6 20000
add_file file7 20000
add_file file8 20000
add_file file9 20000
add_file file10 20000
# Create 10 small files
add_file file1 20
add_file file2 20
rm file3; touch file3
add_file file4 20
chmod u-r file4
add_file file5 20
add_file file6 20
add_file file7 20
add_file file8 20
add_file file9 20
add_file file10 20
# Limit the rate to 10k/s so the backup will take about 10 seconds
${DUPLICACY} backup -limit-rate 10 -threads 4 &
# Kill the backup after 3 seconds
DUPLICACY_PID=$!
sleep 3
kill -2 ${DUPLICACY_PID}
# Fail at the 10th chunk
env DUPLICACY_FAIL_CHUNK=10 ${DUPLICACY} backup
# Try it again to test the multiple-resume case
${DUPLICACY} backup -limit-rate 10 -threads 4&
DUPLICACY_PID=$!
sleep 3
kill -2 ${DUPLICACY_PID}
env DUPLICACY_FAIL_CHUNK=5 ${DUPLICACY} backup
add_file file1 20
add_file file2 20
# Fail the backup before uploading the snapshot
env DUPLICACY_FAIL_SNAPSHOT=true ${DUPLICACY} backup

View File

@@ -0,0 +1,28 @@
#!/bin/bash
# Testing backup and restore of sparse files
. ./test_functions.sh
fixture
pushd ${TEST_REPO}
${DUPLICACY} init integration-tests $TEST_STORAGE -c 1m
for i in `seq 1 10`; do
dd if=/dev/urandom of=file3 bs=1000 count=1000 seek=$((100000 * $i))
done
ls -lsh file3
${DUPLICACY} backup
${DUPLICACY} check --files -stats
rm file1 file3
${DUPLICACY} restore -r 1
${DUPLICACY} -v restore -r 1 -overwrite -stats -hash
ls -lsh file3
popd

View File

@@ -10,6 +10,7 @@ backup
add_file file3
backup
add_file file4
chmod u-r ${TEST_REPO}/file4
backup
add_file file5
restore

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -135,7 +135,7 @@ func (client *B2Client) call(url string, input interface{}) (io.ReadCloser, int6
return nil, 0, err
}
if response.StatusCode < 400 {
if response.StatusCode < 300 {
return response.Body, response.ContentLength, nil
}
@@ -160,6 +160,10 @@ func (client *B2Client) call(url string, input interface{}) (io.ReadCloser, int6
} else if response.StatusCode >= 500 && response.StatusCode <= 599 {
backoff = client.retry(backoff, response)
continue
} else {
LOG_INFO("BACKBLAZE_CALL", "URL request '%s' returned status code %d", url, response.StatusCode)
backoff = client.retry(backoff, response)
continue
}
defer response.Body.Close()
@@ -487,7 +491,7 @@ func (client *B2Client) UploadFile(filePath string, content []byte, rateLimit in
io.Copy(ioutil.Discard, response.Body)
response.Body.Close()
if response.StatusCode < 400 {
if response.StatusCode < 300 {
return nil
}

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -142,6 +142,12 @@ func setEntryContent(entries[] *Entry, chunkLengths[]int, offset int) {
}
totalChunkSize += int64(length)
}
// If there are some unvisited entries (which happens when saving an incomplete snapshot),
// set their sizes to -1 so they won't be saved to the incomplete snapshot
for j := i; j < len(entries); j++ {
entries[j].Size = -1
}
}
// Backup creates a snapshot for the repository 'top'. If 'quickMode' is true, only files with different sizes
@@ -248,15 +254,15 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
incompleteSnapshot.ChunkHashes = incompleteSnapshot.ChunkHashes[:lastCompleteChunk + 1]
incompleteSnapshot.ChunkLengths = incompleteSnapshot.ChunkLengths[:lastCompleteChunk + 1]
remoteSnapshot = incompleteSnapshot
LOG_INFO("FILE_SKIP", "Skipped %d files from previous incomplete backup", len(files))
}
}
var numberOfNewFileChunks int // number of new file chunks
var numberOfNewFileChunks int64 // number of new file chunks
var totalUploadedFileChunkLength int64 // total length of uploaded file chunks
var totalUploadedFileChunkBytes int64 // how many actual bytes have been uploaded
var numberOfNewSnapshotChunks int // number of new snapshot chunks
var totalUploadedSnapshotChunkLength int64 // size of uploaded snapshot chunks
var totalUploadedSnapshotChunkBytes int64 // how many actual bytes have been uploaded
@@ -355,6 +361,12 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
var uploadedChunkLengths []int
var uploadedChunkLock = &sync.Mutex{}
// Set all file sizes to -1 to indicate they haven't been processed. This must be done before creating the file
// reader because the file reader may skip inaccessible files on construction.
for _, entry := range modifiedEntries {
entry.Size = -1
}
// the file reader implements the Reader interface. When an EOF is encounter, it opens the next file unless it
// is the last file.
fileReader := CreateFileReader(shadowTop, modifiedEntries)
@@ -374,6 +386,14 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
keepUploadAlive = int64(value)
}
// Fail at the chunk specified by DUPLICACY_FAIL_CHUNK to simulate a backup error
chunkToFail := -1
if value, found := os.LookupEnv("DUPLICACY_FAIL_CHUNK"); found {
chunkToFail, _ = strconv.Atoi(value)
LOG_INFO("SNAPSHOT_FAIL", "Will abort the backup on chunk %d", chunkToFail)
}
chunkMaker := CreateChunkMaker(manager.config, false)
chunkUploader := CreateChunkUploader(manager.config, manager.storage, nil, threads, nil)
@@ -388,16 +408,16 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
if !localSnapshotReady {
// Lock it to gain exclusive access to uploadedChunkHashes and uploadedChunkLengths
uploadedChunkLock.Lock()
for _, entry := range uploadedEntries {
entry.EndChunk = -1
}
setEntryContent(uploadedEntries, uploadedChunkLengths, len(preservedChunkHashes))
if len(preservedChunkHashes) > 0 {
//localSnapshot.Files = preservedEntries
//localSnapshot.Files = append(preservedEntries, uploadedEntries...)
localSnapshot.ChunkHashes = preservedChunkHashes
localSnapshot.ChunkHashes = append(localSnapshot.ChunkHashes, uploadedChunkHashes...)
localSnapshot.ChunkLengths = preservedChunkLengths
localSnapshot.ChunkLengths = append(localSnapshot.ChunkLengths, uploadedChunkLengths...)
} else {
//localSnapshot.Files = uploadedEntries
localSnapshot.ChunkHashes = uploadedChunkHashes
localSnapshot.ChunkLengths = uploadedChunkLengths
}
@@ -428,16 +448,16 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
LOG_DEBUG("CHUNK_CACHE", "Skipped chunk %s in cache", chunk.GetID())
} else {
if uploadSize > 0 {
numberOfNewFileChunks++
totalUploadedFileChunkLength += int64(chunkSize)
totalUploadedFileChunkBytes += int64(uploadSize)
atomic.AddInt64(&numberOfNewFileChunks, 1)
atomic.AddInt64(&totalUploadedFileChunkLength, int64(chunkSize))
atomic.AddInt64(&totalUploadedFileChunkBytes, int64(uploadSize))
action = "Uploaded"
} else {
LOG_DEBUG("CHUNK_EXIST", "Skipped chunk %s in the storage", chunk.GetID())
}
}
uploadedModifiedFileSize += int64(chunkSize)
uploadedModifiedFileSize := atomic.AddInt64(&uploadedModifiedFileSize, int64(chunkSize))
if IsTracing() || showStatistics {
now := time.Now().Unix()
@@ -494,21 +514,27 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
uploadedChunkLengths = append(uploadedChunkLengths, chunkSize)
uploadedChunkLock.Unlock()
if len(uploadedChunkHashes) == chunkToFail {
LOG_ERROR("SNAPSHOT_FAIL", "Artificially fail the chunk %d for testing purposes", chunkToFail)
}
},
func (fileSize int64, hash string) (io.Reader, bool) {
// Must lock here because the RunAtError function called by other threads may access uploadedEntries
uploadedChunkLock.Lock()
defer uploadedChunkLock.Unlock()
// This function is called when a new file is needed
entry := fileReader.CurrentEntry
entry.Hash = hash
if entry.Size != fileSize {
totalModifiedFileSize += fileSize - entry.Size
entry.Size = fileSize
}
entry.Size = fileSize
uploadedEntries = append(uploadedEntries, entry)
if !showStatistics || IsTracing() || RunInBackground {
LOG_INFO("PACK_END", "Packed %s (%d)", entry.Path, entry.Size)
}
fileReader.NextFile()
if fileReader.CurrentFile != nil {
@@ -544,6 +570,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
err = manager.SnapshotManager.CheckSnapshot(localSnapshot)
if err != nil {
RunAtError = func() {} // Don't save the incomplete snapshot
LOG_ERROR("SNAPSHOT_CHECK", "The snapshot contains an error: %v", err)
return false
}
@@ -630,7 +657,7 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta
LOG_INFO("BACKUP_STATS", "All chunks: %d total, %s bytes; %d new, %s bytes, %s bytes uploaded",
len(localSnapshot.ChunkHashes) + totalSnapshotChunks,
PrettyNumber(totalFileChunkLength + totalSnapshotChunkLength),
numberOfNewFileChunks + numberOfNewSnapshotChunks,
int(numberOfNewFileChunks) + numberOfNewSnapshotChunks,
PrettyNumber(totalUploadedFileChunkLength + totalUploadedSnapshotChunkLength),
PrettyNumber(totalUploadedFileChunkBytes + totalUploadedSnapshotChunkBytes))
@@ -688,6 +715,11 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
LOG_DEBUG("RESTORE_PARAMETERS", "top: %s, revision: %d, in-place: %t, quick: %t, delete: %t",
top, revision, inPlace, quickMode, deleteMode)
if !strings.HasPrefix(GetDuplicacyPreferencePath(), top) {
LOG_INFO("RESTORE_INPLACE", "Forcing in-place mode with a non-default preference path")
inPlace = true
}
if len(patterns) > 0 {
for _, pattern := range patterns {
LOG_TRACE("RESTORE_PATTERN", "%s", pattern)
@@ -749,6 +781,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
i := 0
for _, entry := range remoteSnapshot.Files {
skipped := false
// Find local files that don't exist in the remote snapshot
for i < len(localSnapshot.Files) {
local := localSnapshot.Files[i]
@@ -760,11 +793,18 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
} else {
if compare == 0 {
i++
if quickMode && local.IsSameAs(entry) {
skipped = true
}
}
break
}
}
if skipped {
continue
}
fullPath := joinPath(top, entry.Path)
if entry.IsLink() {
stat, err := os.Lstat(fullPath)
@@ -1110,33 +1150,36 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
var offset int64
existingFile, err = os.Open(fullPath)
if err != nil && !os.IsNotExist(err) {
LOG_TRACE("DOWNLOAD_OPEN", "Can't open the existing file: %v", err)
}
fileHash := ""
if existingFile != nil {
// Break existing file into chunks.
chunkMaker.ForEachChunk(
existingFile,
func (chunk *Chunk, final bool) {
hash := chunk.GetHash()
chunkSize := chunk.GetLength()
existingChunks = append(existingChunks, hash)
existingLengths = append(existingLengths, chunkSize)
offsetMap[hash] = offset
lengthMap[hash] = chunkSize
offset += int64(chunkSize)
},
func (fileSize int64, hash string) (io.Reader, bool) {
fileHash = hash
return nil, false
})
if fileHash == entry.Hash && fileHash != "" {
LOG_TRACE("DOWNLOAD_SKIP", "File %s unchanged (by hash)", entry.Path)
return false
if err != nil {
if os.IsNotExist(err) {
if inPlace && entry.Size > 100 * 1024 * 1024 {
// Create an empty sparse file
existingFile, err = os.OpenFile(fullPath, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0600)
if err != nil {
LOG_ERROR("DOWNLOAD_CREATE", "Failed to create the file %s for in-place writing", fullPath)
return false
}
_, err = existingFile.Seek(entry.Size - 1, 0)
if err != nil {
LOG_ERROR("DOWNLOAD_CREATE", "Failed to resize the initial file %s for in-place writing", fullPath)
return false
}
_, err = existingFile.Write([]byte("\x00"))
if err != nil {
LOG_ERROR("DOWNLOAD_CREATE", "Failed to initialize the sparse file %s for in-place writing", fullPath)
return false
}
existingFile.Close()
existingFile, err = os.Open(fullPath)
if err != nil {
LOG_ERROR("DOWNLOAD_OPEN", "Can't reopen the initial file just created: %v", err)
return false
}
}
} else {
LOG_TRACE("DOWNLOAD_OPEN", "Can't open the existing file: %v", err)
}
} else {
if !overwrite {
LOG_ERROR("DOWNLOAD_OVERWRITE",
"File %s already exists. Please specify the -overwrite option to continue", entry.Path)
@@ -1144,9 +1187,79 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
}
}
if inPlace {
if existingFile == nil {
inPlace = false
fileHash := ""
if existingFile != nil {
if inPlace {
// In inplace mode, we only consider chunks in the existing file with the same offsets, so we
// break the original file at offsets retrieved from the backup
fileHasher := manager.config.NewFileHasher()
buffer := make([]byte, 64 * 1024)
err = nil
for i := entry.StartChunk; i <= entry.EndChunk; i++ {
hasher := manager.config.NewKeyedHasher(manager.config.HashKey)
chunkSize := chunkDownloader.taskList[i].chunkLength
if i == entry.StartChunk {
chunkSize -= entry.StartOffset
} else if i == entry.EndChunk {
chunkSize = entry.EndOffset
}
count := 0
for count < chunkSize {
n := chunkSize - count
if n > cap(buffer) {
n = cap(buffer)
}
n, err := existingFile.Read(buffer[:n])
if n > 0 {
hasher.Write(buffer[:n])
fileHasher.Write(buffer[:n])
count += n
}
if err == io.EOF {
break
}
if err != nil {
LOG_ERROR("DOWNLOAD_SPLIT", "Failed to read existing file: %v", err)
return false
}
}
if count > 0 {
hash := string(hasher.Sum(nil))
existingChunks = append(existingChunks, hash)
existingLengths = append(existingLengths, chunkSize)
offsetMap[hash] = offset
lengthMap[hash] = chunkSize
offset += int64(chunkSize)
}
if err == io.EOF {
break
}
}
fileHash = hex.EncodeToString(fileHasher.Sum(nil))
} else {
// If it is not inplace, we want to reuse any chunks in the existing file regardless their offets, so
// we run the chunk maker to split the original file.
chunkMaker.ForEachChunk(
existingFile,
func (chunk *Chunk, final bool) {
hash := chunk.GetHash()
chunkSize := chunk.GetLength()
existingChunks = append(existingChunks, hash)
existingLengths = append(existingLengths, chunkSize)
offsetMap[hash] = offset
lengthMap[hash] = chunkSize
offset += int64(chunkSize)
},
func (fileSize int64, hash string) (io.Reader, bool) {
fileHash = hash
return nil, false
})
}
if fileHash == entry.Hash && fileHash != "" {
LOG_TRACE("DOWNLOAD_SKIP", "File %s unchanged (by hash)", entry.Path)
return false
}
}
@@ -1162,11 +1275,20 @@ func (manager *BackupManager) RestoreFile(chunkDownloader *ChunkDownloader, chun
LOG_TRACE("DOWNLOAD_INPLACE", "Updating %s in place", fullPath)
existingFile.Close()
existingFile, err = os.OpenFile(fullPath, os.O_RDWR, 0)
if err != nil {
LOG_ERROR("DOWNLOAD_OPEN", "Failed to open the file %s for in-place writing", fullPath)
return false
if existingFile == nil {
// Create an empty file
existingFile, err = os.OpenFile(fullPath, os.O_WRONLY | os.O_CREATE | os.O_TRUNC, 0600)
if err != nil {
LOG_ERROR("DOWNLOAD_CREATE", "Failed to create the file %s for in-place writing", fullPath)
}
} else {
// Close and reopen in a different mode
existingFile.Close()
existingFile, err = os.OpenFile(fullPath, os.O_RDWR, 0)
if err != nil {
LOG_ERROR("DOWNLOAD_OPEN", "Failed to open the file %s for in-place writing", fullPath)
return false
}
}
existingFile.Seek(0, 0)

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -219,8 +219,10 @@ func TestBackupManager(t *testing.T) {
backupManager := CreateBackupManager("host1", storage, testDir, password)
backupManager.SetupSnapshotCache("default")
SetDuplicacyPreferencePath(testDir + "/repository1")
backupManager.Backup(testDir + "/repository1", /*quickMode=*/true, threads, "first", false, false)
time.Sleep(time.Duration(delay) * time.Second)
SetDuplicacyPreferencePath(testDir + "/repository2")
backupManager.Restore(testDir + "/repository2", threads, /*inPlace=*/false, /*quickMode=*/false, threads, /*overwrite=*/true,
/*deleteMode=*/false, /*showStatistics=*/false, /*patterns=*/nil)
@@ -241,8 +243,10 @@ func TestBackupManager(t *testing.T) {
modifyFile(testDir + "/repository1/file2", 0.2)
modifyFile(testDir + "/repository1/dir1/file3", 0.3)
SetDuplicacyPreferencePath(testDir + "/repository1")
backupManager.Backup(testDir + "/repository1", /*quickMode=*/true, threads, "second", false, false)
time.Sleep(time.Duration(delay) * time.Second)
SetDuplicacyPreferencePath(testDir + "/repository2")
backupManager.Restore(testDir + "/repository2", 2, /*inPlace=*/true, /*quickMode=*/true, threads, /*overwrite=*/true,
/*deleteMode=*/false, /*showStatistics=*/false, /*patterns=*/nil)
@@ -255,8 +259,10 @@ func TestBackupManager(t *testing.T) {
}
truncateFile(testDir + "/repository1/file2")
SetDuplicacyPreferencePath(testDir + "/repository1")
backupManager.Backup(testDir + "/repository1", /*quickMode=*/false, threads, "third", false, false)
time.Sleep(time.Duration(delay) * time.Second)
SetDuplicacyPreferencePath(testDir + "/repository2")
backupManager.Restore(testDir + "/repository2", 3, /*inPlace=*/true, /*quickMode=*/false, threads, /*overwrite=*/true,
/*deleteMode=*/false, /*showStatistics=*/false, /*patterns=*/nil)

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -154,6 +154,18 @@ func (chunk *Chunk) GetID() string {
return chunk.id
}
func (chunk *Chunk) VerifyID() {
hasher := chunk.config.NewKeyedHasher(chunk.config.HashKey)
hasher.Write(chunk.buffer.Bytes())
hash := hasher.Sum(nil)
hasher = chunk.config.NewKeyedHasher(chunk.config.IDKey)
hasher.Write([]byte(hash))
chunkID := hex.EncodeToString(hasher.Sum(nil))
if chunkID != chunk.GetID() {
LOG_ERROR("CHUNK_ID", "The chunk id should be %s instead of %s, length: %d", chunkID, chunk.GetID(), len(chunk.buffer.Bytes()))
}
}
// 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).
func (chunk *Chunk) Encrypt(encryptionKey []byte, derivationKey string) (err error) {

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -45,8 +45,8 @@ type ChunkDownloader struct {
completionChannel chan ChunkDownloadCompletion // A downloading goroutine sends back the chunk via this channel after downloading
startTime int64 // The time it starts downloading
totalFileSize int64 // Total file size
downloadedFileSize int64 // Downloaded file size
totalChunkSize int64 // Total chunk size
downloadedChunkSize int64 // Downloaded chunk size
numberOfDownloadedChunks int // The number of chunks that have been downloaded
numberOfDownloadingChunks int // The number of chunks still being downloaded
numberOfActiveChunks int // The number of chunks that is being downloaded or has been downloaded but not reclaimed
@@ -95,7 +95,7 @@ func (downloader *ChunkDownloader) AddFiles(snapshot *Snapshot, files [] *Entry)
downloader.taskList = nil
lastChunkIndex := -1
maximumChunks := 0
downloader.totalFileSize = 0
downloader.totalChunkSize = 0
for _, file := range files {
if file.Size == 0 {
continue
@@ -109,6 +109,7 @@ func (downloader *ChunkDownloader) AddFiles(snapshot *Snapshot, files [] *Entry)
needed: false,
}
downloader.taskList = append(downloader.taskList, task)
downloader.totalChunkSize += int64(snapshot.ChunkLengths[i])
} else {
downloader.taskList[len(downloader.taskList) - 1].needed = true
}
@@ -119,7 +120,6 @@ func (downloader *ChunkDownloader) AddFiles(snapshot *Snapshot, files [] *Entry)
if file.EndChunk - file.StartChunk > maximumChunks {
maximumChunks = file.EndChunk - file.StartChunk
}
downloader.totalFileSize += file.Size
}
}
@@ -177,12 +177,6 @@ func (downloader *ChunkDownloader) Reclaim(chunkIndex int) {
return
}
for i := downloader.lastChunkIndex; i < chunkIndex; i++ {
if !downloader.taskList[i].isDownloading {
atomic.AddInt64(&downloader.downloadedFileSize, int64(downloader.taskList[i].chunkLength))
}
}
for i, _ := range downloader.completedTasks {
if i < chunkIndex && downloader.taskList[i].chunk != nil {
downloader.config.PutChunk(downloader.taskList[i].chunk)
@@ -353,21 +347,20 @@ func (downloader *ChunkDownloader) Download(threadIndex int, task ChunkDownloadT
}
}
if (downloader.showStatistics || IsTracing()) && downloader.totalFileSize > 0 {
downloadedChunkSize := atomic.AddInt64(&downloader.downloadedChunkSize, int64(chunk.GetLength()))
atomic.AddInt64(&downloader.downloadedFileSize, int64(chunk.GetLength()))
downloadFileSize := atomic.LoadInt64(&downloader.downloadedFileSize)
if (downloader.showStatistics || IsTracing()) && downloader.totalChunkSize > 0 {
now := time.Now().Unix()
if now <= downloader.startTime {
now = downloader.startTime + 1
}
speed := downloadFileSize / (now - downloader.startTime)
speed := downloadedChunkSize / (now - downloader.startTime)
remainingTime := int64(0)
if speed > 0 {
remainingTime = (downloader.totalFileSize - downloadFileSize) / speed + 1
remainingTime = (downloader.totalChunkSize - downloadedChunkSize) / speed + 1
}
percentage := float32(downloadFileSize * 1000 / downloader.totalFileSize)
percentage := float32(downloadedChunkSize * 1000 / downloader.totalChunkSize)
LOG_INFO("DOWNLOAD_PROGRESS", "Downloaded chunk %d size %d, %sB/s %s %.1f%%",
task.chunkIndex + 1, chunk.GetLength(),
PrettySize(speed), PrettyTime(remainingTime), percentage / 10)

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -178,6 +178,9 @@ func (maker *ChunkMaker) ForEachChunk(reader io.Reader, endOfChunk func(chunk *C
fileHasher = maker.config.NewFileHasher()
isEOF = false
}
} else {
endOfChunk(chunk, false)
startNewChunk()
}
}

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -92,6 +92,11 @@ func (uploader *ChunkUploader) Upload(threadIndex int, task ChunkUploadTask) boo
chunkSize := chunk.GetLength()
chunkID := chunk.GetID()
// For a snapshot chunk, verify that its chunk id is correct
if uploader.snapshotCache != nil {
chunk.VerifyID()
}
if uploader.snapshotCache != nil && uploader.storage.IsCacheNeeded() {
// Save a copy to the local snapshot.
chunkPath, exist, _, err := uploader.snapshotCache.FindChunk(threadIndex, chunkID, false)

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -104,7 +104,7 @@ func TestUploaderAndDownloader(t *testing.T) {
chunkDownloader := CreateChunkDownloader(config, storage, nil, true, testThreads)
chunkDownloader.totalFileSize = int64(totalFileSize)
chunkDownloader.totalChunkSize = int64(totalFileSize)
for _, chunk := range chunks {
chunkDownloader.AddChunk(chunk.GetHash())

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,7 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
import (

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -32,6 +32,7 @@ type GCDStorage struct {
idCacheLock *sync.Mutex
backoff int
isConnected bool
numberOfThreads int
TestMode bool
@@ -64,6 +65,12 @@ func (storage *GCDStorage) shouldRetry(err error) (bool, error) {
// User Rate Limit Exceeded
message = "User Rate Limit Exceeded"
retry = true
} else if e.Code == 401 {
// Only retry on authorization error when storage has been connected before
if storage.isConnected {
message = "Authorization Error"
retry = true
}
}
} else if e, ok := err.(*url.Error); ok {
message = e.Error()
@@ -295,6 +302,8 @@ func CreateGCDStorage(tokenFile string, storagePath string, threads int) (storag
}
}
storage.isConnected = true
return storage, nil
}

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,11 +1,12 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
import (
"fmt"
"net"
"time"
"sync"
"bytes"
@@ -64,7 +65,17 @@ func NewHubicClient(tokenFile string) (*HubicClient, error) {
}
client := &HubicClient{
HTTPClient: http.DefaultClient,
HTTPClient: &http.Client {
Transport: &http.Transport {
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 60 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
},
},
TokenFile: tokenFile,
Token: token,
TokenLock: &sync.Mutex{},

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
// +build !windows

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -9,6 +9,7 @@ import (
"time"
"sync"
"bytes"
"strings"
"io/ioutil"
"encoding/json"
"io"
@@ -41,6 +42,7 @@ type OneDriveClient struct {
Token *oauth2.Token
TokenLock *sync.Mutex
IsConnected bool
TestMode bool
}
@@ -115,9 +117,27 @@ func (client *OneDriveClient) call(url string, method string, input interface{},
response, err = client.HTTPClient.Do(request)
if err != nil {
if client.IsConnected {
if strings.Contains(err.Error(), "TLS handshake timeout") {
// Give a long timeout regardless of backoff when a TLS timeout happens, hoping that
// idle connections are not to be reused on reconnect.
retryAfter := time.Duration(rand.Float32() * 60000 + 180000)
LOG_INFO("ONEDRIVE_RETRY", "TLS handshake timeout; retry after %d milliseconds", retryAfter)
time.Sleep(retryAfter * time.Millisecond)
} else {
// For all other errors just blindly retry until the maximum is reached
retryAfter := time.Duration(rand.Float32() * 1000.0 * float32(backoff))
LOG_INFO("ONEDRIVE_RETRY", "%v; retry after %d milliseconds", err, retryAfter)
time.Sleep(retryAfter * time.Millisecond)
}
backoff *= 2
continue
}
return nil, 0, err
}
client.IsConnected = true
if response.StatusCode < 400 {
return response.Body, response.ContentLength, nil
}
@@ -139,9 +159,9 @@ func (client *OneDriveClient) call(url string, method string, input interface{},
return nil, 0, err
}
continue
} else if response.StatusCode == 500 || response.StatusCode == 503 || response.StatusCode == 509 {
} else if response.StatusCode > 401 && response.StatusCode != 404 {
retryAfter := time.Duration(rand.Float32() * 1000.0 * float32(backoff))
LOG_INFO("ONEDRIVE_RETRY", "Response status: %d; retry after %d milliseconds", response.StatusCode, retryAfter)
LOG_INFO("ONEDRIVE_RETRY", "Response code: %d; retry after %d milliseconds", response.StatusCode, retryAfter)
time.Sleep(retryAfter * time.Millisecond)
backoff *= 2
continue
@@ -151,7 +171,6 @@ func (client *OneDriveClient) call(url string, method string, input interface{},
}
errorResponse.Error.Status = response.StatusCode
return nil, 0, errorResponse.Error
}
}
@@ -169,7 +188,7 @@ func (client *OneDriveClient) RefreshToken() (err error) {
readCloser, _, err := client.call(OneDriveRefreshTokenURL, "POST", client.Token, "")
if err != nil {
return err
return fmt.Errorf("failed to refresh the access token: %v", err)
}
defer readCloser.Close()

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

212
src/duplicacy_s3cstorage.go Normal file
View File

@@ -0,0 +1,212 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
import (
"time"
"github.com/gilbertchen/goamz/aws"
"github.com/gilbertchen/goamz/s3"
)
// S3CStorage is a storage backend for s3 compatible storages that require V2 Signing.
type S3CStorage struct {
RateLimitedStorage
buckets []*s3.Bucket
storageDir string
}
// CreateS3CStorage creates a amazon s3 storage object.
func CreateS3CStorage(regionName string, endpoint string, bucketName string, storageDir string,
accessKey string, secretKey string, threads int) (storage *S3CStorage, err error) {
var region aws.Region
if endpoint == "" {
if regionName == "" {
regionName = "us-east-1"
}
region = aws.Regions[regionName]
} else {
region = aws.Region { Name: regionName, S3Endpoint:"https://" + endpoint }
}
auth := aws.Auth{ AccessKey: accessKey, SecretKey: secretKey }
var buckets []*s3.Bucket
for i := 0; i < threads; i++ {
s3Client := s3.New(auth, region)
s3Client.AttemptStrategy = aws.AttemptStrategy{
Min: 8,
Total: 300 * time.Second,
Delay: 1000 * time.Millisecond,
}
bucket := s3Client.Bucket(bucketName)
buckets = append(buckets, bucket)
}
if len(storageDir) > 0 && storageDir[len(storageDir) - 1] != '/' {
storageDir += "/"
}
storage = &S3CStorage {
buckets: buckets,
storageDir: storageDir,
}
return storage, nil
}
// ListFiles return the list of files and subdirectories under 'dir' (non-recursively)
func (storage *S3CStorage) ListFiles(threadIndex int, dir string) (files []string, sizes []int64, err error) {
if len(dir) > 0 && dir[len(dir) - 1] != '/' {
dir += "/"
}
dirLength := len(storage.storageDir + dir)
if dir == "snapshots/" {
results, err := storage.buckets[threadIndex].List(storage.storageDir + dir, "/", "", 100)
if err != nil {
return nil, nil, err
}
for _, subDir := range results.CommonPrefixes {
files = append(files, subDir[dirLength:])
}
return files, nil, nil
} else if dir == "chunks/" {
marker := ""
for {
results, err := storage.buckets[threadIndex].List(storage.storageDir + dir, "", marker, 1000)
if err != nil {
return nil, nil, err
}
for _, object := range results.Contents {
files = append(files, object.Key[dirLength:])
sizes = append(sizes, object.Size)
}
if !results.IsTruncated {
break
}
marker = results.Contents[len(results.Contents) - 1].Key
}
return files, sizes, nil
} else {
results, err := storage.buckets[threadIndex].List(storage.storageDir + dir, "", "", 1000)
if err != nil {
return nil, nil, err
}
for _, object := range results.Contents {
files = append(files, object.Key[dirLength:])
}
return files, nil, nil
}
}
// DeleteFile deletes the file or directory at 'filePath'.
func (storage *S3CStorage) DeleteFile(threadIndex int, filePath string) (err error) {
return storage.buckets[threadIndex].Del(storage.storageDir + filePath)
}
// MoveFile renames the file.
func (storage *S3CStorage) MoveFile(threadIndex int, from string, to string) (err error) {
options := s3.CopyOptions { ContentType: "application/duplicacy" }
_, err = storage.buckets[threadIndex].PutCopy(storage.storageDir + to, s3.Private, options, storage.buckets[threadIndex].Name + "/" + storage.storageDir + from)
if err != nil {
return nil
}
return storage.DeleteFile(threadIndex, from)
}
// CreateDirectory creates a new directory.
func (storage *S3CStorage) CreateDirectory(threadIndex int, dir string) (err error) {
return nil
}
// GetFileInfo returns the information about the file or directory at 'filePath'.
func (storage *S3CStorage) GetFileInfo(threadIndex int, filePath string) (exist bool, isDir bool, size int64, err error) {
response, err := storage.buckets[threadIndex].Head(storage.storageDir + filePath, nil)
if err != nil {
if e, ok := err.(*s3.Error); ok && (e.StatusCode == 403 || e.StatusCode == 404) {
return false, false, 0, nil
} else {
return false, false, 0, err
}
}
if response.StatusCode == 403 || response.StatusCode == 404 {
return false, false, 0, nil
} else {
return true, false, response.ContentLength, nil
}
}
// FindChunk finds the chunk with the specified id. If 'isFossil' is true, it will search for chunk files with
// the suffix '.fsl'.
func (storage *S3CStorage) FindChunk(threadIndex int, chunkID string, isFossil bool) (filePath string, exist bool, size int64, err error) {
filePath = "chunks/" + chunkID
if isFossil {
filePath += ".fsl"
}
exist, _, size, err = storage.GetFileInfo(threadIndex, filePath)
if err != nil {
return "", false, 0, err
} else {
return filePath, exist, size, err
}
}
// DownloadFile reads the file at 'filePath' into the chunk.
func (storage *S3CStorage) DownloadFile(threadIndex int, filePath string, chunk *Chunk) (err error) {
readCloser, err := storage.buckets[threadIndex].GetReader(storage.storageDir + filePath)
if err != nil {
return err
}
defer readCloser.Close()
_, err = RateLimitedCopy(chunk, readCloser, storage.DownloadRateLimit / len(storage.buckets))
return err
}
// UploadFile writes 'content' to the file at 'filePath'.
func (storage *S3CStorage) UploadFile(threadIndex int, filePath string, content []byte) (err error) {
options := s3.Options { }
reader := CreateRateLimitedReader(content, storage.UploadRateLimit / len(storage.buckets))
return storage.buckets[threadIndex].PutReader(storage.storageDir + filePath, reader, int64(len(content)), "application/duplicacy", s3.Private, options)
}
// If a local snapshot cache is needed for the storage to avoid downloading/uploading chunks too often when
// managing snapshots.
func (storage *S3CStorage) IsCacheNeeded () (bool) { return true }
// If the 'MoveFile' method is implemented.
func (storage *S3CStorage) IsMoveFileImplemented() (bool) { return true }
// If the storage can guarantee strong consistency.
func (storage *S3CStorage) IsStrongConsistent() (bool) { return false }
// If the storage supports fast listing of files names.
func (storage *S3CStorage) IsFastListing() (bool) { return true }
// Enable the test mode.
func (storage *S3CStorage) EnableTestMode() {}

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -23,7 +23,8 @@ type S3Storage struct {
// CreateS3Storage creates a amazon s3 storage object.
func CreateS3Storage(regionName string, endpoint string, bucketName string, storageDir string,
accessKey string, secretKey string, threads int) (storage *S3Storage, err error) {
accessKey string, secretKey string, threads int,
isSSLSupported bool, isMinioCompatible bool) (storage *S3Storage, err error) {
token := ""
@@ -53,7 +54,9 @@ func CreateS3Storage(regionName string, endpoint string, bucketName string, stor
Region: aws.String(regionName),
Credentials: auth,
Endpoint: aws.String(endpoint),
}
S3ForcePathStyle: aws.Bool(isMinioCompatible),
DisableSSL: aws.Bool(!isSSLSupported),
}
if len(storageDir) > 0 && storageDir[len(storageDir) - 1] != '/' {
storageDir += "/"

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -285,7 +285,7 @@ func (storage *SFTPStorage) UploadFile(threadIndex int, filePath string, content
storage.client.Remove(temporaryFile)
return nil
} else {
return fmt.Errorf("Uploaded file but failed to store it at %s", fullPath)
return fmt.Errorf("Uploaded file but failed to store it at %s: %v", fullPath, err)
}
}

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
// +build !windows

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -149,6 +149,7 @@ func LoadIncompleteSnapshot() (snapshot *Snapshot) {
snapshotFile := path.Join(GetDuplicacyPreferencePath(), "incomplete")
description, err := ioutil.ReadFile(snapshotFile)
if err != nil {
LOG_DEBUG("INCOMPLETE_LOCATE", "Failed to locate incomplete snapshot: %v", err)
return nil
}
@@ -156,6 +157,7 @@ func LoadIncompleteSnapshot() (snapshot *Snapshot) {
err = json.Unmarshal(description, &incompleteSnapshot)
if err != nil {
LOG_DEBUG("INCOMPLETE_PARSE", "Failed to parse incomplete snapshot: %v", err)
return nil
}
@@ -163,6 +165,7 @@ func LoadIncompleteSnapshot() (snapshot *Snapshot) {
for _, chunkHash := range incompleteSnapshot.ChunkHashes {
hash, err := hex.DecodeString(chunkHash)
if err != nil {
LOG_DEBUG("INCOMPLETE_DECODE", "Failed to decode incomplete snapshot: %v", err)
return nil
}
chunkHashes = append(chunkHashes, string(hash))
@@ -181,7 +184,8 @@ func LoadIncompleteSnapshot() (snapshot *Snapshot) {
func SaveIncompleteSnapshot(snapshot *Snapshot) {
var files []*Entry
for _, file := range snapshot.Files {
if file.EndChunk >= 0 {
// All unprocessed files will have a size of -1
if file.Size >= 0 {
file.Attributes = nil
files = append(files, file)
} else {
@@ -199,7 +203,7 @@ func SaveIncompleteSnapshot(snapshot *Snapshot) {
ChunkLengths: snapshot.ChunkLengths,
}
description, err := json.Marshal(incompleteSnapshot)
description, err := json.MarshalIndent(incompleteSnapshot, "", " ")
if err != nil {
LOG_WARN("INCOMPLETE_ENCODE", "Failed to encode the incomplete snapshot: %v", err)
return

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -1092,15 +1092,18 @@ func (manager *SnapshotManager) RetrieveFile(snapshot *Snapshot, file *Entry, ou
}
// FindFile returns the file entry that has the given file name.
func (manager *SnapshotManager) FindFile(snapshot *Snapshot, filePath string) (*Entry) {
func (manager *SnapshotManager) FindFile(snapshot *Snapshot, filePath string, suppressError bool) (*Entry) {
for _, entry := range snapshot.Files {
if entry.Path == filePath {
return entry
}
}
LOG_ERROR("SNAPSHOT_FIND", "No file %s found in snapshot %s at revision %d",
filePath, snapshot.ID, snapshot.Revision)
if !suppressError {
LOG_ERROR("SNAPSHOT_FIND", "No file %s found in snapshot %s at revision %d",
filePath, snapshot.ID, snapshot.Revision)
}
return nil
}
@@ -1139,7 +1142,7 @@ func (manager *SnapshotManager) PrintFile(snapshotID string, revision int, path
return true
}
file := manager.FindFile(snapshot, path)
file := manager.FindFile(snapshot, path, false)
var content [] byte
if !manager.RetrieveFile(snapshot, file, func(chunk []byte) { content = append(content, chunk...) }) {
LOG_ERROR("SNAPSHOT_RETRIEVE", "File %s is corrupted in snapshot %s at revision %d",
@@ -1197,7 +1200,7 @@ func (manager *SnapshotManager) Diff(top string, snapshotID string, revisions []
}
var leftFile []byte
if !manager.RetrieveFile(leftSnapshot, manager.FindFile(leftSnapshot, filePath), func(content []byte) {
if !manager.RetrieveFile(leftSnapshot, manager.FindFile(leftSnapshot, filePath, false), func(content []byte) {
leftFile = append(leftFile, content...)
}) {
LOG_ERROR("SNAPSHOT_DIFF", "File %s is corrupted in snapshot %s at revision %d",
@@ -1207,7 +1210,7 @@ func (manager *SnapshotManager) Diff(top string, snapshotID string, revisions []
var rightFile []byte
if rightSnapshot != nil {
if !manager.RetrieveFile(rightSnapshot, manager.FindFile(rightSnapshot, filePath), func(content []byte) {
if !manager.RetrieveFile(rightSnapshot, manager.FindFile(rightSnapshot, filePath, false), func(content []byte) {
rightFile = append(rightFile, content...)
}) {
LOG_ERROR("SNAPSHOT_DIFF", "File %s is corrupted in snapshot %s at revision %d",
@@ -1376,7 +1379,7 @@ func (manager *SnapshotManager) ShowHistory(top string, snapshotID string, revis
for _, revision := range revisions {
snapshot := manager.DownloadSnapshot(snapshotID, revision)
manager.DownloadSnapshotFileSequence(snapshot, nil)
file := manager.FindFile(snapshot, filePath)
file := manager.FindFile(snapshot, filePath, true)
if file != nil {
@@ -2181,7 +2184,9 @@ func (manager *SnapshotManager) CheckSnapshot(snapshot *Snapshot) (err error) {
if len(entries) > 0 && entries[0].StartChunk != 0 {
return fmt.Errorf("The first file starts at chunk %d", entries[0].StartChunk )
}
if lastChunk < numberOfChunks - 1 {
// There may be a last chunk whose size is 0 so we allow this to happen
if lastChunk < numberOfChunks - 2 {
return fmt.Errorf("The last file ends at chunk %d but the number of chunks is %d", lastChunk, numberOfChunks)
}

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -293,7 +293,7 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
SavePassword(preference, "ssh_password", password)
}
return sftpStorage
} else if matched[1] == "s3" {
} else if matched[1] == "s3" || matched[1] == "s3c" || matched[1] == "minio" || matched[1] == "minios" {
// urlRegex := regexp.MustCompile(`^(\w+)://([\w\-]+@)?([^/]+)(/(.+))?`)
@@ -319,15 +319,27 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
accessKey := GetPassword(preference, "s3_id", "Enter S3 Access Key ID:", true, resetPassword)
secretKey := GetPassword(preference, "s3_secret", "Enter S3 Secret Access Key:", true, resetPassword)
s3Storage, err := CreateS3Storage(region, endpoint, bucket, storageDir, accessKey, secretKey, threads)
if err != nil {
LOG_ERROR("STORAGE_CREATE", "Failed to load the S3 storage at %s: %v", storageURL, err)
return nil
var err error
if matched[1] == "s3c" {
storage, err = CreateS3CStorage(region, endpoint, bucket, storageDir, accessKey, secretKey, threads)
if err != nil {
LOG_ERROR("STORAGE_CREATE", "Failed to load the S3C storage at %s: %v", storageURL, err)
return nil
}
} else {
isMinioCompatible := (matched[1] == "minio" || matched[1] == "minios")
isSSLSupported := (matched[1] == "s3" || matched[1] == "minios")
storage, err = CreateS3Storage(region, endpoint, bucket, storageDir, accessKey, secretKey, threads, isSSLSupported, isMinioCompatible)
if err != nil {
LOG_ERROR("STORAGE_CREATE", "Failed to load the S3 storage at %s: %v", storageURL, err)
return nil
}
}
SavePassword(preference, "s3_id", accessKey)
SavePassword(preference, "s3_secret", secretKey)
return s3Storage
return storage
} else if matched[1] == "dropbox" {
storageDir := matched[3] + matched[5]
token := GetPassword(preference, "dropbox_token", "Enter Dropbox access token:", true, resetPassword)

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
@@ -65,13 +65,19 @@ func loadStorage(localStoragePath string, threads int) (Storage, error) {
port, _ := strconv.Atoi(storage["port"])
return CreateSFTPStorageWithPassword(storage["server"], port, storage["username"], storage["directory"], storage["password"], threads)
} else if testStorageName == "s3" {
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads)
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads, true, false)
} else if testStorageName == "s3c" {
return CreateS3CStorage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads)
} else if testStorageName == "minio" {
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads, false, true)
} else if testStorageName == "minios" {
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads, true, true)
} else if testStorageName == "dropbox" {
return CreateDropboxStorage(storage["token"], storage["directory"], threads)
} else if testStorageName == "b2" {
return CreateB2Storage(storage["account"], storage["key"], storage["bucket"], threads)
} else if testStorageName == "gcs-s3" {
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads)
return CreateS3Storage(storage["region"], storage["endpoint"], storage["bucket"], storage["directory"], storage["access_key"], storage["secret_key"], threads, true, false)
} else if testStorageName == "gcs" {
return CreateGCSStorage(storage["token_file"], storage["bucket"], storage["directory"], threads)
} else if testStorageName == "gcs-sa" {

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
// +build !windows

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy

View File

@@ -1,6 +1,6 @@
// Copyright (c) Acrosync LLC. All rights reserved.
// Licensed under the Fair Source License 0.9 (https://fair.io/)
// User Limitation: 5 users
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy