Compare commits

..

171 Commits

Author SHA1 Message Date
Alexander Neumann
27ea0623d7 Add VERSION file for 0.7.0 2017-07-01 14:12:07 +02:00
Alexander Neumann
390e2bbddc Merge pull request #1070 from restic/warn-unsupported-repo-type
Return an error for invalid backend schemes
2017-06-30 22:15:17 +02:00
Alexander Neumann
b50fc08f39 Add entry to CHANGELOG 2017-06-30 22:15:00 +02:00
Alexander Neumann
b2ce7e8d84 Return an error for invalid backend schemes
Closes #1021
2017-06-30 21:28:39 +02:00
Alexander Neumann
2b1c6d3cf8 Merge pull request #1066 from restic/update-minio-go
Update minio-go
2017-06-30 20:40:43 +02:00
Alexander Neumann
c658305a1b Correct path for rest-server 2017-06-27 21:19:48 +02:00
Alexander Neumann
63235d8f94 Update minio-go 2017-06-26 22:06:57 +02:00
Alexander Neumann
144b7f3386 doc: Correct path in manual 2017-06-22 19:54:55 +02:00
Alexander Neumann
9583dc820f Merge pull request #1051 from restic/refactor-crypto
crypto: Make Encrypt/Decrypt a method of *Key
2017-06-21 19:26:11 +02:00
Alexander Neumann
a03076f2d8 Merge pull request #1056 from restic/fix-1053
prune: Delete invalid/incomplete pack files
2017-06-21 19:25:55 +02:00
Alexander Neumann
d76fa22b4b prune: Delete invalid/incomplete pack files
Closes #1053
2017-06-20 22:53:49 +02:00
Alexander Neumann
f960831f10 crypto: Make Encrypt/Decrypt a method of *Key 2017-06-20 22:14:51 +02:00
Alexander Neumann
b0fb95dfc9 backend tests: Use delayedRemove() 2017-06-19 20:02:49 +02:00
Alexander Neumann
bca9566849 Merge pull request #1050 from restic/extend-fuse-mount
fuse: Add more directories
2017-06-19 19:52:45 +02:00
Alexander Neumann
8760de42fe Merge pull request #1046 from restic/s3-split-open
s3: Split Create() from Open()
2017-06-19 19:52:40 +02:00
Alexander Neumann
2c02efd1fe fuse: Reduce code duplication, add MetaDir 2017-06-18 21:32:07 +02:00
Alexander Neumann
4b4a63ed44 fuse: Add tags dir 2017-06-18 21:32:07 +02:00
Alexander Neumann
64f434eca4 fuse: Add hosts dir 2017-06-18 21:32:07 +02:00
Alexander Neumann
f4e85a53e7 fuse: Add '.' and '..' entries to all directories 2017-06-18 21:32:07 +02:00
Alexander Neumann
f8176a74ec fuse: Rename DirSnapshots -> SnapshotsDir 2017-06-18 21:32:07 +02:00
Alexander Neumann
e60a96a71a swift: Increase delete timeout to 20s 2017-06-18 21:31:48 +02:00
Alexander Neumann
216e2607ca Add entry to CHANGELOG 2017-06-18 21:18:11 +02:00
Alexander Neumann
53f8026018 Merge pull request #1048 from restic/cleanup-fuse-mount
Cleanup/fix fuse mount
2017-06-18 18:41:02 +02:00
Alexander Neumann
de92ce7a88 Merge pull request #1049 from restic/fix-backend-tests-delayed-remove
backend tests: Add configurable delay for delayed remove
2017-06-18 18:31:38 +02:00
Alexander Neumann
eb8041b943 backend tests: Add configurable delay for delayed remove 2017-06-18 17:36:57 +02:00
Alexander Neumann
9c6e9bcf33 fuse: Add build tags for unsupported OS 2017-06-18 17:02:07 +02:00
Alexander Neumann
154816ffd0 fuse: Fix file test 2017-06-18 16:29:00 +02:00
Alexander Neumann
c86e425df6 fuse: Fix file inode 2017-06-18 16:28:55 +02:00
Alexander Neumann
3883c7a190 fuse: Fix blob length cache 2017-06-18 16:28:39 +02:00
Alexander Neumann
a66760d86d fuse: Fix inode handling 2017-06-18 15:11:32 +02:00
Alexander Neumann
52752659c1 fuse: Rewrite fuse implementation 2017-06-18 14:59:44 +02:00
Alexander Neumann
f676c0c41b index: Add Each() to MasterIndex 2017-06-18 14:52:14 +02:00
Alexander Neumann
f31e993f09 fuse: Reenable integration tests 2017-06-18 14:23:35 +02:00
Alexander Neumann
56f610e548 fuse: Remove struct SnapshotWithId 2017-06-18 14:11:33 +02:00
Alexander Neumann
052a6a0acc Move snapshot filter function to restic package 2017-06-18 13:18:12 +02:00
Alexander Neumann
77037e33c9 Move snapshot finding functions to new file 2017-06-18 13:06:52 +02:00
Alexander Neumann
5a34799554 Move Snapshots struct and policy to other files 2017-06-18 13:05:47 +02:00
Alexander Neumann
47282abfa4 fuse: Use Mutex instead of RWMutex 2017-06-17 23:00:38 +02:00
Alexander Neumann
c9cc724b31 s3: Split Create() from Open() 2017-06-17 22:15:58 +02:00
Alexander Neumann
0d3674245b Merge pull request #1043 from restic/fix-gcs
s3: Fix GCS
2017-06-17 10:35:10 +02:00
Alexander Neumann
82b21cdf4a Merge pull request #1027 from restic/s3-set-retry
s3: Allow setting the number of retries for minio-go
2017-06-17 10:34:36 +02:00
Alexander Neumann
c4592f577a Merge pull request #1036 from restic/prune-remove-invalid-files
prune: Remove invalid files
2017-06-16 22:52:44 +02:00
Alexander Neumann
3cd851e578 Update github.com/minio/minio-go 2017-06-16 22:29:40 +02:00
Alexander Neumann
e074833a7d Merge pull request #1045 from restic/prune-fix-progress
prune: Fix progress information
2017-06-16 20:21:55 +02:00
Alexander Neumann
c5f1a83cb4 prune: Fix progress information 2017-06-16 19:03:26 +02:00
Alexander Neumann
1baaa778ee Add entry to CHANGELOG 2017-06-16 12:27:44 +02:00
Alexander Neumann
6a948d5afd s3: Fix backend for Google Cloud Storage 2017-06-16 11:25:06 +02:00
Alexander Neumann
ea66ae0811 s3: Fix IsNotExist() 2017-06-16 10:54:46 +02:00
Alexander Neumann
bf8a155fb1 Update github.com/minio/minio-go 2017-06-16 10:53:38 +02:00
Alexander Neumann
4ae59bef96 prune: Remove invalid files
Closes #1029
2017-06-15 20:56:22 +02:00
Alexander Neumann
eadf5dcb2d Merge pull request #1038 from restic/s3-prevent-close
Improve GCS support
2017-06-15 20:54:52 +02:00
Alexander Neumann
91a24e8229 Merge pull request #1035 from restic/fix-1032
prune: Remove files as the last step
2017-06-15 20:22:42 +02:00
Alexander Neumann
e3c979a7a4 Merge pull request #1034 from restic/fix-1030
prune: Fix status string for narrow terminals
2017-06-15 20:22:33 +02:00
Alexander Neumann
05365706c0 backend/tests: Correct error message and delayed remove 2017-06-15 20:05:35 +02:00
Alexander Neumann
bbca31b661 test/s3: Retry connection to Minio server 2017-06-15 19:51:55 +02:00
Alexander Neumann
eb7fc12e01 backend tests: Delay listing for swift backend 2017-06-15 19:41:07 +02:00
Alexander Neumann
98ae7b1210 s3: Save config in backend 2017-06-15 16:41:09 +02:00
Alexander Neumann
51877cecf7 s3: Prevent closing of the reader for GCS 2017-06-15 16:39:42 +02:00
Alexander Neumann
9053b2000b s3: Delete ignores error if the object doesn't exist 2017-06-15 16:27:19 +02:00
Alexander Neumann
dd6ce5f9d8 Remove backend.Closer, use ioutil.NopCloser() instead 2017-06-15 15:58:23 +02:00
Alexander Neumann
9a8301fc74 prune: Fix status string for narrow terminals
Closes #1030
2017-06-15 15:41:07 +02:00
Alexander Neumann
aabe2a0a30 Merge pull request #1002 from restic/test-codecov
Remove codecov config file
2017-06-15 15:09:50 +02:00
Alexander Neumann
c79fb6fcdd prune: Delete repacked files as the very last step 2017-06-15 14:46:50 +02:00
Alexander Neumann
af9ba3be91 backend: Add IsNotExist 2017-06-15 13:40:27 +02:00
Alexander Neumann
6f24d038f8 prune: Only remove data after index has been uploaded
Closes #1032
2017-06-15 13:12:46 +02:00
Alexander Neumann
cf65893c4b s3: Allow setting the number of retries for minio-go
https://github.com/restic/restic/issues/1013#issuecomment-307883970
2017-06-12 21:09:37 +02:00
Alexander Neumann
bd7d5a429f Merge pull request #1025 from restic/fix-1013
s3: Switch back to high-level API for upload
2017-06-12 19:58:12 +02:00
Alexander Neumann
7b54f6e642 Add entry to CHANGELOG 2017-06-12 19:56:50 +02:00
Alexander Neumann
422c0dfb5e s3: Exit test loop for minio server on success 2017-06-11 20:49:56 +02:00
Alexander Neumann
73b296918b s3: Reorder debug messages
This way the semaphore token acquisition can be observed in the debug
log.
2017-06-11 20:49:53 +02:00
Alexander Neumann
907c201693 debug: Add version number to debug log 2017-06-11 20:48:46 +02:00
Alexander Neumann
58de8bf392 swift/rest: Reduce number of connections 2017-06-11 20:48:46 +02:00
Alexander Neumann
a89a7a783a s3: Correct comment on the connections option 2017-06-11 20:48:46 +02:00
Alexander Neumann
c422010597 s3: Fix test 2017-06-11 20:48:46 +02:00
Alexander Neumann
08e1d9ffad s3: Switch back to high-level API, limit connections 2017-06-11 20:48:42 +02:00
Alexander Neumann
a4e8dc3371 s3: Improve error message in debug log 2017-06-11 11:22:25 +02:00
Alexander Neumann
19da56a6ea debug: Add log before panic() 2017-06-11 11:22:25 +02:00
Alexander Neumann
d3c06c39f9 debug: Fix EOF detection in HTTP transport 2017-06-11 11:22:25 +02:00
Alexander Neumann
6301620428 s3: Add more debug logs 2017-06-11 11:22:25 +02:00
Alexander Neumann
a6f157f346 Merge pull request #1024 from restic/remove-unused
Remove unused code/variables
2017-06-11 11:18:02 +02:00
Alexander Neumann
8d4417ec92 Remove unused code/variables 2017-06-11 09:29:53 +02:00
Alexander Neumann
0b55be2581 prune: Fix debug log 2017-06-10 22:16:42 +02:00
Alexander Neumann
88a59fd0ca options: Handle uint 2017-06-10 21:07:10 +02:00
Alexander Neumann
539674614b Merge pull request #1019 from restic/fix-1017
ls: Print names with percent correctly
2017-06-10 12:43:46 +02:00
Alexander Neumann
9d1b9157d4 ls: Print names with percent correctly
Closes #1017
2017-06-10 12:17:21 +02:00
Alexander Neumann
5f449045d2 Merge pull request #1003 from fwilhe/contributing-md-link
Fix relative link to CONTRIBUTING.md
2017-06-09 20:56:21 +02:00
Alexander Neumann
3e4d236751 Merge pull request #1010 from restic/update-minio-go
Update github.com/minio/minio-go
2017-06-09 20:55:49 +02:00
Alexander Neumann
4fe6593fbe Merge pull request #1011 from restic/fix-1009
pack: Handle small files
2017-06-09 20:53:52 +02:00
Florian Wilhelm
635633379a Fix link to CONTRIBUTING.md 2017-06-09 00:36:31 +02:00
Alexander Neumann
48fecd791d pack: Handle more invalid header cases 2017-06-08 21:04:07 +02:00
Alexander Neumann
a325a20fb4 s3: Increase wait time for minio server 2017-06-08 20:50:56 +02:00
Alexander Neumann
1f0916b01b Merge pull request #1004 from restic/add-migrate-s3
Add 'migrate' command, change s3 layout
2017-06-08 20:48:27 +02:00
Alexander Neumann
eb767ab15f pack: Handle small files 2017-06-08 20:40:12 +02:00
Alexander Neumann
92c0aa3854 Merge pull request #998 from restic/fix-820
fuse: Add cache for blob sizes
2017-06-08 20:21:26 +02:00
Alexander Neumann
a61016cb55 Update github.com/minio/minio-go 2017-06-08 19:40:06 +02:00
Alexander Neumann
eb7ddd6e11 Add entry to CHANGELOG 2017-06-08 19:21:52 +02:00
Alexander Neumann
ff3d2e42f4 migrate: Be a bit more verbose 2017-06-08 19:19:45 +02:00
Alexander Neumann
1aab123b6c Merge pull request #1008 from chaquotay/patch-1
Fixing tiny typo
2017-06-08 19:04:14 +02:00
Stephan Müller
d11f8d294f Fixing tiny typo 2017-06-08 13:27:22 +02:00
Alexander Neumann
04ded881f6 s3: Change the default layout to "default" 2017-06-07 23:08:20 +02:00
Alexander Neumann
4f9bf5312b Add migrate
This commits adds a 'migrate' command and a migration to move s3
repositories from the 's3legacy' to the 'default' layout.
2017-06-07 23:08:02 +02:00
Alexander Neumann
7cf8f59987 layout: Add String() and Name() 2017-06-07 21:59:41 +02:00
Alexander Neumann
b8b5c8e8c9 s3: Rename struct to Backend 2017-06-07 21:59:01 +02:00
Alexander Neumann
a46baf7685 s3: Remove cache 2017-06-07 20:51:45 +02:00
Alexander Neumann
f2a51aa37c Add entry to CHANGELOG 2017-06-07 20:51:08 +02:00
Alexander Neumann
233eaf8ee9 fuse: Improve semantics of the blob size cache
Wrap it in a struct and add a Lookup() function to make clear that it
is only queried, not changed, so we don't have any race conditions.
2017-06-07 20:04:58 +02:00
Alexander Neumann
067be2c551 fuse: Add cache for blob sizes
Closes: #820
2017-06-07 20:04:15 +02:00
Alexander Neumann
550e1feaec Merge pull request #999 from restic/backend-use-semaphore
backends: Use new semaphore
2017-06-07 19:48:32 +02:00
Alexander Neumann
f90ce23f30 Merge pull request #994 from restic/add-context
Add context.Context to the backend
2017-06-07 19:11:56 +02:00
Alexander Neumann
29f8f8fe68 Update github.com/kurin/blazer
Reduces cost-intensive list_files API calls.
2017-06-07 19:10:05 +02:00
Alexander Neumann
48c1e7b00d Fix location tests 2017-06-06 21:12:38 +02:00
Alexander Neumann
2175ccedd2 Remove codecov config file 2017-06-06 21:02:19 +02:00
Alexander Neumann
d4e74f20aa Add context to dump command 2017-06-06 00:37:25 +02:00
Alexander Neumann
aa5bc39311 swift: Use semaphore 2017-06-06 00:33:25 +02:00
Alexander Neumann
46049b4236 rest: Use semaphore 2017-06-06 00:26:29 +02:00
Alexander Neumann
683ebef6c6 s3: Use semaphore 2017-06-06 00:17:39 +02:00
Alexander Neumann
5010e95c23 Add error handling to semaphore 2017-06-06 00:17:21 +02:00
Alexander Neumann
46b7a270a6 Add context parameters to tests 2017-06-05 23:56:59 +02:00
Alexander Neumann
cf497c2728 Add context to restic packages 2017-06-04 14:35:14 +02:00
Alexander Neumann
16fcd07110 Add a Context to the backend 2017-06-04 14:02:44 +02:00
Alexander Neumann
a9a2798910 Merge pull request #993 from restic/improve-search-performance
Improve find
2017-06-04 12:44:29 +02:00
Alexander Neumann
9cd664caa3 Add entry to CHANGELOG 2017-06-04 11:50:38 +02:00
Alexander Neumann
a90e0c6595 find: Check trees only once 2017-06-04 11:42:40 +02:00
Alexander Neumann
7b5efaf7b0 find: Move functions to struct 2017-06-04 11:38:46 +02:00
Alexander Neumann
3b7ca4ac35 find: Improve debug log 2017-06-04 11:22:56 +02:00
Alexander Neumann
40a61b82ce Merge pull request #978 from restic/add-backblaze-backend
Add Backblaze B2 backend
2017-06-03 14:54:04 +02:00
Alexander Neumann
028f43299a Merge pull request #975 from restic/add-swift-backend
Add swift backend
2017-06-03 14:52:47 +02:00
Alexander Neumann
3a4727f0f5 Add entry to CHANGELOG.md 2017-06-03 14:28:29 +02:00
Alexander Neumann
fec89f95fb Improve swift backend 2017-06-03 14:28:18 +02:00
Bartłomiej Święcki
5681d41f76 Implement OpenStack swift backend
This commit implements support for OpenStack swift
storage server, tested on OVH public cloud storage.

Special thanks to jayme-github <tuxnet@gmail.com>
who helped with the implementation.
2017-06-03 14:26:29 +02:00
Alexander Neumann
efd61d97ef Vendor github.com/ncw/swift 2017-06-03 14:25:57 +02:00
Alexander Neumann
3ed56f2192 Add entry to CHANGELOG.md 2017-06-03 14:24:59 +02:00
Alexander Neumann
122462b9b1 Add Backblaze B2 backend
This is based on prior work by Joe Turgeon <arithmetric@gmail.com>
@arithmetric.
2017-06-03 14:24:59 +02:00
Alexander Neumann
2217b9277e Vendor github.com/kurin/blazer 2017-06-03 14:24:59 +02:00
Alexander Neumann
b5e0e3631b Addd nev version section 2017-06-03 14:10:28 +02:00
Alexander Neumann
be68e43871 Fix link 2017-06-02 22:08:04 +02:00
Alexander Neumann
f6034c0882 Merge pull request #990 from tmcarr/fix_readme_links
Fix the links in the readme to render in RST
2017-06-02 21:59:41 +02:00
Travis Carr
f693781bf0 Fix the links in the readme to render right. 2017-06-02 12:32:13 -07:00
Alexander Neumann
3ae9be987f Add VERSION file for 0.6.1 2017-05-31 23:52:13 +02:00
Alexander Neumann
ec0975c388 Add VERSION file for 2017-05-31 23:51:02 +02:00
Alexander Neumann
c2ce484e93 Add version to CHANGELOG 2017-05-31 23:50:54 +02:00
Alexander Neumann
e5c7c314a7 Add section about reproducible build to README
In addition, the build script isn't needed any more.
2017-05-31 23:48:56 +02:00
Alexander Neumann
6d36dcd46e Merge pull request #987 from Thor77/minor-doc-fix
[docs] Fix paragraph not indented correctly in #Autocomplete
2017-05-31 23:23:27 +02:00
Thor77
96c9ecd20e Fix paragraph not indented correctly 2017-05-31 21:40:47 +02:00
Alexander Neumann
997be9a036 Remove PR 2017-05-31 21:34:18 +02:00
Alexander Neumann
31fd8e98b9 Add Entry to CHANGELOG 2017-05-31 21:33:45 +02:00
Alexander Neumann
aa0f874c8d s3: Simplify IsNotExist() 2017-05-31 21:23:01 +02:00
Alexander Neumann
5c59484d2b s3: Return only basename in List() 2017-05-31 21:22:55 +02:00
Alexander Neumann
fba6211c99 Merge pull request #986 from restic/fix-regression-985
Allow many idle connections per host
2017-05-31 20:49:50 +02:00
Alexander Neumann
a8386e7d71 Add entry to CHANGELOG 2017-05-31 19:53:54 +02:00
Alexander Neumann
04b262d8f1 Allow many idle connections per host
Closes #985
2017-05-31 19:39:19 +02:00
Alexander Neumann
4dbbc24a44 Update Go version 2017-05-30 23:05:13 +02:00
Alexander Neumann
725d50554a Merge pull request #981 from restic/reproducible-builds
build.go: Strip temporary path, allow reproducible builds
2017-05-29 23:49:49 +02:00
Alexander Neumann
ed91cafce2 Add entry to CHANGELOG 2017-05-29 23:46:48 +02:00
Alexander Neumann
de48a5ac9c build.go: Strip temporary path, allow reproducible builds 2017-05-29 23:27:25 +02:00
Alexander Neumann
1d167f4680 Merge tag 'v0.6.0'
v0.6.0
2017-05-29 21:35:27 +02:00
Alexander Neumann
efad7ee197 Add VERSION file for 0.6.0 2017-05-29 21:31:41 +02:00
Alexander Neumann
820c88ea73 Add VERSION file for 0.6.0 2017-05-29 21:12:52 +02:00
Alexander Neumann
e7f031c9b3 Merge pull request #976 from restic/backend-fixes
Misc fixes for the backend/test code
2017-05-28 13:30:56 +02:00
Alexander Neumann
f3f6924b61 backend/test: Loose requirement about early error 2017-05-28 13:06:27 +02:00
Alexander Neumann
c5244abad9 rest: Improve error messages 2017-05-28 12:33:47 +02:00
Alexander Neumann
1f5954e2c1 layout: Test DefaultLayout for empty path prefix 2017-05-28 12:33:47 +02:00
Alexander Neumann
e046a2a6da sftp: Use path instead of filepath 2017-05-28 12:33:47 +02:00
Alexander Neumann
8395b53400 backend/test: Reduce verbosity in logs 2017-05-28 12:33:47 +02:00
Alexander Neumann
24ec14738d backend/test: Skip offset == length test 2017-05-28 12:33:47 +02:00
Alexander Neumann
79477fdfe4 backend/test: Randomize test suite 2017-05-28 12:33:47 +02:00
Alexander Neumann
7ec0543af3 testing: Add id to error message in panic 2017-05-28 10:17:04 +02:00
Alexander Neumann
e73e3cb4ba Merge pull request #974 from restic/remove-noninteractive-progress
Remove regular status printing for non terminals
2017-05-25 18:56:55 +02:00
Alexander Neumann
317d9c4559 Add entry to the changelog 2017-05-25 17:06:06 +02:00
Alexander Neumann
5247de552a Remove regular status printing for non terminals 2017-05-25 17:03:48 +02:00
Alexander Neumann
37b107b90b build script: Check for dirty work directory 2017-05-25 15:50:37 +02:00
234 changed files with 20289 additions and 3266 deletions

View File

@@ -2,8 +2,8 @@ language: go
sudo: false
go:
- 1.7.5
- 1.8.1
- 1.7.6
- 1.8.3
- tip
os:
@@ -17,14 +17,14 @@ env:
matrix:
exclude:
- os: osx
go: 1.7.5
go: 1.7.6
- os: osx
go: tip
- os: linux
go: 1.8.1
go: 1.8.3
include:
- os: linux
go: 1.8.1
go: 1.8.3
sudo: true
env:
RESTIC_TEST_FUSE=1

View File

@@ -1,6 +1,87 @@
This file describes changes relevant to all users that are made in each
released version of restic from the perspective of the user.
Important Changes in 0.X.Y
==========================
* New "swift" backend: A new backend for the OpenStack Swift cloud storage
protocol has been added, https://wiki.openstack.org/wiki/Swift
https://github.com/restic/restic/pull/975
https://github.com/restic/restic/pull/648
* New "b2" backend: A new backend for Backblaze B2 cloud storage
service has been added, https://www.backblaze.com
https://github.com/restic/restic/issues/512
https://github.com/restic/restic/pull/978
* Improved performance for the `find` command: Restic recognizes paths it has
already checked for the files in question, so the number of backend requests
is reduced a lot.
https://github.com/restic/restic/issues/989
https://github.com/restic/restic/pull/993
* Improved performance for the fuse mount: Listing directories which contain
large files now is significantly faster.
https://github.com/restic/restic/pull/998
* The default layout for the s3 backend is now `default` (instead of
`s3legacy`). Also, there's a new `migrate` command to convert an existing
repo, it can be run like this: `restic migrate s3_layout`
https://github.com/restic/restic/issues/965
https://github.com/restic/restic/pull/1004
* The fuse mount now has two more directories: `tags` contains a subdir for
each tag, which in turn contains only the snapshots that have this tag. The
subdir `hosts` contains a subdir for each host that has a snapshot, and the
subdir contains the snapshots for that host.
https://github.com/restic/restic/issues/636
https://github.com/restic/restic/pull/1050
Small changes
-------------
* For the s3 backend we're back to using the high-level API the s3 client
library for uploading data, a few users reported dropped connections (which
the library will automatically retry now).
https://github.com/restic/restic/issues/1013
https://github.com/restic/restic/issues/1023
https://github.com/restic/restic/pull/1025
* The `prune` command has been improved and will now remove invalid pack
files, for example files that have not been uploaded completely because a
backup was interrupted.
https://github.com/restic/restic/issues/1029
https://github.com/restic/restic/pull/1036
* restic now tries to detect when an invalid/unknown backend is used and
returns an error message.
https://github.com/restic/restic/issues/1021
https://github.com/restic/restic/pull/1070
Important Changes in 0.6.1
==========================
This is mostly a bugfix release and only contains small changes:
* We've fixed a bug where `rebuild-index` would corrupt the index when used
with the s3 backend together with the `default` layout. This is not the
default setting.
* Backends based on HTTP now allow several idle connections in parallel. This
is especially important for the REST backend, which (when used with a local
server) may create a lot connections and exhaust available ports quickly.
https://github.com/restic/restic/issues/985
https://github.com/restic/restic/pull/986
* Regular status report: We've removed the status report that was printed
every 10 seconds when restic is run non-interactively. You can still force
reporting the current status by sending a `USR1` signal to the process.
https://github.com/restic/restic/pull/974
* The `build.go` now strips the temporary directory used for compilation from
the binary. This is the first step in enabling reproducible builds.
https://github.com/restic/restic/pull/981
Important Changes in 0.6.0
==========================

View File

@@ -18,7 +18,7 @@
FROM ubuntu:14.04
ARG GOVERSION=1.7.5
ARG GOVERSION=1.8.3
ARG GOARCH=amd64
# install dependencies

View File

@@ -68,6 +68,15 @@ following principles in mind:
data should be de-duplicated before it is actually written to the
storage back end to save precious backup space.
Reproducible Builds
-------------------
The binaries released with each restic version starting at 0.6.1 are
`reproducible <https://reproducible-builds.org/>`__, which means that you can
easily reproduce a byte identical version from the source code for that
release. Instructions on how to do that are contained in the
`builder repository <https://github.com/restic/builder>`__.
News
----

View File

@@ -1 +1 @@
0.6.0-rc.1
0.7.0

View File

@@ -195,8 +195,11 @@ func cleanEnv() (env []string) {
// build runs "go build args..." with GOPATH set to gopath.
func build(cwd, goos, goarch, gopath string, args ...string) error {
args = append([]string{"build"}, args...)
cmd := exec.Command("go", args...)
a := []string{"build"}
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", gopath))
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
a = append(a, args...)
cmd := exec.Command("go", a...)
cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos)
if !enableCGO {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")

View File

@@ -1,64 +0,0 @@
#!/bin/bash
set -e
if [[ -z "$VERSION" ]]; then
echo '$VERSION unset'
exit 1
fi
dir=$(mktemp -d --tmpdir restic-release-XXXXXX)
echo "path is ${dir}"
for R in \
darwin/386 \
darwin/amd64 \
freebsd/386 \
freebsd/amd64 \
freebsd/arm \
linux/386 \
linux/amd64 \
linux/arm \
linux/arm64 \
openbsd/386 \
openbsd/amd64 \
windows/386 \
windows/amd64 \
; do \
OS=$(dirname $R)
ARCH=$(basename $R)
filename=restic_${VERSION}_${OS}_${ARCH}
if [[ "$OS" == "windows" ]]; then
filename="${filename}.exe"
fi
echo $filename
go run build.go --goos $OS --goarch $ARCH --output ${filename}
if [[ "$OS" == "windows" ]]; then
zip ${filename%.exe}.zip ${filename}
rm ${filename}
mv ${filename%.exe}.zip ${dir}
else
bzip2 ${filename}
mv ${filename}.bz2 ${dir}
fi
done
echo "packing sources"
git archive --format=tar --prefix=restic-$VERSION/ v$VERSION | gzip -n > restic-$VERSION.tar.gz
mv restic-$VERSION.tar.gz ${dir}
echo "creating checksums"
pushd ${dir}
sha256sum restic_*.{zip,bz2} restic-$VERSION.tar.gz > SHA256SUMS
gpg --armor --detach-sign SHA256SUMS
popd
echo "creating source signature file"
gpg --armor --detach-sign ${dir}/restic-$VERSION.tar.gz
echo
echo "done, path is ${dir}"

View File

@@ -1,2 +0,0 @@
codecov:
disable_default_path_fixes: true

View File

@@ -9,7 +9,7 @@ new feature. This way, duplicate work is prevented and we can discuss
your ideas and design first.
More information and a description of the development environment can be
found in `CONTRIBUTING.md <CONTRIBUTING.md>`__.
found in `CONTRIBUTING.md <https://github.com/restic/restic/blob/master/CONTRIBUTING.md>`__.
A document describing the design of restic and the data structures stored on the
back end is contained in `Design <https://restic.readthedocs.io/en/latest/design.html>`__.

View File

@@ -282,6 +282,96 @@ this command.
Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is irrecoverably lost.
OpenStack Swift
~~~~~~~~~~~~~~~
Restic can backup data to an OpenStack Swift container. Because Swift supports
various authentication methods, credentials are passed through environment
variables. In order to help integration with existing OpenStack installations,
the naming convention of those variables follows official python swift client:
.. code-block:: console
# For keystone v1 authentication
$ export ST_AUTH=<MY_AUTH_URL>
$ export ST_USER=<MY_USER_NAME>
$ export ST_KEY=<MY_USER_PASSWORD>
# For keystone v2 authentication (some variables are optional)
$ export OS_AUTH_URL=<MY_AUTH_URL>
$ export OS_REGION_NAME=<MY_REGION_NAME>
$ export OS_USERNAME=<MY_USERNAME>
$ export OS_PASSWORD=<MY_PASSWORD>
$ export OS_TENANT_ID=<MY_TENANT_ID>
$ export OS_TENANT_NAME=<MY_TENANT_NAME>
# For keystone v3 authentication (some variables are optional)
$ export OS_AUTH_URL=<MY_AUTH_URL>
$ export OS_REGION_NAME=<MY_REGION_NAME>
$ export OS_USERNAME=<MY_USERNAME>
$ export OS_PASSWORD=<MY_PASSWORD>
$ export OS_USER_DOMAIN_NAME=<MY_DOMAIN_NAME>
$ export OS_PROJECT_NAME=<MY_PROJECT_NAME>
$ export OS_PROJECT_DOMAIN_NAME=<MY_PROJECT_DOMAIN_NAME>
# For authentication based on tokens
$ export OS_STORAGE_URL=<MY_STORAGE_URL>
$ export OS_AUTH_TOKEN=<MY_AUTH_TOKEN>
Restic should be compatible with [OpenStack RC
file](https://docs.openstack.org/user-guide/common/cli-set-environment-variables-using-openstack-rc.html)
in most cases.
Once environment variables are set up, a new repository can be created. The
name of swift container and optional path can be specified. If
the container does not exist, it will be created automatically:
.. code-block:: console
$ restic -r swift:container_name:/path init # path is optional
enter password for new backend:
enter password again:
created restic backend eefee03bbd at swift:container_name:/path
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
The policy of new container created by restic can be changed using environment variable:
.. code-block:: console
$ export SWIFT_DEFAULT_CONTAINER_POLICY=<MY_CONTAINER_POLICY>
Backblaze B2
~~~~~~~~~~~~
Restic can backup data to any Backblaze B2 bucket. You need to first setup the
following environment variables with the credentials you obtained when signed
into your B2 account:
.. code-block:: console
$ export B2_ACCOUNT_ID=<MY_ACCOUNT_ID>
$ export B2_ACCOUNT_KEY=<MY_SECRET_ACCOUNT_KEY>
You can then easily initialize a repository stored at Backblaze B2. If the
bucket does not exist yet, it will be created:
.. code-block:: console
$ restic -r b2:bucketname:path/to/repo init
enter password for new backend:
enter password again:
created restic backend eefee03bbd at b2:bucketname:path/to/repo
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
The number of concurrent connections to the B2 service can be set with the `-o
b2.connections=10`. By default, at most five parallel connections are
established.
Password prompt on Windows
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -624,7 +714,7 @@ command to serve the repository with FUSE:
$ mkdir /mnt/restic
$ restic -r /tmp/backup mount /mnt/restic
enter password for repository:
Now serving /tmp/backup at /tmp/restic
Now serving /tmp/backup at /mnt/restic
Don't forget to umount after quitting!
Mounting repositories via FUSE is not possible on Windows and OpenBSD.
@@ -820,8 +910,10 @@ Restic can write out a bash compatible autocompletion script:
NOTE: The current version supports Bash only.
This should work for *nix systems with Bash installed.
By default, the file is written directly to /etc/bash_completion.d
for convenience, and the command may need superuser rights, e.g.:
By default, the file is written directly to ``/etc/bash_completion.d/``
for convenience, and the command may need superuser rights, e.g.
.. code-block:: console
$ sudo restic autocomplete

View File

@@ -64,7 +64,7 @@ changes:
.. image:: images/aws_s3/05_bucket_create_review.png
:alt: Review Bucket Creation
The newly created ``restic-demo`` bucket will no appear on the list of S3
The newly created ``restic-demo`` bucket will now appear on the list of S3
buckets:
.. image:: images/aws_s3/06_buckets_list_after.png

View File

@@ -91,7 +91,7 @@ func (env *TravisEnvironment) Prepare() error {
"golang.org/x/tools/cmd/cover",
"github.com/pierrre/gotestcover",
"github.com/NebulousLabs/glyphcheck",
"github.com/restic/rest-server",
"github.com/restic/rest-server/cmd/rest-server",
}
for _, pkg := range pkgs {
@@ -164,6 +164,20 @@ func (env *TravisEnvironment) RunTests() error {
msg("S3 repository not available\n")
}
// if the test swift service is available, make sure that the test is not skipped
if os.Getenv("RESTIC_TEST_SWIFT") != "" {
ensureTests = append(ensureTests, "restic/backend/swift.TestBackendSwift")
} else {
msg("Swift service not available\n")
}
// if the test b2 repository is available, make sure that the test is not skipped
if os.Getenv("RESTIC_TEST_B2_REPOSITORY") != "" {
ensureTests = append(ensureTests, "restic/backend/b2.TestBackendB2")
} else {
msg("B2 repository not available\n")
}
env.env["RESTIC_TEST_DISALLOW_SKIP"] = strings.Join(ensureTests, ",")
if *runCrossCompile {

View File

@@ -2,6 +2,7 @@ package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
@@ -263,7 +264,7 @@ func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string)
return err
}
err = repo.LoadIndex()
err = repo.LoadIndex(context.TODO())
if err != nil {
return err
}
@@ -274,7 +275,7 @@ func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string)
Hostname: opts.Hostname,
}
_, id, err := r.Archive(opts.StdinFilename, os.Stdin, newArchiveStdinProgress(gopts))
_, id, err := r.Archive(context.TODO(), opts.StdinFilename, os.Stdin, newArchiveStdinProgress(gopts))
if err != nil {
return err
}
@@ -372,7 +373,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
return err
}
err = repo.LoadIndex()
err = repo.LoadIndex(context.TODO())
if err != nil {
return err
}
@@ -391,7 +392,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
// Find last snapshot to set it as parent, if not already set
if !opts.Force && parentSnapshotID == nil {
id, err := restic.FindLatestSnapshot(repo, target, opts.Tags, opts.Hostname)
id, err := restic.FindLatestSnapshot(context.TODO(), repo, target, opts.Tags, opts.Hostname)
if err == nil {
parentSnapshotID = &id
} else if err != restic.ErrNoSnapshotFound {
@@ -489,7 +490,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
Warnf("%s\rwarning for %s: %v\n", ClearLine(), dir, err)
}
_, id, err := arch.Snapshot(newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID)
_, id, err := arch.Snapshot(context.TODO(), newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID)
if err != nil {
return err
}

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
@@ -73,7 +74,7 @@ func runCat(gopts GlobalOptions, args []string) error {
fmt.Println(string(buf))
return nil
case "index":
buf, err := repo.LoadAndDecrypt(restic.IndexFile, id)
buf, err := repo.LoadAndDecrypt(context.TODO(), restic.IndexFile, id)
if err != nil {
return err
}
@@ -83,7 +84,7 @@ func runCat(gopts GlobalOptions, args []string) error {
case "snapshot":
sn := &restic.Snapshot{}
err = repo.LoadJSONUnpacked(restic.SnapshotFile, id, sn)
err = repo.LoadJSONUnpacked(context.TODO(), restic.SnapshotFile, id, sn)
if err != nil {
return err
}
@@ -98,7 +99,7 @@ func runCat(gopts GlobalOptions, args []string) error {
return nil
case "key":
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
buf, err := backend.LoadAll(repo.Backend(), h)
buf, err := backend.LoadAll(context.TODO(), repo.Backend(), h)
if err != nil {
return err
}
@@ -125,7 +126,7 @@ func runCat(gopts GlobalOptions, args []string) error {
fmt.Println(string(buf))
return nil
case "lock":
lock, err := restic.LoadLock(repo, id)
lock, err := restic.LoadLock(context.TODO(), repo, id)
if err != nil {
return err
}
@@ -141,7 +142,7 @@ func runCat(gopts GlobalOptions, args []string) error {
}
// load index, handle all the other types
err = repo.LoadIndex()
err = repo.LoadIndex(context.TODO())
if err != nil {
return err
}
@@ -149,7 +150,7 @@ func runCat(gopts GlobalOptions, args []string) error {
switch tpe {
case "pack":
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
buf, err := backend.LoadAll(repo.Backend(), h)
buf, err := backend.LoadAll(context.TODO(), repo.Backend(), h)
if err != nil {
return err
}
@@ -171,7 +172,7 @@ func runCat(gopts GlobalOptions, args []string) error {
blob := list[0]
buf := make([]byte, blob.Length)
n, err := repo.LoadBlob(t, id, buf)
n, err := repo.LoadBlob(context.TODO(), t, id, buf)
if err != nil {
return err
}

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"os"
"time"
@@ -92,7 +93,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
chkr := checker.New(repo)
Verbosef("Load indexes\n")
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
dupFound := false
for _, hint := range hints {
@@ -113,14 +114,11 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
return errors.Fatal("LoadIndex returned errors")
}
done := make(chan struct{})
defer close(done)
errorsFound := false
errChan := make(chan error)
Verbosef("Check all packs\n")
go chkr.Packs(errChan, done)
go chkr.Packs(context.TODO(), errChan)
for err := range errChan {
errorsFound = true
@@ -129,7 +127,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
Verbosef("Check snapshots, trees and blobs\n")
errChan = make(chan error)
go chkr.Structure(errChan, done)
go chkr.Structure(context.TODO(), errChan)
for err := range errChan {
errorsFound = true
@@ -156,7 +154,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
p := newReadProgress(gopts, restic.Stat{Blobs: chkr.CountPacks()})
errChan := make(chan error)
go chkr.ReadData(p, errChan, done)
go chkr.ReadData(context.TODO(), p, errChan)
for err := range errChan {
errorsFound = true

View File

@@ -1,8 +1,9 @@
// +build debug
// xbuild debug
package main
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -44,11 +45,8 @@ func prettyPrintJSON(wr io.Writer, item interface{}) error {
}
func debugPrintSnapshots(repo *repository.Repository, wr io.Writer) error {
done := make(chan struct{})
defer close(done)
for id := range repo.List(restic.SnapshotFile, done) {
snapshot, err := restic.LoadSnapshot(repo, id)
for id := range repo.List(context.TODO(), restic.SnapshotFile) {
snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id)
if err != nil {
fmt.Fprintf(os.Stderr, "LoadSnapshot(%v): %v", id.Str(), err)
continue
@@ -83,15 +81,12 @@ type Blob struct {
}
func printPacks(repo *repository.Repository, wr io.Writer) error {
done := make(chan struct{})
defer close(done)
f := func(job worker.Job, done <-chan struct{}) (interface{}, error) {
f := func(ctx context.Context, job worker.Job) (interface{}, error) {
name := job.Data.(string)
h := restic.Handle{Type: restic.DataFile, Name: name}
blobInfo, err := repo.Backend().Stat(h)
blobInfo, err := repo.Backend().Stat(ctx, h)
if err != nil {
return nil, err
}
@@ -106,10 +101,10 @@ func printPacks(repo *repository.Repository, wr io.Writer) error {
jobCh := make(chan worker.Job)
resCh := make(chan worker.Job)
wp := worker.New(dumpPackWorkers, f, jobCh, resCh)
wp := worker.New(context.TODO(), dumpPackWorkers, f, jobCh, resCh)
go func() {
for name := range repo.Backend().List(restic.DataFile, done) {
for name := range repo.Backend().List(context.TODO(), restic.DataFile) {
jobCh <- worker.Job{Data: name}
}
close(jobCh)
@@ -146,13 +141,10 @@ func printPacks(repo *repository.Repository, wr io.Writer) error {
}
func dumpIndexes(repo restic.Repository) error {
done := make(chan struct{})
defer close(done)
for id := range repo.List(restic.IndexFile, done) {
for id := range repo.List(context.TODO(), restic.IndexFile) {
fmt.Printf("index_id: %v\n", id)
idx, err := repository.LoadIndex(repo, id)
idx, err := repository.LoadIndex(context.TODO(), repo, id)
if err != nil {
return err
}
@@ -184,7 +176,7 @@ func runDump(gopts GlobalOptions, args []string) error {
}
}
err = repo.LoadIndex()
err = repo.LoadIndex(context.TODO())
if err != nil {
return err
}

View File

@@ -12,7 +12,6 @@ import (
"restic"
"restic/debug"
"restic/errors"
"restic/repository"
)
var cmdFind = &cobra.Command{
@@ -172,59 +171,76 @@ func (s *statefulOutput) Finish() {
}
}
func findInTree(repo *repository.Repository, pat *findPattern, id restic.ID, prefix string, state *statefulOutput) error {
debug.Log("checking tree %v\n", id)
// Finder bundles information needed to find a file or directory.
type Finder struct {
repo restic.Repository
pat findPattern
out statefulOutput
notfound restic.IDSet
}
tree, err := repo.LoadTree(id)
func (f *Finder) findInTree(treeID restic.ID, prefix string) error {
if f.notfound.Has(treeID) {
debug.Log("%v skipping tree %v, has already been checked", prefix, treeID.Str())
return nil
}
debug.Log("%v checking tree %v\n", prefix, treeID.Str())
tree, err := f.repo.LoadTree(context.TODO(), treeID)
if err != nil {
return err
}
var found bool
for _, node := range tree.Nodes {
debug.Log(" testing entry %q\n", node.Name)
name := node.Name
if pat.ignoreCase {
if f.pat.ignoreCase {
name = strings.ToLower(name)
}
m, err := filepath.Match(pat.pattern, name)
m, err := filepath.Match(f.pat.pattern, name)
if err != nil {
return err
}
if m {
debug.Log(" pattern matches\n")
if !pat.oldest.IsZero() && node.ModTime.Before(pat.oldest) {
debug.Log(" ModTime is older than %s\n", pat.oldest)
if !f.pat.oldest.IsZero() && node.ModTime.Before(f.pat.oldest) {
debug.Log(" ModTime is older than %s\n", f.pat.oldest)
continue
}
if !pat.newest.IsZero() && node.ModTime.After(pat.newest) {
debug.Log(" ModTime is newer than %s\n", pat.newest)
if !f.pat.newest.IsZero() && node.ModTime.After(f.pat.newest) {
debug.Log(" ModTime is newer than %s\n", f.pat.newest)
continue
}
state.Print(prefix, node)
} else {
debug.Log(" pattern does not match\n")
debug.Log(" found match\n")
found = true
f.out.Print(prefix, node)
}
if node.Type == "dir" {
if err := findInTree(repo, pat, *node.Subtree, filepath.Join(prefix, node.Name), state); err != nil {
if err := f.findInTree(*node.Subtree, filepath.Join(prefix, node.Name)); err != nil {
return err
}
}
}
if !found {
f.notfound.Insert(treeID)
}
return nil
}
func findInSnapshot(repo *repository.Repository, sn *restic.Snapshot, pat findPattern, state *statefulOutput) error {
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), pat.oldest, pat.newest)
func (f *Finder) findInSnapshot(sn *restic.Snapshot) error {
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), f.pat.oldest, f.pat.newest)
state.newsn = sn
if err := findInTree(repo, &pat, *sn.Tree, string(filepath.Separator), state); err != nil {
f.out.newsn = sn
if err := f.findInTree(*sn.Tree, string(filepath.Separator)); err != nil {
return err
}
return nil
@@ -267,19 +283,25 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
}
}
if err = repo.LoadIndex(); err != nil {
if err = repo.LoadIndex(context.TODO()); err != nil {
return err
}
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
state := statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON}
f := &Finder{
repo: repo,
pat: pat,
out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON},
notfound: restic.NewIDSet(),
}
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
if err = findInSnapshot(repo, sn, pat, &state); err != nil {
if err = f.findInSnapshot(sn); err != nil {
return err
}
}
state.Finish()
f.out.Finish()
return nil
}

View File

@@ -97,7 +97,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
// When explicit snapshots args are given, remove them immediately.
if !opts.DryRun {
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
if err = repo.Backend().Remove(h); err != nil {
if err = repo.Backend().Remove(context.TODO(), h); err != nil {
return err
}
Verbosef("removed snapshot %v\n", sn.ID().Str())
@@ -167,7 +167,7 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
if !opts.DryRun {
for _, sn := range remove {
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
err = repo.Backend().Remove(h)
err = repo.Backend().Remove(context.TODO(), h)
if err != nil {
return err
}

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"restic/errors"
"restic/repository"
@@ -43,7 +44,7 @@ func runInit(gopts GlobalOptions, args []string) error {
s := repository.New(be)
err = s.Init(gopts.password)
err = s.Init(context.TODO(), gopts.password)
if err != nil {
return errors.Fatalf("create key in backend at %s failed: %v\n", gopts.Repo, err)
}

View File

@@ -30,8 +30,8 @@ func listKeys(ctx context.Context, s *repository.Repository) error {
tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created")
tab.RowFormat = "%s%-10s %-10s %-10s %s"
for id := range s.List(restic.KeyFile, ctx.Done()) {
k, err := repository.LoadKey(s, id.String())
for id := range s.List(ctx, restic.KeyFile) {
k, err := repository.LoadKey(ctx, s, id.String())
if err != nil {
Warnf("LoadKey() failed: %v\n", err)
continue
@@ -69,7 +69,7 @@ func addKey(gopts GlobalOptions, repo *repository.Repository) error {
return err
}
id, err := repository.AddKey(repo, pw, repo.Key())
id, err := repository.AddKey(context.TODO(), repo, pw, repo.Key())
if err != nil {
return errors.Fatalf("creating new key failed: %v\n", err)
}
@@ -85,7 +85,7 @@ func deleteKey(repo *repository.Repository, name string) error {
}
h := restic.Handle{Type: restic.KeyFile, Name: name}
err := repo.Backend().Remove(h)
err := repo.Backend().Remove(context.TODO(), h)
if err != nil {
return err
}
@@ -100,13 +100,13 @@ func changePassword(gopts GlobalOptions, repo *repository.Repository) error {
return err
}
id, err := repository.AddKey(repo, pw, repo.Key())
id, err := repository.AddKey(context.TODO(), repo, pw, repo.Key())
if err != nil {
return errors.Fatalf("creating new key failed: %v\n", err)
}
h := restic.Handle{Type: restic.KeyFile, Name: repo.KeyName()}
err = repo.Backend().Remove(h)
err = repo.Backend().Remove(context.TODO(), h)
if err != nil {
return err
}

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"restic"
"restic/errors"
@@ -55,7 +56,7 @@ func runList(opts GlobalOptions, args []string) error {
case "locks":
t = restic.LockFile
case "blobs":
idx, err := index.Load(repo, nil)
idx, err := index.Load(context.TODO(), repo, nil)
if err != nil {
return err
}
@@ -71,7 +72,7 @@ func runList(opts GlobalOptions, args []string) error {
return errors.Fatal("invalid type")
}
for id := range repo.List(t, nil) {
for id := range repo.List(context.TODO(), t) {
Printf("%s\n", id)
}

View File

@@ -46,13 +46,13 @@ func init() {
}
func printTree(repo *repository.Repository, id *restic.ID, prefix string) error {
tree, err := repo.LoadTree(*id)
tree, err := repo.LoadTree(context.TODO(), *id)
if err != nil {
return err
}
for _, entry := range tree.Nodes {
Printf(formatNode(prefix, entry, lsOptions.ListLong) + "\n")
Printf("%s\n", formatNode(prefix, entry, lsOptions.ListLong))
if entry.Type == "dir" && entry.Subtree != nil {
if err = printTree(repo, entry.Subtree, filepath.Join(prefix, entry.Name)); err != nil {
@@ -74,7 +74,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
return err
}
if err = repo.LoadIndex(); err != nil {
if err = repo.LoadIndex(context.TODO()); err != nil {
return err
}

View File

@@ -0,0 +1,100 @@
package main
import (
"restic"
"restic/migrations"
"github.com/spf13/cobra"
)
var cmdMigrate = &cobra.Command{
Use: "migrate [name]",
Short: "apply migrations",
Long: `
The "migrate" command applies migrations to a repository. When no migration
name is explicitely given, a list of migrations that can be applied is printed.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runMigrate(migrateOptions, globalOptions, args)
},
}
// MigrateOptions bundles all options for the 'check' command.
type MigrateOptions struct {
}
var migrateOptions MigrateOptions
func init() {
cmdRoot.AddCommand(cmdMigrate)
}
func checkMigrations(opts MigrateOptions, gopts GlobalOptions, repo restic.Repository) error {
ctx := gopts.ctx
Printf("available migrations:\n")
for _, m := range migrations.All {
ok, err := m.Check(ctx, repo)
if err != nil {
return err
}
if ok {
Printf(" %v: %v\n", m.Name(), m.Desc())
}
}
return nil
}
func applyMigrations(opts MigrateOptions, gopts GlobalOptions, repo restic.Repository, args []string) error {
ctx := gopts.ctx
var firsterr error
for _, name := range args {
for _, m := range migrations.All {
if m.Name() == name {
ok, err := m.Check(ctx, repo)
if err != nil {
return err
}
if !ok {
Warnf("migration %v cannot be applied: check failed\n", m.Name())
continue
}
Printf("applying migration %v...\n", m.Name())
if err = m.Apply(ctx, repo); err != nil {
Warnf("migration %v failed: %v\n", m.Name(), err)
if firsterr == nil {
firsterr = err
}
continue
}
Printf("migration %v: success\n", m.Name())
}
}
}
return firsterr
}
func runMigrate(opts MigrateOptions, gopts GlobalOptions, args []string) error {
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
if len(args) == 0 {
return checkMigrations(opts, gopts, repo)
}
return applyMigrations(opts, gopts, repo, args)
}

View File

@@ -4,6 +4,7 @@
package main
import (
"context"
"os"
"github.com/spf13/cobra"
@@ -64,7 +65,7 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
return err
}
err = repo.LoadIndex()
err = repo.LoadIndex(context.TODO())
if err != nil {
return err
}
@@ -95,14 +96,26 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
return err
}
systemFuse.Debug = func(msg interface{}) {
debug.Log("fuse: %v", msg)
}
cfg := fuse.Config{
OwnerIsRoot: opts.OwnerRoot,
Host: opts.Host,
Tags: opts.Tags,
Paths: opts.Paths,
}
root, err := fuse.NewRoot(context.TODO(), repo, cfg)
if err != nil {
return err
}
Printf("Now serving the repository at %s\n", mountpoint)
Printf("Don't forget to umount after quitting!\n")
root := fs.Tree{}
root.Add("snapshots", fuse.NewSnapshotsDir(repo, opts.OwnerRoot, opts.Paths, opts.Tags, opts.Host))
debug.Log("serving mount at %v", mountpoint)
err = fs.Serve(c, &root)
err = fs.Serve(c, root)
if err != nil {
return err
}

View File

@@ -1,7 +1,6 @@
package main
import (
"context"
"fmt"
"restic"
"restic/debug"
@@ -29,6 +28,18 @@ func init() {
cmdRoot.AddCommand(cmdPrune)
}
func shortenStatus(maxLength int, s string) string {
if len(s) <= maxLength {
return s
}
if maxLength < 3 {
return s[:maxLength]
}
return s[:maxLength-3] + "..."
}
// newProgressMax returns a progress that counts blobs.
func newProgressMax(show bool, max uint64, description string) *restic.Progress {
if !show {
@@ -44,10 +55,7 @@ func newProgressMax(show bool, max uint64, description string) *restic.Progress
s.Blobs, max, description)
if w := stdoutTerminalWidth(); w > 0 {
if len(status) > w {
max := w - len(status) - 4
status = status[:max] + "... "
}
status = shortenStatus(w, status)
}
PrintProgress("%s", status)
@@ -76,14 +84,13 @@ func runPrune(gopts GlobalOptions) error {
}
func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
err := repo.LoadIndex()
ctx := gopts.ctx
err := repo.LoadIndex(ctx)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
var stats struct {
blobs int
packs int
@@ -92,18 +99,22 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
}
Verbosef("counting files in repo\n")
for range repo.List(restic.DataFile, ctx.Done()) {
for range repo.List(ctx, restic.DataFile) {
stats.packs++
}
Verbosef("building new index for repo\n")
bar := newProgressMax(!gopts.Quiet, uint64(stats.packs), "packs")
idx, err := index.New(repo, bar)
idx, invalidFiles, err := index.New(ctx, repo, restic.NewIDSet(), bar)
if err != nil {
return err
}
for _, id := range invalidFiles {
Warnf("incomplete pack file (will be removed): %v\n", id)
}
blobs := 0
for _, pack := range idx.Packs {
stats.bytes += pack.Size
@@ -135,7 +146,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
Verbosef("load all snapshots\n")
// find referenced blobs
snapshots, err := restic.LoadAllSnapshots(repo)
snapshots, err := restic.LoadAllSnapshots(ctx, repo)
if err != nil {
return err
}
@@ -152,12 +163,16 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
for _, sn := range snapshots {
debug.Log("process snapshot %v", sn.ID().Str())
err = restic.FindUsedBlobs(repo, *sn.Tree, usedBlobs, seenBlobs)
err = restic.FindUsedBlobs(ctx, repo, *sn.Tree, usedBlobs, seenBlobs)
if err != nil {
if repo.Backend().IsNotExist(err) {
return errors.Fatal("unable to load a tree from the repo: " + err.Error())
}
return err
}
debug.Log("found %v blobs for snapshot %v", sn.ID().Str())
debug.Log("processed snapshot %v", sn.ID().Str())
bar.Report(restic.Stat{Blobs: 1})
}
bar.Done()
@@ -185,6 +200,12 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
// find packs that are unneeded
removePacks := restic.NewIDSet()
Verbosef("will remove %d invalid files\n", len(invalidFiles))
for _, id := range invalidFiles {
removePacks.Insert(id)
}
for packID, p := range idx.Packs {
hasActiveBlob := false
@@ -214,22 +235,28 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
Verbosef("will delete %d packs and rewrite %d packs, this frees %s\n",
len(removePacks), len(rewritePacks), formatBytes(uint64(removeBytes)))
var repackedBlobs restic.IDSet
if len(rewritePacks) != 0 {
bar = newProgressMax(!gopts.Quiet, uint64(len(rewritePacks)), "packs rewritten")
bar.Start()
err = repository.Repack(repo, rewritePacks, usedBlobs, bar)
repackedBlobs, err = repository.Repack(ctx, repo, rewritePacks, usedBlobs, bar)
if err != nil {
return err
}
bar.Done()
}
if err = rebuildIndex(ctx, repo, removePacks); err != nil {
return err
}
removePacks.Merge(repackedBlobs)
if len(removePacks) != 0 {
bar = newProgressMax(!gopts.Quiet, uint64(len(removePacks)), "packs deleted")
bar.Start()
for packID := range removePacks {
h := restic.Handle{Type: restic.DataFile, Name: packID.String()}
err = repo.Backend().Remove(h)
err = repo.Backend().Remove(ctx, h)
if err != nil {
Warnf("unable to remove file %v from the repository\n", packID.Str())
}
@@ -238,10 +265,6 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
bar.Done()
}
if err = rebuildIndex(ctx, repo); err != nil {
return err
}
Verbosef("done\n")
return nil
}

View File

@@ -38,19 +38,19 @@ func runRebuildIndex(gopts GlobalOptions) error {
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
return rebuildIndex(ctx, repo)
return rebuildIndex(ctx, repo, restic.NewIDSet())
}
func rebuildIndex(ctx context.Context, repo restic.Repository) error {
func rebuildIndex(ctx context.Context, repo restic.Repository, ignorePacks restic.IDSet) error {
Verbosef("counting files in repo\n")
var packs uint64
for range repo.List(restic.DataFile, ctx.Done()) {
for range repo.List(ctx, restic.DataFile) {
packs++
}
bar := newProgressMax(!globalOptions.Quiet, packs, "packs")
idx, err := index.New(repo, bar)
bar := newProgressMax(!globalOptions.Quiet, packs-uint64(len(ignorePacks)), "packs")
idx, _, err := index.New(ctx, repo, ignorePacks, bar)
if err != nil {
return err
}
@@ -58,11 +58,11 @@ func rebuildIndex(ctx context.Context, repo restic.Repository) error {
Verbosef("finding old index files\n")
var supersedes restic.IDs
for id := range repo.List(restic.IndexFile, ctx.Done()) {
for id := range repo.List(ctx, restic.IndexFile) {
supersedes = append(supersedes, id)
}
id, err := idx.Save(repo, supersedes)
id, err := idx.Save(ctx, repo, supersedes)
if err != nil {
return err
}
@@ -72,7 +72,7 @@ func rebuildIndex(ctx context.Context, repo restic.Repository) error {
Verbosef("remove %d old index files\n", len(supersedes))
for _, id := range supersedes {
if err := repo.Backend().Remove(restic.Handle{
if err := repo.Backend().Remove(ctx, restic.Handle{
Type: restic.IndexFile,
Name: id.String(),
}); err != nil {

View File

@@ -50,6 +50,8 @@ func init() {
}
func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
ctx := gopts.ctx
if len(args) != 1 {
return errors.Fatal("no snapshot ID specified")
}
@@ -79,7 +81,7 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
}
}
err = repo.LoadIndex()
err = repo.LoadIndex(ctx)
if err != nil {
return err
}
@@ -87,7 +89,7 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
var id restic.ID
if snapshotIDString == "latest" {
id, err = restic.FindLatestSnapshot(repo, opts.Paths, opts.Tags, opts.Host)
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host)
if err != nil {
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
}
@@ -136,7 +138,7 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
err = res.RestoreTo(opts.Target)
err = res.RestoreTo(ctx, opts.Target)
if totalErrors > 0 {
Printf("There were %d errors\n", totalErrors)
}

View File

@@ -76,7 +76,7 @@ func changeTags(repo *repository.Repository, sn *restic.Snapshot, setTags, addTa
}
// Save the new snapshot.
id, err := repo.SaveJSONUnpacked(restic.SnapshotFile, sn)
id, err := repo.SaveJSONUnpacked(context.TODO(), restic.SnapshotFile, sn)
if err != nil {
return false, err
}
@@ -89,7 +89,7 @@ func changeTags(repo *repository.Repository, sn *restic.Snapshot, setTags, addTa
// Remove the old snapshot.
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
if err = repo.Backend().Remove(h); err != nil {
if err = repo.Backend().Remove(context.TODO(), h); err != nil {
return false, err
}

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"restic"
"github.com/spf13/cobra"
@@ -41,7 +42,7 @@ func runUnlock(opts UnlockOptions, gopts GlobalOptions) error {
fn = restic.RemoveAllLocks
}
err = fn(repo)
err = fn(context.TODO(), repo)
if err != nil {
return err
}

View File

@@ -22,7 +22,7 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
// Process all snapshot IDs given as arguments.
for _, s := range snapshotIDs {
if s == "latest" {
id, err = restic.FindLatestSnapshot(repo, paths, tags, host)
id, err = restic.FindLatestSnapshot(ctx, repo, paths, tags, host)
if err != nil {
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Host:%v)\n", s, paths, tags, host)
usedFilter = true
@@ -44,7 +44,7 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
}
for _, id := range ids.Uniq() {
sn, err := restic.LoadSnapshot(repo, id)
sn, err := restic.LoadSnapshot(ctx, repo, id)
if err != nil {
Warnf("Ignoring %q, could not load snapshot: %v\n", id, err)
continue
@@ -58,15 +58,7 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
return
}
for id := range repo.List(restic.SnapshotFile, ctx.Done()) {
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
Warnf("Ignoring %q, could not load snapshot: %v\n", id, err)
continue
}
if (host != "" && host != sn.Hostname) || !sn.HasTags(tags) || !sn.HasPaths(paths) {
continue
}
for _, sn := range restic.FindFilteredSnapshots(ctx, repo, host, tags, paths) {
select {
case <-ctx.Done():
return

View File

@@ -8,10 +8,6 @@ import (
// TestFlags checks for double defined flags, the commands will panic on
// ParseFlags() when a shorthand flag is defined twice.
func TestFlags(t *testing.T) {
type FlagParser interface {
ParseFlags([]string) error
}
for _, cmd := range cmdRoot.Commands() {
t.Run(cmd.Name(), func(t *testing.T) {
cmd.Flags().SetOutput(ioutil.Discard)

View File

@@ -11,11 +11,13 @@ import (
"strings"
"syscall"
"restic/backend/b2"
"restic/backend/local"
"restic/backend/location"
"restic/backend/rest"
"restic/backend/s3"
"restic/backend/sftp"
"restic/backend/swift"
"restic/debug"
"restic/options"
"restic/repository"
@@ -308,7 +310,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
}
}
err = s.SearchKey(opts.password, maxKeys)
err = s.SearchKey(context.TODO(), opts.password, maxKeys)
if err != nil {
return nil, errors.Fatalf("unable to open repo: %v", err)
}
@@ -356,6 +358,37 @@ func parseConfig(loc location.Location, opts options.Options) (interface{}, erro
debug.Log("opening s3 repository at %#v", cfg)
return cfg, nil
case "swift":
cfg := loc.Config.(swift.Config)
if err := swift.ApplyEnvironment("", &cfg); err != nil {
return nil, err
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening swift repository at %#v", cfg)
return cfg, nil
case "b2":
cfg := loc.Config.(b2.Config)
if cfg.AccountID == "" {
cfg.AccountID = os.Getenv("B2_ACCOUNT_ID")
}
if cfg.Key == "" {
cfg.Key = os.Getenv("B2_ACCOUNT_KEY")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening b2 repository at %#v", cfg)
return cfg, nil
case "rest":
cfg := loc.Config.(rest.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
@@ -391,6 +424,10 @@ func open(s string, opts options.Options) (restic.Backend, error) {
be, err = sftp.Open(cfg.(sftp.Config))
case "s3":
be, err = s3.Open(cfg.(s3.Config))
case "swift":
be, err = swift.Open(cfg.(swift.Config))
case "b2":
be, err = b2.Open(cfg.(b2.Config))
case "rest":
be, err = rest.Open(cfg.(rest.Config))
@@ -403,7 +440,7 @@ func open(s string, opts options.Options) (restic.Backend, error) {
}
// check if config is there
fi, err := be.Stat(restic.Handle{Type: restic.ConfigFile})
fi, err := be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil {
return nil, errors.Fatalf("unable to open config file: %v\nIs there a repository at the following location?\n%v", err, s)
}
@@ -434,7 +471,11 @@ func create(s string, opts options.Options) (restic.Backend, error) {
case "sftp":
return sftp.Create(cfg.(sftp.Config))
case "s3":
return s3.Open(cfg.(s3.Config))
return s3.Create(cfg.(s3.Config))
case "swift":
return swift.Open(cfg.(swift.Config))
case "b2":
return b2.Create(cfg.(b2.Config))
case "rest":
return rest.Create(cfg.(rest.Config))
}

View File

@@ -1,10 +1,10 @@
// +build ignore
// +build !openbsd
// +build !windows
package main
import (
"context"
"fmt"
"io/ioutil"
"os"
@@ -55,17 +55,15 @@ func waitForMount(t testing.TB, dir string) {
t.Errorf("subdir %q of dir %s never appeared", mountTestSubdir, dir)
}
func mount(t testing.TB, global GlobalOptions, dir string) {
cmd := &CmdMount{global: &global}
OK(t, cmd.Mount(dir))
func testRunMount(t testing.TB, gopts GlobalOptions, dir string) {
opts := MountOptions{}
OK(t, runMount(opts, gopts, []string{dir}))
}
func umount(t testing.TB, global GlobalOptions, dir string) {
cmd := &CmdMount{global: &global}
func testRunUmount(t testing.TB, gopts GlobalOptions, dir string) {
var err error
for i := 0; i < mountWait; i++ {
if err = cmd.Umount(dir); err == nil {
if err = umount(dir); err == nil {
t.Logf("directory %v umounted", dir)
return
}
@@ -87,9 +85,10 @@ func listSnapshots(t testing.TB, dir string) []string {
func checkSnapshots(t testing.TB, global GlobalOptions, repo *repository.Repository, mountpoint, repodir string, snapshotIDs restic.IDs) {
t.Logf("checking for %d snapshots: %v", len(snapshotIDs), snapshotIDs)
go mount(t, global, mountpoint)
go testRunMount(t, global, mountpoint)
waitForMount(t, mountpoint)
defer umount(t, global, mountpoint)
defer testRunUmount(t, global, mountpoint)
if !snapshotsDirExists(t, mountpoint) {
t.Fatal(`virtual directory "snapshots" doesn't exist`)
@@ -110,7 +109,7 @@ func checkSnapshots(t testing.TB, global GlobalOptions, repo *repository.Reposit
}
for _, id := range snapshotIDs {
snapshot, err := restic.LoadSnapshot(repo, id)
snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id)
OK(t, err)
ts := snapshot.Time.Format(time.RFC3339)
@@ -144,45 +143,46 @@ func TestMount(t *testing.T) {
t.Skip("Skipping fuse tests")
}
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) {
cmdInit(t, global)
repo, err := global.OpenRepository()
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
mountpoint, err := ioutil.TempDir(TestTempDir, "restic-test-mount-")
OK(t, err)
mountpoint, err := ioutil.TempDir(TestTempDir, "restic-test-mount-")
testRunInit(t, gopts)
repo, err := OpenRepository(gopts)
OK(t, err)
// We remove the mountpoint now to check that cmdMount creates it
RemoveAll(t, mountpoint)
checkSnapshots(t, global, repo, mountpoint, env.repo, []restic.ID{})
checkSnapshots(t, gopts, repo, mountpoint, env.repo, []restic.ID{})
SetupTarTestFixture(t, env.testdata, filepath.Join("testdata", "backup-data.tar.gz"))
// first backup
cmdBackup(t, global, []string{env.testdata}, nil)
snapshotIDs := cmdList(t, global, "snapshots")
testRunBackup(t, []string{env.testdata}, BackupOptions{}, gopts)
snapshotIDs := testRunList(t, "snapshots", gopts)
Assert(t, len(snapshotIDs) == 1,
"expected one snapshot, got %v", snapshotIDs)
checkSnapshots(t, global, repo, mountpoint, env.repo, snapshotIDs)
checkSnapshots(t, gopts, repo, mountpoint, env.repo, snapshotIDs)
// second backup, implicit incremental
cmdBackup(t, global, []string{env.testdata}, nil)
snapshotIDs = cmdList(t, global, "snapshots")
testRunBackup(t, []string{env.testdata}, BackupOptions{}, gopts)
snapshotIDs = testRunList(t, "snapshots", gopts)
Assert(t, len(snapshotIDs) == 2,
"expected two snapshots, got %v", snapshotIDs)
checkSnapshots(t, global, repo, mountpoint, env.repo, snapshotIDs)
checkSnapshots(t, gopts, repo, mountpoint, env.repo, snapshotIDs)
// third backup, explicit incremental
cmdBackup(t, global, []string{env.testdata}, &snapshotIDs[0])
snapshotIDs = cmdList(t, global, "snapshots")
bopts := BackupOptions{Parent: snapshotIDs[0].String()}
testRunBackup(t, []string{env.testdata}, bopts, gopts)
snapshotIDs = testRunList(t, "snapshots", gopts)
Assert(t, len(snapshotIDs) == 3,
"expected three snapshots, got %v", snapshotIDs)
checkSnapshots(t, global, repo, mountpoint, env.repo, snapshotIDs)
checkSnapshots(t, gopts, repo, mountpoint, env.repo, snapshotIDs)
})
}
@@ -191,10 +191,10 @@ func TestMountSameTimestamps(t *testing.T) {
t.Skip("Skipping fuse tests")
}
withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
SetupTarTestFixture(t, env.base, filepath.Join("testdata", "repo-same-timestamps.tar.gz"))
repo, err := global.OpenRepository()
repo, err := OpenRepository(gopts)
OK(t, err)
mountpoint, err := ioutil.TempDir(TestTempDir, "restic-test-mount-")
@@ -206,6 +206,6 @@ func TestMountSameTimestamps(t *testing.T) {
restic.TestParseID("5fd0d8b2ef0fa5d23e58f1e460188abb0f525c0f0c4af8365a1280c807a80a1b"),
}
checkSnapshots(t, global, repo, mountpoint, env.repo, ids)
checkSnapshots(t, gopts, repo, mountpoint, env.repo, ids)
})
}

View File

@@ -52,11 +52,6 @@ func nlink(info os.FileInfo) uint64 {
return uint64(stat.Nlink)
}
func inode(info os.FileInfo) uint64 {
stat, _ := info.Sys().(*syscall.Stat_t)
return uint64(stat.Ino)
}
func createFileSetPerHardlink(dir string) map[uint64][]string {
var stat syscall.Stat_t
linkTests := make(map[uint64][]string)

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"os"
"sync"
@@ -32,7 +33,7 @@ func lockRepository(repo *repository.Repository, exclusive bool) (*restic.Lock,
lockFn = restic.NewExclusiveLock
}
lock, err := lockFn(repo)
lock, err := lockFn(context.TODO(), repo)
if err != nil {
return nil, err
}
@@ -75,7 +76,7 @@ func refreshLocks(wg *sync.WaitGroup, done <-chan struct{}) {
debug.Log("refreshing locks")
globalLocks.Lock()
for _, lock := range globalLocks.locks {
err := lock.Refresh()
err := lock.Refresh(context.TODO())
if err != nil {
fmt.Fprintf(os.Stderr, "unable to refresh lock: %v\n", err)
}

View File

@@ -9,6 +9,7 @@ import (
"restic"
"restic/debug"
"restic/options"
"runtime"
"github.com/spf13/cobra"
@@ -57,6 +58,8 @@ func init() {
func main() {
debug.Log("main %#v", os.Args)
debug.Log("restic %s, compiled with %v on %v/%v",
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
err := cmdRoot.Execute()
switch {

View File

@@ -1,6 +1,7 @@
package archiver
import (
"context"
"io"
"restic"
"restic/debug"
@@ -20,7 +21,7 @@ type Reader struct {
}
// Archive reads data from the reader and saves it to the repo.
func (r *Reader) Archive(name string, rd io.Reader, p *restic.Progress) (*restic.Snapshot, restic.ID, error) {
func (r *Reader) Archive(ctx context.Context, name string, rd io.Reader, p *restic.Progress) (*restic.Snapshot, restic.ID, error) {
if name == "" {
return nil, restic.ID{}, errors.New("no filename given")
}
@@ -53,7 +54,7 @@ func (r *Reader) Archive(name string, rd io.Reader, p *restic.Progress) (*restic
id := restic.Hash(chunk.Data)
if !repo.Index().Has(id, restic.DataBlob) {
_, err := repo.SaveBlob(restic.DataBlob, chunk.Data, id)
_, err := repo.SaveBlob(ctx, restic.DataBlob, chunk.Data, id)
if err != nil {
return nil, restic.ID{}, err
}
@@ -87,14 +88,14 @@ func (r *Reader) Archive(name string, rd io.Reader, p *restic.Progress) (*restic
},
}
treeID, err := repo.SaveTree(tree)
treeID, err := repo.SaveTree(ctx, tree)
if err != nil {
return nil, restic.ID{}, err
}
sn.Tree = &treeID
debug.Log("tree saved as %v", treeID.Str())
id, err := repo.SaveJSONUnpacked(restic.SnapshotFile, sn)
id, err := repo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
if err != nil {
return nil, restic.ID{}, err
}
@@ -106,7 +107,7 @@ func (r *Reader) Archive(name string, rd io.Reader, p *restic.Progress) (*restic
return nil, restic.ID{}, err
}
err = repo.SaveIndex()
err = repo.SaveIndex(ctx)
if err != nil {
return nil, restic.ID{}, err
}

View File

@@ -2,6 +2,7 @@ package archiver
import (
"bytes"
"context"
"errors"
"io"
"math/rand"
@@ -12,7 +13,7 @@ import (
)
func loadBlob(t *testing.T, repo restic.Repository, id restic.ID, buf []byte) int {
n, err := repo.LoadBlob(restic.DataBlob, id, buf)
n, err := repo.LoadBlob(context.TODO(), restic.DataBlob, id, buf)
if err != nil {
t.Fatalf("LoadBlob(%v) returned error %v", id, err)
}
@@ -21,7 +22,7 @@ func loadBlob(t *testing.T, repo restic.Repository, id restic.ID, buf []byte) in
}
func checkSavedFile(t *testing.T, repo restic.Repository, treeID restic.ID, name string, rd io.Reader) {
tree, err := repo.LoadTree(treeID)
tree, err := repo.LoadTree(context.TODO(), treeID)
if err != nil {
t.Fatalf("LoadTree() returned error %v", err)
}
@@ -85,7 +86,7 @@ func TestArchiveReader(t *testing.T) {
Tags: []string{"test"},
}
sn, id, err := r.Archive("fakefile", f, nil)
sn, id, err := r.Archive(context.TODO(), "fakefile", f, nil)
if err != nil {
t.Fatalf("ArchiveReader() returned error %v", err)
}
@@ -111,7 +112,7 @@ func TestArchiveReaderNull(t *testing.T) {
Tags: []string{"test"},
}
sn, id, err := r.Archive("fakefile", bytes.NewReader(nil), nil)
sn, id, err := r.Archive(context.TODO(), "fakefile", bytes.NewReader(nil), nil)
if err != nil {
t.Fatalf("ArchiveReader() returned error %v", err)
}
@@ -132,11 +133,8 @@ func (e errReader) Read([]byte) (int, error) {
}
func countSnapshots(t testing.TB, repo restic.Repository) int {
done := make(chan struct{})
defer close(done)
snapshots := 0
for range repo.List(restic.SnapshotFile, done) {
for range repo.List(context.TODO(), restic.SnapshotFile) {
snapshots++
}
return snapshots
@@ -152,7 +150,7 @@ func TestArchiveReaderError(t *testing.T) {
Tags: []string{"test"},
}
sn, id, err := r.Archive("fakefile", errReader("error returned by reading stdin"), nil)
sn, id, err := r.Archive(context.TODO(), "fakefile", errReader("error returned by reading stdin"), nil)
if err == nil {
t.Errorf("expected error not returned")
}
@@ -195,7 +193,7 @@ func BenchmarkArchiveReader(t *testing.B) {
t.ResetTimer()
for i := 0; i < t.N; i++ {
_, _, err := r.Archive("fakefile", bytes.NewReader(buf), nil)
_, _, err := r.Archive(context.TODO(), "fakefile", bytes.NewReader(buf), nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -1,6 +1,7 @@
package archiver
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -92,7 +93,7 @@ func (arch *Archiver) isKnownBlob(id restic.ID, t restic.BlobType) bool {
}
// Save stores a blob read from rd in the repository.
func (arch *Archiver) Save(t restic.BlobType, data []byte, id restic.ID) error {
func (arch *Archiver) Save(ctx context.Context, t restic.BlobType, data []byte, id restic.ID) error {
debug.Log("Save(%v, %v)\n", t, id.Str())
if arch.isKnownBlob(id, restic.DataBlob) {
@@ -100,7 +101,7 @@ func (arch *Archiver) Save(t restic.BlobType, data []byte, id restic.ID) error {
return nil
}
_, err := arch.repo.SaveBlob(t, data, id)
_, err := arch.repo.SaveBlob(ctx, t, data, id)
if err != nil {
debug.Log("Save(%v, %v): error %v\n", t, id.Str(), err)
return err
@@ -111,7 +112,7 @@ func (arch *Archiver) Save(t restic.BlobType, data []byte, id restic.ID) error {
}
// SaveTreeJSON stores a tree in the repository.
func (arch *Archiver) SaveTreeJSON(tree *restic.Tree) (restic.ID, error) {
func (arch *Archiver) SaveTreeJSON(ctx context.Context, tree *restic.Tree) (restic.ID, error) {
data, err := json.Marshal(tree)
if err != nil {
return restic.ID{}, errors.Wrap(err, "Marshal")
@@ -124,7 +125,7 @@ func (arch *Archiver) SaveTreeJSON(tree *restic.Tree) (restic.ID, error) {
return id, nil
}
return arch.repo.SaveBlob(restic.TreeBlob, data, id)
return arch.repo.SaveBlob(ctx, restic.TreeBlob, data, id)
}
func (arch *Archiver) reloadFileIfChanged(node *restic.Node, file fs.File) (*restic.Node, error) {
@@ -153,13 +154,14 @@ type saveResult struct {
bytes uint64
}
func (arch *Archiver) saveChunk(chunk chunker.Chunk, p *restic.Progress, token struct{}, file fs.File, resultChannel chan<- saveResult) {
func (arch *Archiver) saveChunk(ctx context.Context, chunk chunker.Chunk, p *restic.Progress, token struct{}, file fs.File, resultChannel chan<- saveResult) {
defer freeBuf(chunk.Data)
id := restic.Hash(chunk.Data)
err := arch.Save(restic.DataBlob, chunk.Data, id)
err := arch.Save(ctx, restic.DataBlob, chunk.Data, id)
// TODO handle error
if err != nil {
debug.Log("Save(%v) failed: %v", id.Str(), err)
panic(err)
}
@@ -206,7 +208,7 @@ func updateNodeContent(node *restic.Node, results []saveResult) error {
// SaveFile stores the content of the file on the backend as a Blob by calling
// Save for each chunk.
func (arch *Archiver) SaveFile(p *restic.Progress, node *restic.Node) (*restic.Node, error) {
func (arch *Archiver) SaveFile(ctx context.Context, p *restic.Progress, node *restic.Node) (*restic.Node, error) {
file, err := fs.Open(node.Path)
defer file.Close()
if err != nil {
@@ -234,7 +236,7 @@ func (arch *Archiver) SaveFile(p *restic.Progress, node *restic.Node) (*restic.N
}
resCh := make(chan saveResult, 1)
go arch.saveChunk(chunk, p, <-arch.blobToken, file, resCh)
go arch.saveChunk(ctx, chunk, p, <-arch.blobToken, file, resCh)
resultChannels = append(resultChannels, resCh)
}
@@ -247,7 +249,7 @@ func (arch *Archiver) SaveFile(p *restic.Progress, node *restic.Node) (*restic.N
return node, err
}
func (arch *Archiver) fileWorker(wg *sync.WaitGroup, p *restic.Progress, done <-chan struct{}, entCh <-chan pipe.Entry) {
func (arch *Archiver) fileWorker(ctx context.Context, wg *sync.WaitGroup, p *restic.Progress, entCh <-chan pipe.Entry) {
defer func() {
debug.Log("done")
wg.Done()
@@ -305,7 +307,7 @@ func (arch *Archiver) fileWorker(wg *sync.WaitGroup, p *restic.Progress, done <-
// otherwise read file normally
if node.Type == "file" && len(node.Content) == 0 {
debug.Log(" read and save %v", e.Path())
node, err = arch.SaveFile(p, node)
node, err = arch.SaveFile(ctx, p, node)
if err != nil {
fmt.Fprintf(os.Stderr, "error for %v: %v\n", node.Path, err)
arch.Warn(e.Path(), nil, err)
@@ -322,14 +324,14 @@ func (arch *Archiver) fileWorker(wg *sync.WaitGroup, p *restic.Progress, done <-
debug.Log(" processed %v, %d blobs", e.Path(), len(node.Content))
e.Result() <- node
p.Report(restic.Stat{Files: 1})
case <-done:
case <-ctx.Done():
// pipeline was cancelled
return
}
}
}
func (arch *Archiver) dirWorker(wg *sync.WaitGroup, p *restic.Progress, done <-chan struct{}, dirCh <-chan pipe.Dir) {
func (arch *Archiver) dirWorker(ctx context.Context, wg *sync.WaitGroup, p *restic.Progress, dirCh <-chan pipe.Dir) {
debug.Log("start")
defer func() {
debug.Log("done")
@@ -398,7 +400,7 @@ func (arch *Archiver) dirWorker(wg *sync.WaitGroup, p *restic.Progress, done <-c
node.Error = err.Error()
}
id, err := arch.SaveTreeJSON(tree)
id, err := arch.SaveTreeJSON(ctx, tree)
if err != nil {
panic(err)
}
@@ -415,7 +417,7 @@ func (arch *Archiver) dirWorker(wg *sync.WaitGroup, p *restic.Progress, done <-c
if dir.Path() != "" {
p.Report(restic.Stat{Dirs: 1})
}
case <-done:
case <-ctx.Done():
// pipeline was cancelled
return
}
@@ -427,7 +429,7 @@ type archivePipe struct {
New <-chan pipe.Job
}
func copyJobs(done <-chan struct{}, in <-chan pipe.Job, out chan<- pipe.Job) {
func copyJobs(ctx context.Context, in <-chan pipe.Job, out chan<- pipe.Job) {
var (
// disable sending on the outCh until we received a job
outCh chan<- pipe.Job
@@ -439,7 +441,7 @@ func copyJobs(done <-chan struct{}, in <-chan pipe.Job, out chan<- pipe.Job) {
for {
select {
case <-done:
case <-ctx.Done():
return
case job, ok = <-inCh:
if !ok {
@@ -462,7 +464,7 @@ type archiveJob struct {
new pipe.Job
}
func (a *archivePipe) compare(done <-chan struct{}, out chan<- pipe.Job) {
func (a *archivePipe) compare(ctx context.Context, out chan<- pipe.Job) {
defer func() {
close(out)
debug.Log("done")
@@ -488,7 +490,7 @@ func (a *archivePipe) compare(done <-chan struct{}, out chan<- pipe.Job) {
out <- archiveJob{new: newJob}.Copy()
}
copyJobs(done, a.New, out)
copyJobs(ctx, a.New, out)
return
}
@@ -585,7 +587,7 @@ func (j archiveJob) Copy() pipe.Job {
const saveIndexTime = 30 * time.Second
// saveIndexes regularly queries the master index for full indexes and saves them.
func (arch *Archiver) saveIndexes(wg *sync.WaitGroup, done <-chan struct{}) {
func (arch *Archiver) saveIndexes(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
ticker := time.NewTicker(saveIndexTime)
@@ -593,11 +595,11 @@ func (arch *Archiver) saveIndexes(wg *sync.WaitGroup, done <-chan struct{}) {
for {
select {
case <-done:
case <-ctx.Done():
return
case <-ticker.C:
debug.Log("saving full indexes")
err := arch.repo.SaveFullIndex()
err := arch.repo.SaveFullIndex(ctx)
if err != nil {
debug.Log("save indexes returned an error: %v", err)
fmt.Fprintf(os.Stderr, "error saving preliminary index: %v\n", err)
@@ -634,7 +636,7 @@ func (p baseNameSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// Snapshot creates a snapshot of the given paths. If parentrestic.ID is set, this is
// used to compare the files to the ones archived at the time this snapshot was
// taken.
func (arch *Archiver) Snapshot(p *restic.Progress, paths, tags []string, hostname string, parentID *restic.ID) (*restic.Snapshot, restic.ID, error) {
func (arch *Archiver) Snapshot(ctx context.Context, p *restic.Progress, paths, tags []string, hostname string, parentID *restic.ID) (*restic.Snapshot, restic.ID, error) {
paths = unique(paths)
sort.Sort(baseNameSlice(paths))
@@ -643,7 +645,6 @@ func (arch *Archiver) Snapshot(p *restic.Progress, paths, tags []string, hostnam
debug.RunHook("Archiver.Snapshot", nil)
// signal the whole pipeline to stop
done := make(chan struct{})
var err error
p.Start()
@@ -663,14 +664,14 @@ func (arch *Archiver) Snapshot(p *restic.Progress, paths, tags []string, hostnam
sn.Parent = parentID
// load parent snapshot
parent, err := restic.LoadSnapshot(arch.repo, *parentID)
parent, err := restic.LoadSnapshot(ctx, arch.repo, *parentID)
if err != nil {
return nil, restic.ID{}, err
}
// start walker on old tree
ch := make(chan walk.TreeJob)
go walk.Tree(arch.repo, *parent.Tree, done, ch)
go walk.Tree(ctx, arch.repo, *parent.Tree, ch)
jobs.Old = ch
} else {
// use closed channel
@@ -683,13 +684,13 @@ func (arch *Archiver) Snapshot(p *restic.Progress, paths, tags []string, hostnam
pipeCh := make(chan pipe.Job)
resCh := make(chan pipe.Result, 1)
go func() {
pipe.Walk(paths, arch.SelectFilter, done, pipeCh, resCh)
pipe.Walk(ctx, paths, arch.SelectFilter, pipeCh, resCh)
debug.Log("pipe.Walk done")
}()
jobs.New = pipeCh
ch := make(chan pipe.Job)
go jobs.compare(done, ch)
go jobs.compare(ctx, ch)
var wg sync.WaitGroup
entCh := make(chan pipe.Entry)
@@ -708,22 +709,22 @@ func (arch *Archiver) Snapshot(p *restic.Progress, paths, tags []string, hostnam
// run workers
for i := 0; i < maxConcurrency; i++ {
wg.Add(2)
go arch.fileWorker(&wg, p, done, entCh)
go arch.dirWorker(&wg, p, done, dirCh)
go arch.fileWorker(ctx, &wg, p, entCh)
go arch.dirWorker(ctx, &wg, p, dirCh)
}
// run index saver
var wgIndexSaver sync.WaitGroup
stopIndexSaver := make(chan struct{})
indexCtx, indexCancel := context.WithCancel(ctx)
wgIndexSaver.Add(1)
go arch.saveIndexes(&wgIndexSaver, stopIndexSaver)
go arch.saveIndexes(indexCtx, &wgIndexSaver)
// wait for all workers to terminate
debug.Log("wait for workers")
wg.Wait()
// stop index saver
close(stopIndexSaver)
indexCancel()
wgIndexSaver.Wait()
debug.Log("workers terminated")
@@ -740,7 +741,7 @@ func (arch *Archiver) Snapshot(p *restic.Progress, paths, tags []string, hostnam
sn.Tree = root.Subtree
// load top-level tree again to see if it is empty
toptree, err := arch.repo.LoadTree(*root.Subtree)
toptree, err := arch.repo.LoadTree(ctx, *root.Subtree)
if err != nil {
return nil, restic.ID{}, err
}
@@ -750,7 +751,7 @@ func (arch *Archiver) Snapshot(p *restic.Progress, paths, tags []string, hostnam
}
// save index
err = arch.repo.SaveIndex()
err = arch.repo.SaveIndex(ctx)
if err != nil {
debug.Log("error saving index: %v", err)
return nil, restic.ID{}, err
@@ -759,7 +760,7 @@ func (arch *Archiver) Snapshot(p *restic.Progress, paths, tags []string, hostnam
debug.Log("saved indexes")
// save snapshot
id, err := arch.repo.SaveJSONUnpacked(restic.SnapshotFile, sn)
id, err := arch.repo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
if err != nil {
return nil, restic.ID{}, err
}

View File

@@ -1,6 +1,7 @@
package archiver_test
import (
"context"
"crypto/rand"
"io"
mrand "math/rand"
@@ -39,33 +40,33 @@ func randomID() restic.ID {
func forgetfulBackend() restic.Backend {
be := &mock.Backend{}
be.TestFn = func(h restic.Handle) (bool, error) {
be.TestFn = func(ctx context.Context, h restic.Handle) (bool, error) {
return false, nil
}
be.LoadFn = func(h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
be.LoadFn = func(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
return nil, errors.New("not found")
}
be.SaveFn = func(h restic.Handle, rd io.Reader) error {
be.SaveFn = func(ctx context.Context, h restic.Handle, rd io.Reader) error {
return nil
}
be.StatFn = func(h restic.Handle) (restic.FileInfo, error) {
be.StatFn = func(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
return restic.FileInfo{}, errors.New("not found")
}
be.RemoveFn = func(h restic.Handle) error {
be.RemoveFn = func(ctx context.Context, h restic.Handle) error {
return nil
}
be.ListFn = func(t restic.FileType, done <-chan struct{}) <-chan string {
be.ListFn = func(ctx context.Context, t restic.FileType) <-chan string {
ch := make(chan string)
close(ch)
return ch
}
be.DeleteFn = func() error {
be.DeleteFn = func(ctx context.Context) error {
return nil
}
@@ -80,7 +81,7 @@ func testArchiverDuplication(t *testing.T) {
repo := repository.New(forgetfulBackend())
err = repo.Init("foo")
err = repo.Init(context.TODO(), "foo")
if err != nil {
t.Fatal(err)
}
@@ -108,7 +109,7 @@ func testArchiverDuplication(t *testing.T) {
buf := make([]byte, 50)
err := arch.Save(restic.DataBlob, buf, id)
err := arch.Save(context.TODO(), restic.DataBlob, buf, id)
if err != nil {
t.Fatal(err)
}
@@ -127,7 +128,7 @@ func testArchiverDuplication(t *testing.T) {
case <-done:
return
case <-ticker.C:
err := repo.SaveFullIndex()
err := repo.SaveFullIndex(context.TODO())
if err != nil {
t.Fatal(err)
}

View File

@@ -1,6 +1,7 @@
package archiver
import (
"context"
"os"
"testing"
@@ -83,10 +84,10 @@ func (j testPipeJob) Error() error { return j.err }
func (j testPipeJob) Info() os.FileInfo { return j.fi }
func (j testPipeJob) Result() chan<- pipe.Result { return j.res }
func testTreeWalker(done <-chan struct{}, out chan<- walk.TreeJob) {
func testTreeWalker(ctx context.Context, out chan<- walk.TreeJob) {
for _, e := range treeJobs {
select {
case <-done:
case <-ctx.Done():
return
case out <- walk.TreeJob{Path: e}:
}
@@ -95,10 +96,10 @@ func testTreeWalker(done <-chan struct{}, out chan<- walk.TreeJob) {
close(out)
}
func testPipeWalker(done <-chan struct{}, out chan<- pipe.Job) {
func testPipeWalker(ctx context.Context, out chan<- pipe.Job) {
for _, e := range pipeJobs {
select {
case <-done:
case <-ctx.Done():
return
case out <- testPipeJob{path: e}:
}
@@ -108,19 +109,19 @@ func testPipeWalker(done <-chan struct{}, out chan<- pipe.Job) {
}
func TestArchivePipe(t *testing.T) {
done := make(chan struct{})
ctx := context.TODO()
treeCh := make(chan walk.TreeJob)
pipeCh := make(chan pipe.Job)
go testTreeWalker(done, treeCh)
go testPipeWalker(done, pipeCh)
go testTreeWalker(ctx, treeCh)
go testPipeWalker(ctx, pipeCh)
p := archivePipe{Old: treeCh, New: pipeCh}
ch := make(chan pipe.Job)
go p.compare(done, ch)
go p.compare(ctx, ch)
i := 0
for job := range ch {

View File

@@ -2,6 +2,7 @@ package archiver_test
import (
"bytes"
"context"
"io"
"testing"
"time"
@@ -42,7 +43,7 @@ func benchmarkChunkEncrypt(b testing.TB, buf, buf2 []byte, rd Rdr, key *crypto.K
Assert(b, uint(len(chunk.Data)) == chunk.Length,
"invalid length: got %d, expected %d", len(chunk.Data), chunk.Length)
_, err = crypto.Encrypt(key, buf2, chunk.Data)
_, err = key.Encrypt(buf2, chunk.Data)
OK(b, err)
}
}
@@ -75,7 +76,7 @@ func benchmarkChunkEncryptP(b *testing.PB, buf []byte, rd Rdr, key *crypto.Key)
}
// reduce length of chunkBuf
crypto.Encrypt(key, chunk.Data, chunk.Data)
key.Encrypt(chunk.Data, chunk.Data)
}
}
@@ -104,7 +105,7 @@ func archiveDirectory(b testing.TB) {
arch := archiver.New(repo)
_, id, err := arch.Snapshot(nil, []string{BenchArchiveDirectory}, nil, "localhost", nil)
_, id, err := arch.Snapshot(context.TODO(), nil, []string{BenchArchiveDirectory}, nil, "localhost", nil)
OK(b, err)
b.Logf("snapshot archived as %v", id)
@@ -129,7 +130,7 @@ func BenchmarkArchiveDirectory(b *testing.B) {
}
func countPacks(repo restic.Repository, t restic.FileType) (n uint) {
for range repo.Backend().List(t, nil) {
for range repo.Backend().List(context.TODO(), t) {
n++
}
@@ -234,7 +235,7 @@ func testParallelSaveWithDuplication(t *testing.T, seed int) {
id := restic.Hash(c.Data)
time.Sleep(time.Duration(id[0]))
err := arch.Save(restic.DataBlob, c.Data, id)
err := arch.Save(context.TODO(), restic.DataBlob, c.Data, id)
<-barrier
errChan <- err
}(c, errChan)
@@ -246,7 +247,7 @@ func testParallelSaveWithDuplication(t *testing.T, seed int) {
}
OK(t, repo.Flush())
OK(t, repo.SaveIndex())
OK(t, repo.SaveIndex(context.TODO()))
chkr := createAndInitChecker(t, repo)
assertNoUnreferencedPacks(t, chkr)
@@ -271,7 +272,7 @@ func getRandomData(seed int, size int) []chunker.Chunk {
func createAndInitChecker(t *testing.T, repo restic.Repository) *checker.Checker {
chkr := checker.New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) > 0 {
t.Fatalf("expected no errors, got %v: %v", len(errs), errs)
}
@@ -284,11 +285,8 @@ func createAndInitChecker(t *testing.T, repo restic.Repository) *checker.Checker
}
func assertNoUnreferencedPacks(t *testing.T, chkr *checker.Checker) {
done := make(chan struct{})
defer close(done)
errChan := make(chan error)
go chkr.Packs(errChan, done)
go chkr.Packs(context.TODO(), errChan)
for err := range errChan {
OK(t, err)
@@ -301,7 +299,7 @@ func TestArchiveEmptySnapshot(t *testing.T) {
arch := archiver.New(repo)
sn, id, err := arch.Snapshot(nil, []string{"file-does-not-exist-123123213123", "file2-does-not-exist-too-123123123"}, nil, "localhost", nil)
sn, id, err := arch.Snapshot(context.TODO(), nil, []string{"file-does-not-exist-123123213123", "file2-does-not-exist-too-123123123"}, nil, "localhost", nil)
if err == nil {
t.Errorf("expected error for empty snapshot, got nil")
}

View File

@@ -1,6 +1,7 @@
package archiver
import (
"context"
"restic"
"testing"
)
@@ -8,7 +9,7 @@ import (
// TestSnapshot creates a new snapshot of path.
func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *restic.ID) *restic.Snapshot {
arch := New(repo)
sn, _, err := arch.Snapshot(nil, []string{path}, []string{"test"}, "localhost", parent)
sn, _, err := arch.Snapshot(context.TODO(), nil, []string{path}, []string{"test"}, "localhost", parent)
if err != nil {
t.Fatal(err)
}

View File

@@ -1,6 +1,9 @@
package restic
import "io"
import (
"context"
"io"
)
// Backend is used to store and access data.
type Backend interface {
@@ -9,30 +12,34 @@ type Backend interface {
Location() string
// Test a boolean value whether a File with the name and type exists.
Test(h Handle) (bool, error)
Test(ctx context.Context, h Handle) (bool, error)
// Remove removes a File with type t and name.
Remove(h Handle) error
Remove(ctx context.Context, h Handle) error
// Close the backend
Close() error
// Save stores the data in the backend under the given handle.
Save(h Handle, rd io.Reader) error
Save(ctx context.Context, h Handle, rd io.Reader) error
// Load returns a reader that yields the contents of the file at h at the
// given offset. If length is larger than zero, only a portion of the file
// is returned. rd must be closed after use. If an error is returned, the
// ReadCloser must be nil.
Load(h Handle, length int, offset int64) (io.ReadCloser, error)
Load(ctx context.Context, h Handle, length int, offset int64) (io.ReadCloser, error)
// Stat returns information about the File identified by h.
Stat(h Handle) (FileInfo, error)
Stat(ctx context.Context, h Handle) (FileInfo, error)
// List returns a channel that yields all names of files of type t in an
// arbitrary order. A goroutine is started for this. If the channel done is
// closed, sending stops.
List(t FileType, done <-chan struct{}) <-chan string
// arbitrary order. A goroutine is started for this, which is stopped when
// ctx is cancelled.
List(ctx context.Context, t FileType) <-chan string
// IsNotExist returns true if the error was caused by a non-existing file
// in the backend.
IsNotExist(err error) bool
}
// FileInfo is returned by Stat() and contains information about a file in the

377
src/restic/backend/b2/b2.go Normal file
View File

@@ -0,0 +1,377 @@
package b2
import (
"context"
"io"
"path"
"restic"
"strings"
"restic/backend"
"restic/debug"
"restic/errors"
"github.com/kurin/blazer/b2"
)
// b2Backend is a backend which stores its data on Backblaze B2.
type b2Backend struct {
client *b2.Client
bucket *b2.Bucket
cfg Config
backend.Layout
sem *backend.Semaphore
}
// ensure statically that *b2Backend implements restic.Backend.
var _ restic.Backend = &b2Backend{}
func newClient(ctx context.Context, cfg Config) (*b2.Client, error) {
opts := []b2.ClientOption{b2.Transport(backend.Transport())}
c, err := b2.NewClient(ctx, cfg.AccountID, cfg.Key, opts...)
if err != nil {
return nil, errors.Wrap(err, "b2.NewClient")
}
return c, nil
}
// Open opens a connection to the B2 service.
func Open(cfg Config) (restic.Backend, error) {
debug.Log("cfg %#v", cfg)
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
client, err := newClient(ctx, cfg)
if err != nil {
return nil, err
}
bucket, err := client.Bucket(ctx, cfg.Bucket)
if err != nil {
return nil, errors.Wrap(err, "Bucket")
}
sem, err := backend.NewSemaphore(cfg.Connections)
if err != nil {
return nil, err
}
be := &b2Backend{
client: client,
bucket: bucket,
cfg: cfg,
Layout: &backend.DefaultLayout{
Join: path.Join,
Path: cfg.Prefix,
},
sem: sem,
}
return be, nil
}
// Create opens a connection to the B2 service. If the bucket does not exist yet,
// it is created.
func Create(cfg Config) (restic.Backend, error) {
debug.Log("cfg %#v", cfg)
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
client, err := newClient(ctx, cfg)
if err != nil {
return nil, err
}
attr := b2.BucketAttrs{
Type: b2.Private,
}
bucket, err := client.NewBucket(ctx, cfg.Bucket, &attr)
if err != nil {
return nil, errors.Wrap(err, "NewBucket")
}
sem, err := backend.NewSemaphore(cfg.Connections)
if err != nil {
return nil, err
}
be := &b2Backend{
client: client,
bucket: bucket,
cfg: cfg,
Layout: &backend.DefaultLayout{
Join: path.Join,
Path: cfg.Prefix,
},
sem: sem,
}
present, err := be.Test(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil {
return nil, err
}
if present {
return nil, errors.New("config already exists")
}
return be, nil
}
// Location returns the location for the backend.
func (be *b2Backend) Location() string {
return be.cfg.Bucket
}
// wrapReader wraps an io.ReadCloser to run an additional function on Close.
type wrapReader struct {
io.ReadCloser
eofSeen bool
f func()
}
func (wr *wrapReader) Read(p []byte) (int, error) {
if wr.eofSeen {
return 0, io.EOF
}
n, err := wr.ReadCloser.Read(p)
if err == io.EOF {
wr.eofSeen = true
}
return n, err
}
func (wr *wrapReader) Close() error {
err := wr.ReadCloser.Close()
wr.f()
return err
}
// IsNotExist returns true if the error is caused by a non-existing file.
func (be *b2Backend) IsNotExist(err error) bool {
return b2.IsNotExist(errors.Cause(err))
}
// Load returns the data stored in the backend for h at the given offset
// and saves it in p. Load has the same semantics as io.ReaderAt.
func (be *b2Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h))
if err := h.Valid(); err != nil {
return nil, err
}
if offset < 0 {
return nil, errors.New("offset is negative")
}
if length < 0 {
return nil, errors.Errorf("invalid length %d", length)
}
ctx, cancel := context.WithCancel(ctx)
be.sem.GetToken()
name := be.Layout.Filename(h)
obj := be.bucket.Object(name)
if offset == 0 && length == 0 {
rd := obj.NewReader(ctx)
wrapper := &wrapReader{
ReadCloser: rd,
f: func() {
cancel()
be.sem.ReleaseToken()
},
}
return wrapper, nil
}
// pass a negative length to NewRangeReader so that the remainder of the
// file is read.
if length == 0 {
length = -1
}
rd := obj.NewRangeReader(ctx, offset, int64(length))
wrapper := &wrapReader{
ReadCloser: rd,
f: func() {
cancel()
be.sem.ReleaseToken()
},
}
return wrapper, nil
}
// Save stores data in the backend at the handle.
func (be *b2Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if err := h.Valid(); err != nil {
return err
}
be.sem.GetToken()
defer be.sem.ReleaseToken()
name := be.Filename(h)
debug.Log("Save %v, name %v", h, name)
obj := be.bucket.Object(name)
_, err = obj.Attrs(ctx)
if err == nil {
debug.Log(" %v already exists", h)
return errors.New("key already exists")
}
w := obj.NewWriter(ctx)
n, err := io.Copy(w, rd)
debug.Log(" saved %d bytes, err %v", n, err)
if err != nil {
_ = w.Close()
return errors.Wrap(err, "Copy")
}
return errors.Wrap(w.Close(), "Close")
}
// Stat returns information about a blob.
func (be *b2Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) {
debug.Log("Stat %v", h)
be.sem.GetToken()
defer be.sem.ReleaseToken()
name := be.Filename(h)
obj := be.bucket.Object(name)
info, err := obj.Attrs(ctx)
if err != nil {
debug.Log("Attrs() err %v", err)
return restic.FileInfo{}, errors.Wrap(err, "Stat")
}
return restic.FileInfo{Size: info.Size}, nil
}
// Test returns true if a blob of the given type and name exists in the backend.
func (be *b2Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
debug.Log("Test %v", h)
be.sem.GetToken()
defer be.sem.ReleaseToken()
found := false
name := be.Filename(h)
obj := be.bucket.Object(name)
info, err := obj.Attrs(ctx)
if err == nil && info != nil && info.Status == b2.Uploaded {
found = true
}
return found, nil
}
// Remove removes the blob with the given name and type.
func (be *b2Backend) Remove(ctx context.Context, h restic.Handle) error {
debug.Log("Remove %v", h)
be.sem.GetToken()
defer be.sem.ReleaseToken()
obj := be.bucket.Object(be.Filename(h))
return errors.Wrap(obj.Delete(ctx), "Delete")
}
// List returns a channel that yields all names of blobs of type t. A
// goroutine is started for this. If the channel done is closed, sending
// stops.
func (be *b2Backend) List(ctx context.Context, t restic.FileType) <-chan string {
debug.Log("List %v", t)
ch := make(chan string)
ctx, cancel := context.WithCancel(ctx)
be.sem.GetToken()
go func() {
defer close(ch)
defer cancel()
defer be.sem.ReleaseToken()
prefix := be.Dirname(restic.Handle{Type: t})
cur := &b2.Cursor{Prefix: prefix}
for {
objs, c, err := be.bucket.ListCurrentObjects(ctx, 1000, cur)
if err != nil && err != io.EOF {
return
}
for _, obj := range objs {
// Skip objects returned that do not have the specified prefix.
if !strings.HasPrefix(obj.Name(), prefix) {
continue
}
m := path.Base(obj.Name())
if m == "" {
continue
}
select {
case ch <- m:
case <-ctx.Done():
return
}
}
if err == io.EOF {
return
}
cur = c
}
}()
return ch
}
// Remove keys for a specified backend type.
func (be *b2Backend) removeKeys(ctx context.Context, t restic.FileType) error {
debug.Log("removeKeys %v", t)
for key := range be.List(ctx, t) {
err := be.Remove(ctx, restic.Handle{Type: t, Name: key})
if err != nil {
return err
}
}
return nil
}
// Delete removes all restic keys in the bucket. It will not remove the bucket itself.
func (be *b2Backend) Delete(ctx context.Context) error {
alltypes := []restic.FileType{
restic.DataFile,
restic.KeyFile,
restic.LockFile,
restic.SnapshotFile,
restic.IndexFile}
for _, t := range alltypes {
err := be.removeKeys(ctx, t)
if err != nil {
return nil
}
}
err := be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
if err != nil && b2.IsNotExist(errors.Cause(err)) {
err = nil
}
return err
}
// Close does nothing
func (be *b2Backend) Close() error { return nil }

View File

@@ -0,0 +1,97 @@
package b2_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"restic"
"restic/backend/b2"
"restic/backend/test"
. "restic/test"
)
func newB2TestSuite(t testing.TB) *test.Suite {
return &test.Suite{
// do not use excessive data
MinimalData: true,
// wait for at most 10 seconds for removed files to disappear
WaitForDelayedRemoval: 10 * time.Second,
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
b2cfg, err := b2.ParseConfig(os.Getenv("RESTIC_TEST_B2_REPOSITORY"))
if err != nil {
return nil, err
}
cfg := b2cfg.(b2.Config)
cfg.AccountID = os.Getenv("RESTIC_TEST_B2_ACCOUNT_ID")
cfg.Key = os.Getenv("RESTIC_TEST_B2_ACCOUNT_KEY")
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
return cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(b2.Config)
return b2.Create(cfg)
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(b2.Config)
return b2.Open(cfg)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(b2.Config)
be, err := b2.Open(cfg)
if err != nil {
return err
}
if err := be.(restic.Deleter).Delete(context.TODO()); err != nil {
return err
}
return nil
},
}
}
func testVars(t testing.TB) {
vars := []string{
"RESTIC_TEST_B2_ACCOUNT_ID",
"RESTIC_TEST_B2_ACCOUNT_KEY",
"RESTIC_TEST_B2_REPOSITORY",
}
for _, v := range vars {
if os.Getenv(v) == "" {
t.Skipf("environment variable %v not set", v)
return
}
}
}
func TestBackendB2(t *testing.T) {
defer func() {
if t.Skipped() {
SkipDisallowed(t, "restic/backend/b2.TestBackendB2")
}
}()
testVars(t)
newB2TestSuite(t).RunTests(t)
}
func BenchmarkBackendb2(t *testing.B) {
testVars(t)
newB2TestSuite(t).RunBenchmarks(t)
}

View File

@@ -0,0 +1,93 @@
package b2
import (
"path"
"regexp"
"strings"
"restic/errors"
"restic/options"
)
// Config contains all configuration necessary to connect to an b2 compatible
// server.
type Config struct {
AccountID string
Key string
Bucket string
Prefix string
Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"`
}
// NewConfig returns a new config with default options applied.
func NewConfig() Config {
return Config{
Connections: 5,
}
}
func init() {
options.Register("b2", Config{})
}
var bucketName = regexp.MustCompile("^[a-zA-Z0-9-]+$")
// checkBucketName tests the bucket name against the rules at
// https://help.backblaze.com/hc/en-us/articles/217666908-What-you-need-to-know-about-B2-Bucket-names
func checkBucketName(name string) error {
if name == "" {
return errors.New("bucket name is empty")
}
if len(name) < 6 {
return errors.New("bucket name is too short")
}
if len(name) > 50 {
return errors.New("bucket name is too long")
}
if !bucketName.MatchString(name) {
return errors.New("bucket name contains invalid characters, allowed are: a-z, 0-9, dash (-)")
}
return nil
}
// ParseConfig parses the string s and extracts the b2 config. The supported
// configuration format is b2:bucketname/prefix. If no prefix is given the
// prefix "restic" will be used.
func ParseConfig(s string) (interface{}, error) {
if !strings.HasPrefix(s, "b2:") {
return nil, errors.New("invalid format, want: b2:bucket-name[:path]")
}
s = s[3:]
data := strings.SplitN(s, ":", 2)
if len(data) == 0 || len(data[0]) == 0 {
return nil, errors.New("bucket name not found")
}
cfg := NewConfig()
cfg.Bucket = data[0]
if err := checkBucketName(cfg.Bucket); err != nil {
return nil, err
}
if len(data) == 2 {
p := data[1]
if len(p) > 0 {
p = path.Clean(p)
}
if len(p) > 0 && path.IsAbs(p) {
p = p[1:]
}
cfg.Prefix = p
}
return cfg, nil
}

View File

@@ -0,0 +1,92 @@
package b2
import "testing"
var configTests = []struct {
s string
cfg Config
}{
{"b2:bucketname", Config{
Bucket: "bucketname",
Prefix: "",
Connections: 5,
}},
{"b2:bucketname:", Config{
Bucket: "bucketname",
Prefix: "",
Connections: 5,
}},
{"b2:bucketname:/prefix/directory", Config{
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
}},
{"b2:foobar", Config{
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
{"b2:foobar:", Config{
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
{"b2:foobar:/", Config{
Bucket: "foobar",
Prefix: "",
Connections: 5,
}},
}
func TestParseConfig(t *testing.T) {
for _, test := range configTests {
t.Run("", func(t *testing.T) {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Fatalf("%s failed: %v", test.s, err)
}
if cfg != test.cfg {
t.Fatalf("input: %s\n wrong config, want:\n %#v\ngot:\n %#v",
test.s, test.cfg, cfg)
}
})
}
}
var invalidConfigTests = []struct {
s string
err string
}{
{
"b2",
"invalid format, want: b2:bucket-name[:path]",
},
{
"b2:",
"bucket name not found",
},
{
"b2:bucket_name",
"bucket name contains invalid characters, allowed are: a-z, 0-9, dash (-)",
},
{
"b2:bucketname/prefix/directory/",
"bucket name contains invalid characters, allowed are: a-z, 0-9, dash (-)",
},
}
func TestInvalidConfig(t *testing.T) {
for _, test := range invalidConfigTests {
t.Run("", func(t *testing.T) {
cfg, err := ParseConfig(test.s)
if err == nil {
t.Fatalf("expected error not found for invalid config: %v, cfg is:\n%#v", test.s, cfg)
}
if err.Error() != test.err {
t.Fatalf("unexpected error found, want:\n %v\ngot:\n %v", test.err, err.Error())
}
})
}
}

View File

@@ -18,6 +18,7 @@ func Transport() http.RoundTripper {
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,

View File

@@ -17,6 +17,7 @@ type Layout interface {
Dirname(restic.Handle) string
Basedir(restic.FileType) string
Paths() []string
Name() string
}
// Filesystem is the abstraction of a file system used for a backend.

View File

@@ -19,6 +19,15 @@ var defaultLayoutPaths = map[restic.FileType]string{
restic.KeyFile: "keys",
}
func (l *DefaultLayout) String() string {
return "<DefaultLayout>"
}
// Name returns the name for this layout.
func (l *DefaultLayout) Name() string {
return "default"
}
// Dirname returns the directory path for a given file type and name.
func (l *DefaultLayout) Dirname(h restic.Handle) string {
p := defaultLayoutPaths[h.Type]
@@ -34,7 +43,7 @@ func (l *DefaultLayout) Dirname(h restic.Handle) string {
func (l *DefaultLayout) Filename(h restic.Handle) string {
name := h.Name
if h.Type == restic.ConfigFile {
name = "config"
return l.Join(l.Path, "config")
}
return l.Join(l.Dirname(h), name)

View File

@@ -11,6 +11,15 @@ type RESTLayout struct {
var restLayoutPaths = defaultLayoutPaths
func (l *RESTLayout) String() string {
return "<RESTLayout>"
}
// Name returns the name for this layout.
func (l *RESTLayout) Name() string {
return "rest"
}
// Dirname returns the directory path for a given file type and name.
func (l *RESTLayout) Dirname(h restic.Handle) string {
if h.Type == restic.ConfigFile {

View File

@@ -18,6 +18,15 @@ var s3LayoutPaths = map[restic.FileType]string{
restic.KeyFile: "key",
}
func (l *S3LegacyLayout) String() string {
return "<S3LegacyLayout>"
}
// Name returns the name for this layout.
func (l *S3LegacyLayout) Name() string {
return "s3legacy"
}
// join calls Join with the first empty elements removed.
func (l *S3LegacyLayout) join(url string, items ...string) string {
for len(items) > 0 && items[0] == "" {

View File

@@ -12,53 +12,103 @@ import (
)
func TestDefaultLayout(t *testing.T) {
path, cleanup := TempDir(t)
tempdir, cleanup := TempDir(t)
defer cleanup()
var tests = []struct {
path string
join func(...string) string
restic.Handle
filename string
}{
{
tempdir,
filepath.Join,
restic.Handle{Type: restic.DataFile, Name: "0123456"},
filepath.Join(path, "data", "01", "0123456"),
filepath.Join(tempdir, "data", "01", "0123456"),
},
{
tempdir,
filepath.Join,
restic.Handle{Type: restic.ConfigFile, Name: "CFG"},
filepath.Join(path, "config"),
filepath.Join(tempdir, "config"),
},
{
tempdir,
filepath.Join,
restic.Handle{Type: restic.SnapshotFile, Name: "123456"},
filepath.Join(path, "snapshots", "123456"),
filepath.Join(tempdir, "snapshots", "123456"),
},
{
tempdir,
filepath.Join,
restic.Handle{Type: restic.IndexFile, Name: "123456"},
filepath.Join(path, "index", "123456"),
filepath.Join(tempdir, "index", "123456"),
},
{
tempdir,
filepath.Join,
restic.Handle{Type: restic.LockFile, Name: "123456"},
filepath.Join(path, "locks", "123456"),
filepath.Join(tempdir, "locks", "123456"),
},
{
tempdir,
filepath.Join,
restic.Handle{Type: restic.KeyFile, Name: "123456"},
filepath.Join(path, "keys", "123456"),
filepath.Join(tempdir, "keys", "123456"),
},
{
"",
path.Join,
restic.Handle{Type: restic.DataFile, Name: "0123456"},
"data/01/0123456",
},
{
"",
path.Join,
restic.Handle{Type: restic.ConfigFile, Name: "CFG"},
"config",
},
{
"",
path.Join,
restic.Handle{Type: restic.SnapshotFile, Name: "123456"},
"snapshots/123456",
},
{
"",
path.Join,
restic.Handle{Type: restic.IndexFile, Name: "123456"},
"index/123456",
},
{
"",
path.Join,
restic.Handle{Type: restic.LockFile, Name: "123456"},
"locks/123456",
},
{
"",
path.Join,
restic.Handle{Type: restic.KeyFile, Name: "123456"},
"keys/123456",
},
}
l := &DefaultLayout{
Path: path,
Join: filepath.Join,
}
t.Run("Paths", func(t *testing.T) {
l := &DefaultLayout{
Path: tempdir,
Join: filepath.Join,
}
dirs := l.Paths()
want := []string{
filepath.Join(path, "data"),
filepath.Join(path, "snapshots"),
filepath.Join(path, "index"),
filepath.Join(path, "locks"),
filepath.Join(path, "keys"),
filepath.Join(tempdir, "data"),
filepath.Join(tempdir, "snapshots"),
filepath.Join(tempdir, "index"),
filepath.Join(tempdir, "locks"),
filepath.Join(tempdir, "keys"),
}
sort.Sort(sort.StringSlice(want))
@@ -71,6 +121,11 @@ func TestDefaultLayout(t *testing.T) {
for _, test := range tests {
t.Run(fmt.Sprintf("%v/%v", test.Type, test.Handle.Name), func(t *testing.T) {
l := &DefaultLayout{
Path: test.path,
Join: test.join,
}
filename := l.Filename(test.Handle)
if filename != test.filename {
t.Fatalf("wrong filename, want %v, got %v", test.filename, filename)

View File

@@ -1,6 +1,7 @@
package local
import (
"context"
"path/filepath"
"restic"
. "restic/test"
@@ -47,7 +48,7 @@ func TestLayout(t *testing.T) {
}
datafiles := make(map[string]bool)
for id := range be.List(restic.DataFile, nil) {
for id := range be.List(context.TODO(), restic.DataFile) {
datafiles[id] = false
}

View File

@@ -1,6 +1,7 @@
package local
import (
"context"
"io"
"os"
"path/filepath"
@@ -74,8 +75,13 @@ func (b *Local) Location() string {
return b.Path
}
// IsNotExist returns true if the error is caused by a non existing file.
func (b *Local) IsNotExist(err error) bool {
return os.IsNotExist(errors.Cause(err))
}
// Save stores data in the backend at the handle.
func (b *Local) Save(h restic.Handle, rd io.Reader) (err error) {
func (b *Local) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
debug.Log("Save %v", h)
if err := h.Valid(); err != nil {
return err
@@ -100,7 +106,7 @@ func (b *Local) Save(h restic.Handle, rd io.Reader) (err error) {
return errors.Wrap(err, "MkdirAll")
}
return b.Save(h, rd)
return b.Save(ctx, h, rd)
}
if err != nil {
@@ -110,12 +116,12 @@ func (b *Local) Save(h restic.Handle, rd io.Reader) (err error) {
// save data, then sync
_, err = io.Copy(f, rd)
if err != nil {
f.Close()
_ = f.Close()
return errors.Wrap(err, "Write")
}
if err = f.Sync(); err != nil {
f.Close()
_ = f.Close()
return errors.Wrap(err, "Sync")
}
@@ -136,7 +142,7 @@ func (b *Local) Save(h restic.Handle, rd io.Reader) (err error) {
// Load returns a reader that yields the contents of the file at h at the
// given offset. If length is nonzero, only a portion of the file is
// returned. rd must be closed after use.
func (b *Local) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
func (b *Local) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
debug.Log("Load %v, length %v, offset %v", h, length, offset)
if err := h.Valid(); err != nil {
return nil, err
@@ -154,7 +160,7 @@ func (b *Local) Load(h restic.Handle, length int, offset int64) (io.ReadCloser,
if offset > 0 {
_, err = f.Seek(offset, 0)
if err != nil {
f.Close()
_ = f.Close()
return nil, err
}
}
@@ -167,7 +173,7 @@ func (b *Local) Load(h restic.Handle, length int, offset int64) (io.ReadCloser,
}
// Stat returns information about a blob.
func (b *Local) Stat(h restic.Handle) (restic.FileInfo, error) {
func (b *Local) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
debug.Log("Stat %v", h)
if err := h.Valid(); err != nil {
return restic.FileInfo{}, err
@@ -182,7 +188,7 @@ func (b *Local) Stat(h restic.Handle) (restic.FileInfo, error) {
}
// Test returns true if a blob of the given type and name exists in the backend.
func (b *Local) Test(h restic.Handle) (bool, error) {
func (b *Local) Test(ctx context.Context, h restic.Handle) (bool, error) {
debug.Log("Test %v", h)
_, err := fs.Stat(b.Filename(h))
if err != nil {
@@ -196,7 +202,7 @@ func (b *Local) Test(h restic.Handle) (bool, error) {
}
// Remove removes the blob with the given name and type.
func (b *Local) Remove(h restic.Handle) error {
func (b *Local) Remove(ctx context.Context, h restic.Handle) error {
debug.Log("Remove %v", h)
fn := b.Filename(h)
@@ -214,9 +220,8 @@ func isFile(fi os.FileInfo) bool {
}
// List returns a channel that yields all names of blobs of type t. A
// goroutine is started for this. If the channel done is closed, sending
// stops.
func (b *Local) List(t restic.FileType, done <-chan struct{}) <-chan string {
// goroutine is started for this.
func (b *Local) List(ctx context.Context, t restic.FileType) <-chan string {
debug.Log("List %v", t)
ch := make(chan string)
@@ -235,7 +240,7 @@ func (b *Local) List(t restic.FileType, done <-chan struct{}) <-chan string {
select {
case ch <- filepath.Base(path):
case <-done:
case <-ctx.Done():
return err
}

View File

@@ -4,10 +4,13 @@ package location
import (
"strings"
"restic/backend/b2"
"restic/backend/local"
"restic/backend/rest"
"restic/backend/s3"
"restic/backend/sftp"
"restic/backend/swift"
"restic/errors"
)
// Location specifies the location of a repository, including the method of
@@ -25,12 +28,44 @@ type parser struct {
// parsers is a list of valid config parsers for the backends. The first parser
// is the fallback and should always be set to the local backend.
var parsers = []parser{
{"b2", b2.ParseConfig},
{"local", local.ParseConfig},
{"sftp", sftp.ParseConfig},
{"s3", s3.ParseConfig},
{"swift", swift.ParseConfig},
{"rest", rest.ParseConfig},
}
func isPath(s string) bool {
if strings.HasPrefix(s, "../") || strings.HasPrefix(s, `..\`) {
return true
}
if strings.HasPrefix(s, "/") || strings.HasPrefix(s, `\`) {
return true
}
if len(s) < 3 {
return false
}
// check for drive paths
drive := s[0]
if !(drive >= 'a' && drive <= 'z') && !(drive >= 'A' && drive <= 'Z') {
return false
}
if s[1] != ':' {
return false
}
if s[2] != '\\' && s[2] != '/' {
return false
}
return true
}
// Parse extracts repository location information from the string s. If s
// starts with a backend name followed by a colon, that backend's Parse()
// function is called. Otherwise, the local backend is used which interprets s
@@ -52,7 +87,11 @@ func Parse(s string) (u Location, err error) {
return u, nil
}
// try again, with the local parser and the prefix "local:"
// if s is not a path or contains ":", it's ambiguous
if !isPath(s) && strings.ContainsRune(s, ':') {
return Location{}, errors.New("invalid backend\nIf the repo is in a local directory, you need to add a `local:` prefix")
}
u.Scheme = "local"
u.Config, err = local.ParseConfig("local:" + s)
if err != nil {

View File

@@ -5,10 +5,12 @@ import (
"reflect"
"testing"
"restic/backend/b2"
"restic/backend/local"
"restic/backend/rest"
"restic/backend/s3"
"restic/backend/sftp"
"restic/backend/swift"
)
func parseURL(s string) *url.URL {
@@ -56,6 +58,14 @@ var parseTests = []struct {
},
},
},
{
"/dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Path: "/dir1/dir2",
},
},
},
{
"local:../dir1/dir2",
Location{Scheme: "local",
@@ -72,7 +82,46 @@ var parseTests = []struct {
},
},
},
{
"/dir1:foobar/dir2",
Location{Scheme: "local",
Config: local.Config{
Path: "/dir1:foobar/dir2",
},
},
},
{
`\dir1\foobar\dir2`,
Location{Scheme: "local",
Config: local.Config{
Path: `\dir1\foobar\dir2`,
},
},
},
{
`c:\dir1\foobar\dir2`,
Location{Scheme: "local",
Config: local.Config{
Path: `c:\dir1\foobar\dir2`,
},
},
},
{
`C:\Users\appveyor\AppData\Local\Temp\1\restic-test-879453535\repo`,
Location{Scheme: "local",
Config: local.Config{
Path: `C:\Users\appveyor\AppData\Local\Temp\1\restic-test-879453535\repo`,
},
},
},
{
`c:/dir1/foobar/dir2`,
Location{Scheme: "local",
Config: local.Config{
Path: `c:/dir1/foobar/dir2`,
},
},
},
{
"sftp:user@host:/srv/repo",
Location{Scheme: "sftp",
@@ -118,9 +167,10 @@ var parseTests = []struct {
"s3://eu-central-1/bucketname",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "restic",
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "restic",
Connections: 5,
},
},
},
@@ -128,9 +178,10 @@ var parseTests = []struct {
"s3://hostname.foo/bucketname",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "restic",
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "restic",
Connections: 5,
},
},
},
@@ -138,9 +189,10 @@ var parseTests = []struct {
"s3://hostname.foo/bucketname/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "prefix/directory",
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
},
},
},
@@ -148,9 +200,10 @@ var parseTests = []struct {
"s3:eu-central-1/repo",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "restic",
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "restic",
Connections: 5,
},
},
},
@@ -158,9 +211,10 @@ var parseTests = []struct {
"s3:eu-central-1/repo/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "prefix/directory",
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "prefix/directory",
Connections: 5,
},
},
},
@@ -168,9 +222,10 @@ var parseTests = []struct {
"s3:https://hostname.foo/repo",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "restic",
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "restic",
Connections: 5,
},
},
},
@@ -178,9 +233,10 @@ var parseTests = []struct {
"s3:https://hostname.foo/repo/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "prefix/directory",
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "prefix/directory",
Connections: 5,
},
},
},
@@ -188,10 +244,31 @@ var parseTests = []struct {
"s3:http://hostname.foo/repo",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "restic",
UseHTTP: true,
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "restic",
UseHTTP: true,
Connections: 5,
},
},
},
{
"swift:container17:/",
Location{Scheme: "swift",
Config: swift.Config{
Container: "container17",
Prefix: "",
Connections: 5,
},
},
},
{
"swift:container17:/prefix97",
Location{Scheme: "swift",
Config: swift.Config{
Container: "container17",
Prefix: "prefix97",
Connections: 5,
},
},
},
@@ -199,7 +276,26 @@ var parseTests = []struct {
"rest:http://hostname.foo:1234/",
Location{Scheme: "rest",
Config: rest.Config{
URL: parseURL("http://hostname.foo:1234/"),
URL: parseURL("http://hostname.foo:1234/"),
Connections: 5,
},
},
},
{
"b2:bucketname:/prefix", Location{Scheme: "b2",
Config: b2.Config{
Bucket: "bucketname",
Prefix: "prefix",
Connections: 5,
},
},
},
{
"b2:bucketname", Location{Scheme: "b2",
Config: b2.Config{
Bucket: "bucketname",
Prefix: "",
Connections: 5,
},
},
},
@@ -225,3 +321,19 @@ func TestParse(t *testing.T) {
})
}
}
func TestInvalidScheme(t *testing.T) {
var invalidSchemes = []string{
"foobar:xxx",
"foobar:/dir/dir2",
}
for _, s := range invalidSchemes {
t.Run(s, func(t *testing.T) {
_, err := Parse(s)
if err == nil {
t.Fatalf("error for invalid location %q not found", s)
}
})
}
}

View File

@@ -2,12 +2,12 @@ package mem
import (
"bytes"
"context"
"io"
"io/ioutil"
"restic"
"sync"
"restic/backend"
"restic/errors"
"restic/debug"
@@ -18,6 +18,8 @@ type memMap map[restic.Handle][]byte
// make sure that MemoryBackend implements backend.Backend
var _ restic.Backend = &MemoryBackend{}
var errNotFound = errors.New("not found")
// MemoryBackend is a mock backend that uses a map for storing all data in
// memory. This should only be used for tests.
type MemoryBackend struct {
@@ -37,7 +39,7 @@ func New() *MemoryBackend {
}
// Test returns whether a file exists.
func (be *MemoryBackend) Test(h restic.Handle) (bool, error) {
func (be *MemoryBackend) Test(ctx context.Context, h restic.Handle) (bool, error) {
be.m.Lock()
defer be.m.Unlock()
@@ -50,8 +52,13 @@ func (be *MemoryBackend) Test(h restic.Handle) (bool, error) {
return false, nil
}
// IsNotExist returns true if the file does not exist.
func (be *MemoryBackend) IsNotExist(err error) bool {
return errors.Cause(err) == errNotFound
}
// Save adds new Data to the backend.
func (be *MemoryBackend) Save(h restic.Handle, rd io.Reader) error {
func (be *MemoryBackend) Save(ctx context.Context, h restic.Handle, rd io.Reader) error {
if err := h.Valid(); err != nil {
return err
}
@@ -81,7 +88,7 @@ func (be *MemoryBackend) Save(h restic.Handle, rd io.Reader) error {
// Load returns a reader that yields the contents of the file at h at the
// given offset. If length is nonzero, only a portion of the file is
// returned. rd must be closed after use.
func (be *MemoryBackend) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
func (be *MemoryBackend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
if err := h.Valid(); err != nil {
return nil, err
}
@@ -100,7 +107,7 @@ func (be *MemoryBackend) Load(h restic.Handle, length int, offset int64) (io.Rea
}
if _, ok := be.data[h]; !ok {
return nil, errors.New("no such data")
return nil, errNotFound
}
buf := be.data[h]
@@ -113,11 +120,11 @@ func (be *MemoryBackend) Load(h restic.Handle, length int, offset int64) (io.Rea
buf = buf[:length]
}
return backend.Closer{Reader: bytes.NewReader(buf)}, nil
return ioutil.NopCloser(bytes.NewReader(buf)), nil
}
// Stat returns information about a file in the backend.
func (be *MemoryBackend) Stat(h restic.Handle) (restic.FileInfo, error) {
func (be *MemoryBackend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
be.m.Lock()
defer be.m.Unlock()
@@ -133,21 +140,21 @@ func (be *MemoryBackend) Stat(h restic.Handle) (restic.FileInfo, error) {
e, ok := be.data[h]
if !ok {
return restic.FileInfo{}, errors.New("no such data")
return restic.FileInfo{}, errNotFound
}
return restic.FileInfo{Size: int64(len(e))}, nil
}
// Remove deletes a file from the backend.
func (be *MemoryBackend) Remove(h restic.Handle) error {
func (be *MemoryBackend) Remove(ctx context.Context, h restic.Handle) error {
be.m.Lock()
defer be.m.Unlock()
debug.Log("Remove %v", h)
if _, ok := be.data[h]; !ok {
return errors.New("no such data")
return errNotFound
}
delete(be.data, h)
@@ -156,7 +163,7 @@ func (be *MemoryBackend) Remove(h restic.Handle) error {
}
// List returns a channel which yields entries from the backend.
func (be *MemoryBackend) List(t restic.FileType, done <-chan struct{}) <-chan string {
func (be *MemoryBackend) List(ctx context.Context, t restic.FileType) <-chan string {
be.m.Lock()
defer be.m.Unlock()
@@ -177,7 +184,7 @@ func (be *MemoryBackend) List(t restic.FileType, done <-chan struct{}) <-chan st
for _, id := range ids {
select {
case ch <- id:
case <-done:
case <-ctx.Done():
return
}
}
@@ -192,7 +199,7 @@ func (be *MemoryBackend) Location() string {
}
// Delete removes all data in the backend.
func (be *MemoryBackend) Delete() error {
func (be *MemoryBackend) Delete(ctx context.Context) error {
be.m.Lock()
defer be.m.Unlock()

View File

@@ -1,6 +1,7 @@
package mem_test
import (
"context"
"restic"
"testing"
@@ -25,7 +26,7 @@ func newTestSuite() *test.Suite {
Create: func(cfg interface{}) (restic.Backend, error) {
c := cfg.(*memConfig)
if c.be != nil {
ok, err := c.be.Test(restic.Handle{Type: restic.ConfigFile})
ok, err := c.be.Test(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil {
return nil, err
}

View File

@@ -5,11 +5,24 @@ import (
"strings"
"restic/errors"
"restic/options"
)
// Config contains all configuration necessary to connect to a REST server.
type Config struct {
URL *url.URL
URL *url.URL
Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"`
}
func init() {
options.Register("rest", Config{})
}
// NewConfig returns a new Config with the default values filled in.
func NewConfig() Config {
return Config{
Connections: 5,
}
}
// ParseConfig parses the string s and extracts the REST server URL.
@@ -25,6 +38,7 @@ func ParseConfig(s string) (interface{}, error) {
return nil, errors.Wrap(err, "url.Parse")
}
cfg := Config{URL: u}
cfg := NewConfig()
cfg.URL = u
return cfg, nil
}

View File

@@ -20,7 +20,8 @@ var configTests = []struct {
cfg Config
}{
{"rest:http://localhost:1234", Config{
URL: parseURL("http://localhost:1234"),
URL: parseURL("http://localhost:1234"),
Connections: 5,
}},
}

View File

@@ -1,6 +1,7 @@
package rest
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -11,32 +12,32 @@ import (
"restic"
"strings"
"golang.org/x/net/context/ctxhttp"
"restic/debug"
"restic/errors"
"restic/backend"
)
const connLimit = 40
// make sure the rest backend implements restic.Backend
var _ restic.Backend = &restBackend{}
type restBackend struct {
url *url.URL
connChan chan struct{}
client http.Client
url *url.URL
sem *backend.Semaphore
client *http.Client
backend.Layout
}
// Open opens the REST backend with the given config.
func Open(cfg Config) (restic.Backend, error) {
connChan := make(chan struct{}, connLimit)
for i := 0; i < connLimit; i++ {
connChan <- struct{}{}
}
client := &http.Client{Transport: backend.Transport()}
client := http.Client{Transport: backend.Transport()}
sem, err := backend.NewSemaphore(cfg.Connections)
if err != nil {
return nil, err
}
// use url without trailing slash for layout
url := cfg.URL.String()
@@ -45,10 +46,10 @@ func Open(cfg Config) (restic.Backend, error) {
}
be := &restBackend{
url: cfg.URL,
connChan: connChan,
client: client,
Layout: &backend.RESTLayout{URL: url, Join: path.Join},
url: cfg.URL,
client: client,
Layout: &backend.RESTLayout{URL: url, Join: path.Join},
sem: sem,
}
return be, nil
@@ -61,7 +62,7 @@ func Create(cfg Config) (restic.Backend, error) {
return nil, err
}
_, err = be.Stat(restic.Handle{Type: restic.ConfigFile})
_, err = be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err == nil {
return nil, errors.Fatal("config file already exists")
}
@@ -99,22 +100,24 @@ func (b *restBackend) Location() string {
}
// Save stores data in the backend at the handle.
func (b *restBackend) Save(h restic.Handle, rd io.Reader) (err error) {
func (b *restBackend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
if err := h.Valid(); err != nil {
return err
}
// make sure that client.Post() cannot close the reader by wrapping it in
// backend.Closer, which has a noop method.
rd = backend.Closer{Reader: rd}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
<-b.connChan
resp, err := b.client.Post(b.Filename(h), "binary/octet-stream", rd)
b.connChan <- struct{}{}
// make sure that client.Post() cannot close the reader by wrapping it
rd = ioutil.NopCloser(rd)
b.sem.GetToken()
resp, err := ctxhttp.Post(ctx, b.client, b.Filename(h), "binary/octet-stream", rd)
b.sem.ReleaseToken()
if resp != nil {
defer func() {
io.Copy(ioutil.Discard, resp.Body)
_, _ = io.Copy(ioutil.Discard, resp.Body)
e := resp.Body.Close()
if err == nil {
@@ -127,18 +130,34 @@ func (b *restBackend) Save(h restic.Handle, rd io.Reader) (err error) {
return errors.Wrap(err, "client.Post")
}
// fmt.Printf("status is %v (%v)\n", resp.Status, resp.StatusCode)
if resp.StatusCode != 200 {
return errors.Errorf("unexpected HTTP response code %v", resp.StatusCode)
return errors.Errorf("server response unexpected: %v (%v)", resp.Status, resp.StatusCode)
}
return nil
}
// ErrIsNotExist is returned whenever the requested file does not exist on the
// server.
type ErrIsNotExist struct {
restic.Handle
}
func (e ErrIsNotExist) Error() string {
return fmt.Sprintf("%v does not exist", e.Handle)
}
// IsNotExist returns true if the error was caused by a non-existing file.
func (b *restBackend) IsNotExist(err error) bool {
err = errors.Cause(err)
_, ok := err.(ErrIsNotExist)
return ok
}
// Load returns a reader that yields the contents of the file at h at the
// given offset. If length is nonzero, only a portion of the file is
// returned. rd must be closed after use.
func (b *restBackend) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
func (b *restBackend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
debug.Log("Load %v, length %v, offset %v", h, length, offset)
if err := h.Valid(); err != nil {
return nil, err
@@ -164,47 +183,56 @@ func (b *restBackend) Load(h restic.Handle, length int, offset int64) (io.ReadCl
req.Header.Add("Range", byteRange)
debug.Log("Load(%v) send range %v", h, byteRange)
<-b.connChan
resp, err := b.client.Do(req)
b.connChan <- struct{}{}
b.sem.GetToken()
resp, err := ctxhttp.Do(ctx, b.client, req)
b.sem.ReleaseToken()
if err != nil {
if resp != nil {
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
_, _ = io.Copy(ioutil.Discard, resp.Body)
_ = resp.Body.Close()
}
return nil, errors.Wrap(err, "client.Do")
}
if resp.StatusCode == http.StatusNotFound {
_ = resp.Body.Close()
return nil, ErrIsNotExist{h}
}
if resp.StatusCode != 200 && resp.StatusCode != 206 {
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
return nil, errors.Errorf("unexpected HTTP response code %v", resp.StatusCode)
_ = resp.Body.Close()
return nil, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status)
}
return resp.Body, nil
}
// Stat returns information about a blob.
func (b *restBackend) Stat(h restic.Handle) (restic.FileInfo, error) {
func (b *restBackend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
if err := h.Valid(); err != nil {
return restic.FileInfo{}, err
}
<-b.connChan
resp, err := b.client.Head(b.Filename(h))
b.connChan <- struct{}{}
b.sem.GetToken()
resp, err := ctxhttp.Head(ctx, b.client, b.Filename(h))
b.sem.ReleaseToken()
if err != nil {
return restic.FileInfo{}, errors.Wrap(err, "client.Head")
}
io.Copy(ioutil.Discard, resp.Body)
_, _ = io.Copy(ioutil.Discard, resp.Body)
if err = resp.Body.Close(); err != nil {
return restic.FileInfo{}, errors.Wrap(err, "Close")
}
if resp.StatusCode == http.StatusNotFound {
_ = resp.Body.Close()
return restic.FileInfo{}, ErrIsNotExist{h}
}
if resp.StatusCode != 200 {
return restic.FileInfo{}, errors.Errorf("unexpected HTTP response code %v", resp.StatusCode)
return restic.FileInfo{}, errors.Errorf("unexpected HTTP response (%v): %v", resp.StatusCode, resp.Status)
}
if resp.ContentLength < 0 {
@@ -219,8 +247,8 @@ func (b *restBackend) Stat(h restic.Handle) (restic.FileInfo, error) {
}
// Test returns true if a blob of the given type and name exists in the backend.
func (b *restBackend) Test(h restic.Handle) (bool, error) {
_, err := b.Stat(h)
func (b *restBackend) Test(ctx context.Context, h restic.Handle) (bool, error) {
_, err := b.Stat(ctx, h)
if err != nil {
return false, nil
}
@@ -229,7 +257,7 @@ func (b *restBackend) Test(h restic.Handle) (bool, error) {
}
// Remove removes the blob with the given name and type.
func (b *restBackend) Remove(h restic.Handle) error {
func (b *restBackend) Remove(ctx context.Context, h restic.Handle) error {
if err := h.Valid(); err != nil {
return err
}
@@ -238,26 +266,35 @@ func (b *restBackend) Remove(h restic.Handle) error {
if err != nil {
return errors.Wrap(err, "http.NewRequest")
}
<-b.connChan
resp, err := b.client.Do(req)
b.connChan <- struct{}{}
b.sem.GetToken()
resp, err := ctxhttp.Do(ctx, b.client, req)
b.sem.ReleaseToken()
if err != nil {
return errors.Wrap(err, "client.Do")
}
if resp.StatusCode == http.StatusNotFound {
_ = resp.Body.Close()
return ErrIsNotExist{h}
}
if resp.StatusCode != 200 {
return errors.Errorf("blob not removed, server response: %v (%v)", resp.Status, resp.StatusCode)
}
io.Copy(ioutil.Discard, resp.Body)
return resp.Body.Close()
_, err = io.Copy(ioutil.Discard, resp.Body)
if err != nil {
return errors.Wrap(err, "Copy")
}
return errors.Wrap(resp.Body.Close(), "Close")
}
// List returns a channel that yields all names of blobs of type t. A
// goroutine is started for this. If the channel done is closed, sending
// stops.
func (b *restBackend) List(t restic.FileType, done <-chan struct{}) <-chan string {
func (b *restBackend) List(ctx context.Context, t restic.FileType) <-chan string {
ch := make(chan string)
url := b.Dirname(restic.Handle{Type: t})
@@ -265,13 +302,13 @@ func (b *restBackend) List(t restic.FileType, done <-chan struct{}) <-chan strin
url += "/"
}
<-b.connChan
resp, err := b.client.Get(url)
b.connChan <- struct{}{}
b.sem.GetToken()
resp, err := ctxhttp.Get(ctx, b.client, url)
b.sem.ReleaseToken()
if resp != nil {
defer func() {
io.Copy(ioutil.Discard, resp.Body)
_, _ = io.Copy(ioutil.Discard, resp.Body)
e := resp.Body.Close()
if err == nil {
@@ -297,7 +334,7 @@ func (b *restBackend) List(t restic.FileType, done <-chan struct{}) <-chan strin
for _, m := range list {
select {
case ch <- m:
case <-done:
case <-ctx.Done():
return
}
}

View File

@@ -76,9 +76,8 @@ func newTestSuite(ctx context.Context, t testing.TB) *test.Suite {
t.Fatal(err)
}
cfg := rest.Config{
URL: url,
}
cfg := rest.NewConfig()
cfg.URL = url
return cfg, nil
},

View File

@@ -18,6 +18,16 @@ type Config struct {
Bucket string
Prefix string
Layout string `option:"layout" help:"use this backend layout (default: auto-detect)"`
Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"`
MaxRetries uint `option:"retries" help:"set the number of retries attempted"`
}
// NewConfig returns a new Config with the default values filled in.
func NewConfig() Config {
return Config{
Connections: 5,
}
}
func init() {
@@ -70,10 +80,10 @@ func createConfig(endpoint string, p []string, useHTTP bool) (interface{}, error
default:
prefix = path.Clean(p[1])
}
return Config{
Endpoint: endpoint,
UseHTTP: useHTTP,
Bucket: p[0],
Prefix: prefix,
}, nil
cfg := NewConfig()
cfg.Endpoint = endpoint
cfg.UseHTTP = useHTTP
cfg.Bucket = p[0]
cfg.Prefix = prefix
return cfg, nil
}

View File

@@ -7,78 +7,92 @@ var configTests = []struct {
cfg Config
}{
{"s3://eu-central-1/bucketname", Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "restic",
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "restic",
Connections: 5,
}},
{"s3://eu-central-1/bucketname/", Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "restic",
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "restic",
Connections: 5,
}},
{"s3://eu-central-1/bucketname/prefix/directory", Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "prefix/directory",
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
}},
{"s3://eu-central-1/bucketname/prefix/directory/", Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "prefix/directory",
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "prefix/directory",
Connections: 5,
}},
{"s3:eu-central-1/foobar", Config{
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "restic",
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "restic",
Connections: 5,
}},
{"s3:eu-central-1/foobar/", Config{
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "restic",
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "restic",
Connections: 5,
}},
{"s3:eu-central-1/foobar/prefix/directory", Config{
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "prefix/directory",
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "prefix/directory",
Connections: 5,
}},
{"s3:eu-central-1/foobar/prefix/directory/", Config{
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "prefix/directory",
Endpoint: "eu-central-1",
Bucket: "foobar",
Prefix: "prefix/directory",
Connections: 5,
}},
{"s3:https://hostname:9999/foobar", Config{
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "restic",
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "restic",
Connections: 5,
}},
{"s3:https://hostname:9999/foobar/", Config{
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "restic",
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "restic",
Connections: 5,
}},
{"s3:http://hostname:9999/foobar", Config{
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "restic",
UseHTTP: true,
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "restic",
UseHTTP: true,
Connections: 5,
}},
{"s3:http://hostname:9999/foobar/", Config{
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "restic",
UseHTTP: true,
Endpoint: "hostname:9999",
Bucket: "foobar",
Prefix: "restic",
UseHTTP: true,
Connections: 5,
}},
{"s3:http://hostname:9999/bucket/prefix/directory", Config{
Endpoint: "hostname:9999",
Bucket: "bucket",
Prefix: "prefix/directory",
UseHTTP: true,
Endpoint: "hostname:9999",
Bucket: "bucket",
Prefix: "prefix/directory",
UseHTTP: true,
Connections: 5,
}},
{"s3:http://hostname:9999/bucket/prefix/directory/", Config{
Endpoint: "hostname:9999",
Bucket: "bucket",
Prefix: "prefix/directory",
UseHTTP: true,
Endpoint: "hostname:9999",
Bucket: "bucket",
Prefix: "prefix/directory",
UseHTTP: true,
Connections: 5,
}},
}

View File

@@ -1,13 +1,13 @@
package s3
import (
"context"
"fmt"
"io"
"os"
"path"
"restic"
"strings"
"sync"
"time"
"restic/backend"
@@ -18,36 +18,40 @@ import (
"restic/debug"
)
const connLimit = 10
// s3 is a backend which stores the data on an S3 endpoint.
type s3 struct {
client *minio.Client
connChan chan struct{}
bucketname string
prefix string
cacheMutex sync.RWMutex
cacheObjSize map[string]int64
// Backend stores data on an S3 endpoint.
type Backend struct {
client *minio.Client
sem *backend.Semaphore
cfg Config
backend.Layout
}
const defaultLayout = "s3legacy"
// make sure that *Backend implements backend.Backend
var _ restic.Backend = &Backend{}
// Open opens the S3 backend at bucket and region. The bucket is created if it
// does not exist yet.
func Open(cfg Config) (restic.Backend, error) {
const defaultLayout = "default"
func open(cfg Config) (*Backend, error) {
debug.Log("open, config %#v", cfg)
if cfg.MaxRetries > 0 {
minio.MaxRetry = int(cfg.MaxRetries)
}
client, err := minio.New(cfg.Endpoint, cfg.KeyID, cfg.Secret, !cfg.UseHTTP)
if err != nil {
return nil, errors.Wrap(err, "minio.New")
}
be := &s3{
client: client,
bucketname: cfg.Bucket,
prefix: cfg.Prefix,
cacheObjSize: make(map[string]int64),
sem, err := backend.NewSemaphore(cfg.Connections)
if err != nil {
return nil, err
}
be := &Backend{
client: client,
sem: sem,
cfg: cfg,
}
client.SetCustomTransport(backend.Transport())
@@ -59,9 +63,20 @@ func Open(cfg Config) (restic.Backend, error) {
be.Layout = l
be.createConnections()
return be, nil
}
found, err := client.BucketExists(cfg.Bucket)
// Open opens the S3 backend at bucket and region. The bucket is created if it
// does not exist yet.
func Open(cfg Config) (restic.Backend, error) {
return open(cfg)
}
// Create opens the S3 backend at bucket and region and creates the bucket if
// it does not exist yet.
func Create(cfg Config) (restic.Backend, error) {
be, err := open(cfg)
found, err := be.client.BucketExists(cfg.Bucket)
if err != nil {
debug.Log("BucketExists(%v) returned err %v", cfg.Bucket, err)
return nil, errors.Wrap(err, "client.BucketExists")
@@ -69,7 +84,7 @@ func Open(cfg Config) (restic.Backend, error) {
if !found {
// create new bucket with default ACL in default region
err = client.MakeBucket(cfg.Bucket, "")
err = be.client.MakeBucket(cfg.Bucket, "")
if err != nil {
return nil, errors.Wrap(err, "client.MakeBucket")
}
@@ -78,17 +93,14 @@ func Open(cfg Config) (restic.Backend, error) {
return be, nil
}
func (be *s3) createConnections() {
be.connChan = make(chan struct{}, connLimit)
for i := 0; i < connLimit; i++ {
be.connChan <- struct{}{}
}
}
// IsNotExist returns true if the error is caused by a not existing file.
func (be *s3) IsNotExist(err error) bool {
func (be *Backend) IsNotExist(err error) bool {
debug.Log("IsNotExist(%T, %#v)", err, err)
if os.IsNotExist(err) {
if os.IsNotExist(errors.Cause(err)) {
return true
}
if e, ok := errors.Cause(err).(minio.ErrorResponse); ok && e.Code == "NoSuchKey" {
return true
}
@@ -96,7 +108,7 @@ func (be *s3) IsNotExist(err error) bool {
}
// Join combines path components with slashes.
func (be *s3) Join(p ...string) string {
func (be *Backend) Join(p ...string) string {
return path.Join(p...)
}
@@ -116,7 +128,7 @@ func (fi fileInfo) IsDir() bool { return fi.isDir } // abbreviation for
func (fi fileInfo) Sys() interface{} { return nil } // underlying data source (can return nil)
// ReadDir returns the entries for a directory.
func (be *s3) ReadDir(dir string) (list []os.FileInfo, err error) {
func (be *Backend) ReadDir(dir string) (list []os.FileInfo, err error) {
debug.Log("ReadDir(%v)", dir)
// make sure dir ends with a slash
@@ -127,7 +139,7 @@ func (be *s3) ReadDir(dir string) (list []os.FileInfo, err error) {
done := make(chan struct{})
defer close(done)
for obj := range be.client.ListObjects(be.bucketname, dir, false, done) {
for obj := range be.client.ListObjects(be.cfg.Bucket, dir, false, done) {
if obj.Key == "" {
continue
}
@@ -157,93 +169,96 @@ func (be *s3) ReadDir(dir string) (list []os.FileInfo, err error) {
}
// Location returns this backend's location (the bucket name).
func (be *s3) Location() string {
return be.bucketname
func (be *Backend) Location() string {
return be.Join(be.cfg.Bucket, be.cfg.Prefix)
}
// getRemainingSize returns number of bytes remaining. If it is not possible to
// determine the size, panic() is called.
func getRemainingSize(rd io.Reader) (size int64, err error) {
type Sizer interface {
Size() int64
}
type Lenner interface {
Len() int
}
if r, ok := rd.(Lenner); ok {
size = int64(r.Len())
} else if r, ok := rd.(Sizer); ok {
size = r.Size()
} else if f, ok := rd.(*os.File); ok {
fi, err := f.Stat()
if err != nil {
return 0, err
}
pos, err := f.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
size = fi.Size() - pos
} else {
panic(fmt.Sprintf("Save() got passed a reader without a method to determine the data size, type is %T", rd))
}
return size, nil
// Path returns the path in the bucket that is used for this backend.
func (be *Backend) Path() string {
return be.cfg.Prefix
}
// preventCloser wraps an io.Reader to run a function instead of the original Close() function.
type preventCloser struct {
// nopCloserFile wraps *os.File and overwrites the Close() method with method
// that does nothing. In addition, the method Len() is implemented, which
// returns the size of the file (filesize - current offset).
type nopCloserFile struct {
*os.File
}
func (f nopCloserFile) Close() error {
debug.Log("prevented Close()")
return nil
}
// Len returns the remaining length of the file (filesize - current offset).
func (f nopCloserFile) Len() int {
debug.Log("Len() called")
fi, err := f.Stat()
if err != nil {
panic(err)
}
pos, err := f.Seek(0, io.SeekCurrent)
if err != nil {
panic(err)
}
size := fi.Size() - pos
debug.Log("returning file size %v", size)
return int(size)
}
type lenner interface {
Len() int
io.Reader
f func()
}
func (wr preventCloser) Close() error {
wr.f()
// nopCloserLenner wraps a lenner and overwrites the Close() method with method
// that does nothing. In addition, the method Size() is implemented, which
// returns the size of the file (filesize - current offset).
type nopCloserLenner struct {
lenner
}
func (f *nopCloserLenner) Close() error {
debug.Log("prevented Close()")
return nil
}
// Save stores data in the backend at the handle.
func (be *s3) Save(h restic.Handle, rd io.Reader) (err error) {
func (be *Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
debug.Log("Save %v", h)
if err := h.Valid(); err != nil {
return err
}
objName := be.Filename(h)
size, err := getRemainingSize(rd)
if err != nil {
return err
}
debug.Log("Save %v at %v", h, objName)
// Check key does not already exist
_, err = be.client.StatObject(be.bucketname, objName)
_, err = be.client.StatObject(be.cfg.Bucket, objName)
if err == nil {
debug.Log("%v already exists", h)
return errors.New("key already exists")
}
<-be.connChan
// wrap the reader so that net/http client cannot close the reader, return
// the token instead.
rd = preventCloser{
Reader: rd,
f: func() {
debug.Log("Close()")
},
// prevent the HTTP client from closing a file
if f, ok := rd.(*os.File); ok {
debug.Log("reader is %#T, using nopCloserFile{}", rd)
rd = nopCloserFile{f}
} else if l, ok := rd.(lenner); ok {
debug.Log("reader is %#T, using nopCloserLenner{}", rd)
rd = nopCloserLenner{l}
} else {
debug.Log("reader is %#T, no specific workaround enabled", rd)
}
debug.Log("PutObject(%v, %v)", be.bucketname, objName)
coreClient := minio.Core{be.client}
info, err := coreClient.PutObject(be.bucketname, objName, size, rd, nil, nil, nil)
be.sem.GetToken()
debug.Log("PutObject(%v, %v)", be.cfg.Bucket, objName)
n, err := be.client.PutObject(be.cfg.Bucket, objName, rd, "application/octet-stream")
be.sem.ReleaseToken()
// return token
be.connChan <- struct{}{}
debug.Log("%v -> %v bytes, err %#v", objName, info.Size, err)
debug.Log("%v -> %v bytes, err %#v: %v", objName, n, err, err)
return errors.Wrap(err, "client.PutObject")
}
@@ -263,7 +278,7 @@ func (wr wrapReader) Close() error {
// Load returns a reader that yields the contents of the file at h at the
// given offset. If length is nonzero, only a portion of the file is
// returned. rd must be closed after use.
func (be *s3) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h))
if err := h.Valid(); err != nil {
return nil, err
@@ -279,22 +294,20 @@ func (be *s3) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, er
objName := be.Filename(h)
// get token for connection
<-be.connChan
byteRange := fmt.Sprintf("bytes=%d-", offset)
if length > 0 {
byteRange = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1)
}
headers := minio.NewGetReqHeaders()
headers.Add("Range", byteRange)
be.sem.GetToken()
debug.Log("Load(%v) send range %v", h, byteRange)
coreClient := minio.Core{be.client}
rd, _, err := coreClient.GetObject(be.bucketname, objName, headers)
coreClient := minio.Core{Client: be.client}
rd, _, err := coreClient.GetObject(be.cfg.Bucket, objName, headers)
if err != nil {
// return token
be.connChan <- struct{}{}
be.sem.ReleaseToken()
return nil, err
}
@@ -302,8 +315,7 @@ func (be *s3) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, er
ReadCloser: rd,
f: func() {
debug.Log("Close()")
// return token
be.connChan <- struct{}{}
be.sem.ReleaseToken()
},
}
@@ -311,13 +323,13 @@ func (be *s3) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, er
}
// Stat returns information about a blob.
func (be *s3) Stat(h restic.Handle) (bi restic.FileInfo, err error) {
func (be *Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) {
debug.Log("%v", h)
objName := be.Filename(h)
var obj *minio.Object
obj, err = be.client.GetObject(be.bucketname, objName)
obj, err = be.client.GetObject(be.cfg.Bucket, objName)
if err != nil {
debug.Log("GetObject() err %v", err)
return restic.FileInfo{}, errors.Wrap(err, "client.GetObject")
@@ -341,10 +353,10 @@ func (be *s3) Stat(h restic.Handle) (bi restic.FileInfo, err error) {
}
// Test returns true if a blob of the given type and name exists in the backend.
func (be *s3) Test(h restic.Handle) (bool, error) {
func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
found := false
objName := be.Filename(h)
_, err := be.client.StatObject(be.bucketname, objName)
_, err := be.client.StatObject(be.cfg.Bucket, objName)
if err == nil {
found = true
}
@@ -354,17 +366,22 @@ func (be *s3) Test(h restic.Handle) (bool, error) {
}
// Remove removes the blob with the given name and type.
func (be *s3) Remove(h restic.Handle) error {
func (be *Backend) Remove(ctx context.Context, h restic.Handle) error {
objName := be.Filename(h)
err := be.client.RemoveObject(be.bucketname, objName)
err := be.client.RemoveObject(be.cfg.Bucket, objName)
debug.Log("Remove(%v) at %v -> err %v", h, objName, err)
if be.IsNotExist(err) {
err = nil
}
return errors.Wrap(err, "client.RemoveObject")
}
// List returns a channel that yields all names of blobs of type t. A
// goroutine is started for this. If the channel done is closed, sending
// stops.
func (be *s3) List(t restic.FileType, done <-chan struct{}) <-chan string {
func (be *Backend) List(ctx context.Context, t restic.FileType) <-chan string {
debug.Log("listing %v", t)
ch := make(chan string)
@@ -375,7 +392,7 @@ func (be *s3) List(t restic.FileType, done <-chan struct{}) <-chan string {
prefix += "/"
}
listresp := be.client.ListObjects(be.bucketname, prefix, true, done)
listresp := be.client.ListObjects(be.cfg.Bucket, prefix, true, ctx.Done())
go func() {
defer close(ch)
@@ -386,8 +403,8 @@ func (be *s3) List(t restic.FileType, done <-chan struct{}) <-chan string {
}
select {
case ch <- m:
case <-done:
case ch <- path.Base(m):
case <-ctx.Done():
return
}
}
@@ -397,11 +414,9 @@ func (be *s3) List(t restic.FileType, done <-chan struct{}) <-chan string {
}
// Remove keys for a specified backend type.
func (be *s3) removeKeys(t restic.FileType) error {
done := make(chan struct{})
defer close(done)
for key := range be.List(restic.DataFile, done) {
err := be.Remove(restic.Handle{Type: restic.DataFile, Name: key})
func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error {
for key := range be.List(ctx, restic.DataFile) {
err := be.Remove(ctx, restic.Handle{Type: restic.DataFile, Name: key})
if err != nil {
return err
}
@@ -411,7 +426,7 @@ func (be *s3) removeKeys(t restic.FileType) error {
}
// Delete removes all restic keys in the bucket. It will not remove the bucket itself.
func (be *s3) Delete() error {
func (be *Backend) Delete(ctx context.Context) error {
alltypes := []restic.FileType{
restic.DataFile,
restic.KeyFile,
@@ -420,14 +435,32 @@ func (be *s3) Delete() error {
restic.IndexFile}
for _, t := range alltypes {
err := be.removeKeys(t)
err := be.removeKeys(ctx, t)
if err != nil {
return nil
}
}
return be.Remove(restic.Handle{Type: restic.ConfigFile})
return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
}
// Close does nothing
func (be *s3) Close() error { return nil }
func (be *Backend) Close() error { return nil }
// Rename moves a file based on the new layout l.
func (be *Backend) Rename(h restic.Handle, l backend.Layout) error {
debug.Log("Rename %v to %v", h, l)
oldname := be.Filename(h)
newname := l.Filename(h)
debug.Log(" %v -> %v", oldname, newname)
coreClient := minio.Core{Client: be.client}
err := coreClient.CopyObject(be.cfg.Bucket, newname, path.Join(be.cfg.Bucket, oldname), minio.CopyConditions{})
if err != nil {
debug.Log("copy failed: %v", err)
return err
}
return be.client.RemoveObject(be.cfg.Bucket, oldname)
}

View File

@@ -1,71 +0,0 @@
package s3
import (
"bytes"
"io"
"io/ioutil"
"os"
"restic/test"
"testing"
)
func writeFile(t testing.TB, data []byte, offset int64) *os.File {
tempfile, err := ioutil.TempFile("", "restic-test-")
if err != nil {
t.Fatal(err)
}
if _, err = tempfile.Write(data); err != nil {
t.Fatal(err)
}
if _, err = tempfile.Seek(offset, io.SeekStart); err != nil {
t.Fatal(err)
}
return tempfile
}
func TestGetRemainingSize(t *testing.T) {
length := 18 * 1123
partialRead := 1005
data := test.Random(23, length)
partReader := bytes.NewReader(data)
buf := make([]byte, partialRead)
_, _ = io.ReadFull(partReader, buf)
partFileReader := writeFile(t, data, int64(partialRead))
defer func() {
if err := partFileReader.Close(); err != nil {
t.Fatal(err)
}
if err := os.Remove(partFileReader.Name()); err != nil {
t.Fatal(err)
}
}()
var tests = []struct {
io.Reader
size int64
}{
{bytes.NewReader([]byte("foobar test")), 11},
{partReader, int64(length - partialRead)},
{partFileReader, int64(length - partialRead)},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
size, err := getRemainingSize(test.Reader)
if err != nil {
t.Fatal(err)
}
if size != test.size {
t.Fatalf("invalid size returned, want %v, got %v", test.size, size)
}
})
}
}

View File

@@ -49,17 +49,16 @@ func runMinio(ctx context.Context, t testing.TB, dir, key, secret string) func()
// wait until the TCP port is reachable
var success bool
for i := 0; i < 10; i++ {
for i := 0; i < 100; i++ {
time.Sleep(200 * time.Millisecond)
c, err := net.Dial("tcp", "localhost:9000")
if err != nil {
continue
}
success = true
if err := c.Close(); err != nil {
t.Fatal(err)
if err == nil {
success = true
if err := c.Close(); err != nil {
t.Fatal(err)
}
break
}
}
@@ -104,6 +103,21 @@ type MinioTestConfig struct {
stopServer func()
}
func createS3(t testing.TB, cfg MinioTestConfig) (be restic.Backend, err error) {
for i := 0; i < 10; i++ {
be, err = s3.Create(cfg.Config)
if err != nil {
t.Logf("s3 open: try %d: error %v", i, err)
time.Sleep(500 * time.Millisecond)
continue
}
break
}
return be, err
}
func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
return &test.Suite{
// NewConfig returns a config for a new temporary backend that will be used in tests.
@@ -114,14 +128,13 @@ func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
key, secret := newRandomCredentials(t)
cfg.stopServer = runMinio(ctx, t, cfg.tempdir, key, secret)
cfg.Config = s3.Config{
Endpoint: "localhost:9000",
Bucket: "restictestbucket",
Prefix: fmt.Sprintf("test-%d", time.Now().UnixNano()),
UseHTTP: true,
KeyID: key,
Secret: secret,
}
cfg.Config = s3.NewConfig()
cfg.Config.Endpoint = "localhost:9000"
cfg.Config.Bucket = "restictestbucket"
cfg.Config.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
cfg.Config.UseHTTP = true
cfg.Config.KeyID = key
cfg.Config.Secret = secret
return cfg, nil
},
@@ -129,12 +142,12 @@ func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(MinioTestConfig)
be, err := s3.Open(cfg.Config)
be, err := createS3(t, cfg)
if err != nil {
return nil, err
}
exists, err := be.Test(restic.Handle{Type: restic.ConfigFile})
exists, err := be.Test(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil {
return nil, err
}
@@ -223,12 +236,12 @@ func newS3TestSuite(t testing.TB) *test.Suite {
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(s3.Config)
be, err := s3.Open(cfg)
be, err := s3.Create(cfg)
if err != nil {
return nil, err
}
exists, err := be.Test(restic.Handle{Type: restic.ConfigFile})
exists, err := be.Test(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil {
return nil, err
}
@@ -255,7 +268,7 @@ func newS3TestSuite(t testing.TB) *test.Suite {
return err
}
if err := be.(restic.Deleter).Delete(); err != nil {
if err := be.(restic.Deleter).Delete(context.TODO()); err != nil {
return err
}

View File

@@ -0,0 +1,28 @@
package backend
import "restic/errors"
// Semaphore limits access to a restricted resource.
type Semaphore struct {
ch chan struct{}
}
// NewSemaphore returns a new semaphore with capacity n.
func NewSemaphore(n uint) (*Semaphore, error) {
if n <= 0 {
return nil, errors.New("must be a positive number")
}
return &Semaphore{
ch: make(chan struct{}, n),
}, nil
}
// GetToken blocks until a Token is available.
func (s *Semaphore) GetToken() {
s.ch <- struct{}{}
}
// ReleaseToken returns a token.
func (s *Semaphore) ReleaseToken() {
<-s.ch
}

View File

@@ -1,6 +1,7 @@
package sftp_test
import (
"context"
"fmt"
"path/filepath"
"restic"
@@ -54,7 +55,7 @@ func TestLayout(t *testing.T) {
}
datafiles := make(map[string]bool)
for id := range be.List(restic.DataFile, nil) {
for id := range be.List(context.TODO(), restic.DataFile) {
datafiles[id] = false
}

View File

@@ -2,12 +2,12 @@ package sftp
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"restic"
"strings"
"time"
@@ -263,7 +263,7 @@ func Join(parts ...string) string {
}
// Save stores data in the backend at the handle.
func (r *SFTP) Save(h restic.Handle, rd io.Reader) (err error) {
func (r *SFTP) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
debug.Log("Save %v", h)
if err := r.clientError(); err != nil {
return err
@@ -284,7 +284,7 @@ func (r *SFTP) Save(h restic.Handle, rd io.Reader) (err error) {
return errors.Wrap(err, "MkdirAll")
}
return r.Save(h, rd)
return r.Save(ctx, h, rd)
}
if err != nil {
@@ -316,7 +316,7 @@ func (r *SFTP) Save(h restic.Handle, rd io.Reader) (err error) {
// Load returns a reader that yields the contents of the file at h at the
// given offset. If length is nonzero, only a portion of the file is
// returned. rd must be closed after use.
func (r *SFTP) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
func (r *SFTP) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
debug.Log("Load %v, length %v, offset %v", h, length, offset)
if err := h.Valid(); err != nil {
return nil, err
@@ -347,7 +347,7 @@ func (r *SFTP) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, e
}
// Stat returns information about a blob.
func (r *SFTP) Stat(h restic.Handle) (restic.FileInfo, error) {
func (r *SFTP) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
debug.Log("Stat(%v)", h)
if err := r.clientError(); err != nil {
return restic.FileInfo{}, err
@@ -366,7 +366,7 @@ func (r *SFTP) Stat(h restic.Handle) (restic.FileInfo, error) {
}
// Test returns true if a blob of the given type and name exists in the backend.
func (r *SFTP) Test(h restic.Handle) (bool, error) {
func (r *SFTP) Test(ctx context.Context, h restic.Handle) (bool, error) {
debug.Log("Test(%v)", h)
if err := r.clientError(); err != nil {
return false, err
@@ -385,7 +385,7 @@ func (r *SFTP) Test(h restic.Handle) (bool, error) {
}
// Remove removes the content stored at name.
func (r *SFTP) Remove(h restic.Handle) error {
func (r *SFTP) Remove(ctx context.Context, h restic.Handle) error {
debug.Log("Remove(%v)", h)
if err := r.clientError(); err != nil {
return err
@@ -397,7 +397,7 @@ func (r *SFTP) Remove(h restic.Handle) error {
// List returns a channel that yields all names of blobs of type t. A
// goroutine is started for this. If the channel done is closed, sending
// stops.
func (r *SFTP) List(t restic.FileType, done <-chan struct{}) <-chan string {
func (r *SFTP) List(ctx context.Context, t restic.FileType) <-chan string {
debug.Log("List %v", t)
ch := make(chan string)
@@ -416,8 +416,8 @@ func (r *SFTP) List(t restic.FileType, done <-chan struct{}) <-chan string {
}
select {
case ch <- filepath.Base(walker.Path()):
case <-done:
case ch <- path.Base(walker.Path()):
case <-ctx.Done():
return
}
}

View File

@@ -0,0 +1,109 @@
package swift
import (
"os"
"restic/errors"
"restic/options"
"strings"
)
// Config contains basic configuration needed to specify swift location for a swift server
type Config struct {
UserName string
Domain string
APIKey string
AuthURL string
Region string
Tenant string
TenantID string
TenantDomain string
TrustID string
StorageURL string
AuthToken string
Container string
Prefix string
DefaultContainerPolicy string
Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"`
}
func init() {
options.Register("swift", Config{})
}
// NewConfig returns a new config with the default values filled in.
func NewConfig() Config {
return Config{
Connections: 5,
}
}
// ParseConfig parses the string s and extract swift's container name and prefix.
func ParseConfig(s string) (interface{}, error) {
data := strings.SplitN(s, ":", 3)
if len(data) != 3 {
return nil, errors.New("invalid URL, expected: swift:container-name:/[prefix]")
}
scheme, container, prefix := data[0], data[1], data[2]
if scheme != "swift" {
return nil, errors.Errorf("unexpected prefix: %s", data[0])
}
if len(prefix) == 0 {
return nil, errors.Errorf("prefix is empty")
}
if prefix[0] != '/' {
return nil, errors.Errorf("prefix does not start with slash (/)")
}
prefix = prefix[1:]
cfg := NewConfig()
cfg.Container = container
cfg.Prefix = prefix
return cfg, nil
}
// ApplyEnvironment saves values from the environment to the config.
func ApplyEnvironment(prefix string, cfg interface{}) error {
c := cfg.(*Config)
for _, val := range []struct {
s *string
env string
}{
// v2/v3 specific
{&c.UserName, prefix + "OS_USERNAME"},
{&c.APIKey, prefix + "OS_PASSWORD"},
{&c.Region, prefix + "OS_REGION_NAME"},
{&c.AuthURL, prefix + "OS_AUTH_URL"},
// v3 specific
{&c.Domain, prefix + "OS_USER_DOMAIN_NAME"},
{&c.Tenant, prefix + "OS_PROJECT_NAME"},
{&c.TenantDomain, prefix + "OS_PROJECT_DOMAIN_NAME"},
// v2 specific
{&c.TenantID, prefix + "OS_TENANT_ID"},
{&c.Tenant, prefix + "OS_TENANT_NAME"},
// v1 specific
{&c.AuthURL, prefix + "ST_AUTH"},
{&c.UserName, prefix + "ST_USER"},
{&c.APIKey, prefix + "ST_KEY"},
// Manual authentication
{&c.StorageURL, prefix + "OS_STORAGE_URL"},
{&c.AuthToken, prefix + "OS_AUTH_TOKEN"},
{&c.DefaultContainerPolicy, prefix + "SWIFT_DEFAULT_CONTAINER_POLICY"},
} {
if *val.s == "" {
*val.s = os.Getenv(val.env)
}
}
return nil
}

View File

@@ -0,0 +1,72 @@
package swift
import "testing"
var configTests = []struct {
s string
cfg Config
}{
{
"swift:cnt1:/",
Config{
Container: "cnt1",
Prefix: "",
Connections: 5,
},
},
{
"swift:cnt2:/prefix",
Config{Container: "cnt2",
Prefix: "prefix",
Connections: 5,
},
},
{
"swift:cnt3:/prefix/longer",
Config{Container: "cnt3",
Prefix: "prefix/longer",
Connections: 5,
},
},
}
func TestParseConfig(t *testing.T) {
for _, test := range configTests {
t.Run("", func(t *testing.T) {
v, err := ParseConfig(test.s)
if err != nil {
t.Fatalf("parsing %q failed: %v", test.s, err)
}
cfg, ok := v.(Config)
if !ok {
t.Fatalf("wrong type returned, want Config, got %T", cfg)
}
if cfg != test.cfg {
t.Fatalf("wrong output for %q, want:\n %#v\ngot:\n %#v",
test.s, test.cfg, cfg)
}
})
}
}
var configTestsInvalid = []string{
"swift://hostname/container",
"swift:////",
"swift://",
"swift:////prefix",
"swift:container",
"swift:container:",
"swift:container/prefix",
}
func TestParseConfigInvalid(t *testing.T) {
for i, test := range configTestsInvalid {
_, err := ParseConfig(test)
if err == nil {
t.Errorf("test %d: invalid config %s did not return an error", i, test)
continue
}
}
}

View File

@@ -0,0 +1,331 @@
package swift
import (
"context"
"fmt"
"io"
"net/http"
"path"
"path/filepath"
"restic"
"restic/backend"
"restic/debug"
"restic/errors"
"strings"
"time"
"github.com/ncw/swift"
)
const connLimit = 10
// beSwift is a backend which stores the data on a swift endpoint.
type beSwift struct {
conn *swift.Connection
sem *backend.Semaphore
container string // Container name
prefix string // Prefix of object names in the container
backend.Layout
}
// ensure statically that *beSwift implements restic.Backend.
var _ restic.Backend = &beSwift{}
// Open opens the swift backend at a container in region. The container is
// created if it does not exist yet.
func Open(cfg Config) (restic.Backend, error) {
debug.Log("config %#v", cfg)
sem, err := backend.NewSemaphore(cfg.Connections)
if err != nil {
return nil, err
}
be := &beSwift{
conn: &swift.Connection{
UserName: cfg.UserName,
Domain: cfg.Domain,
ApiKey: cfg.APIKey,
AuthUrl: cfg.AuthURL,
Region: cfg.Region,
Tenant: cfg.Tenant,
TenantId: cfg.TenantID,
TenantDomain: cfg.TenantDomain,
TrustId: cfg.TrustID,
StorageUrl: cfg.StorageURL,
AuthToken: cfg.AuthToken,
ConnectTimeout: time.Minute,
Timeout: time.Minute,
Transport: backend.Transport(),
},
sem: sem,
container: cfg.Container,
prefix: cfg.Prefix,
Layout: &backend.DefaultLayout{
Path: cfg.Prefix,
Join: path.Join,
},
}
// Authenticate if needed
if !be.conn.Authenticated() {
if err := be.conn.Authenticate(); err != nil {
return nil, errors.Wrap(err, "conn.Authenticate")
}
}
// Ensure container exists
switch _, _, err := be.conn.Container(be.container); err {
case nil:
// Container exists
case swift.ContainerNotFound:
err = be.createContainer(cfg.DefaultContainerPolicy)
if err != nil {
return nil, errors.Wrap(err, "beSwift.createContainer")
}
default:
return nil, errors.Wrap(err, "conn.Container")
}
// check that the server supports byte ranges
_, hdr, err := be.conn.Account()
if err != nil {
return nil, errors.Wrap(err, "Account()")
}
if hdr["Accept-Ranges"] != "bytes" {
return nil, errors.New("backend does not support byte range")
}
return be, nil
}
func (be *beSwift) createContainer(policy string) error {
var h swift.Headers
if policy != "" {
h = swift.Headers{
"X-Storage-Policy": policy,
}
}
return be.conn.ContainerCreate(be.container, h)
}
// Location returns this backend's location (the container name).
func (be *beSwift) Location() string {
return be.container
}
// Load returns a reader that yields the contents of the file at h at the
// given offset. If length is nonzero, only a portion of the file is
// returned. rd must be closed after use.
func (be *beSwift) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
debug.Log("Load %v, length %v, offset %v", h, length, offset)
if err := h.Valid(); err != nil {
return nil, err
}
if offset < 0 {
return nil, errors.New("offset is negative")
}
if length < 0 {
return nil, errors.Errorf("invalid length %d", length)
}
objName := be.Filename(h)
be.sem.GetToken()
defer func() {
be.sem.ReleaseToken()
}()
headers := swift.Headers{}
if offset > 0 {
headers["Range"] = fmt.Sprintf("bytes=%d-", offset)
}
if length > 0 {
headers["Range"] = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1)
}
if _, ok := headers["Range"]; ok {
debug.Log("Load(%v) send range %v", h, headers["Range"])
}
obj, _, err := be.conn.ObjectOpen(be.container, objName, false, headers)
if err != nil {
debug.Log(" err %v", err)
return nil, errors.Wrap(err, "conn.ObjectOpen")
}
return obj, nil
}
// Save stores data in the backend at the handle.
func (be *beSwift) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
if err = h.Valid(); err != nil {
return err
}
objName := be.Filename(h)
debug.Log("Save %v at %v", h, objName)
// Check key does not already exist
switch _, _, err = be.conn.Object(be.container, objName); err {
case nil:
debug.Log("%v already exists", h)
return errors.New("key already exists")
case swift.ObjectNotFound:
// Ok, that's what we want
default:
return errors.Wrap(err, "conn.Object")
}
be.sem.GetToken()
defer func() {
be.sem.ReleaseToken()
}()
encoding := "binary/octet-stream"
debug.Log("PutObject(%v, %v, %v)", be.container, objName, encoding)
_, err = be.conn.ObjectPut(be.container, objName, rd, true, "", encoding, nil)
debug.Log("%v, err %#v", objName, err)
return errors.Wrap(err, "client.PutObject")
}
// Stat returns information about a blob.
func (be *beSwift) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) {
debug.Log("%v", h)
objName := be.Filename(h)
obj, _, err := be.conn.Object(be.container, objName)
if err != nil {
debug.Log("Object() err %v", err)
return restic.FileInfo{}, errors.Wrap(err, "conn.Object")
}
return restic.FileInfo{Size: obj.Bytes}, nil
}
// Test returns true if a blob of the given type and name exists in the backend.
func (be *beSwift) Test(ctx context.Context, h restic.Handle) (bool, error) {
objName := be.Filename(h)
switch _, _, err := be.conn.Object(be.container, objName); err {
case nil:
return true, nil
case swift.ObjectNotFound:
return false, nil
default:
return false, errors.Wrap(err, "conn.Object")
}
}
// Remove removes the blob with the given name and type.
func (be *beSwift) Remove(ctx context.Context, h restic.Handle) error {
objName := be.Filename(h)
err := be.conn.ObjectDelete(be.container, objName)
debug.Log("Remove(%v) -> err %v", h, err)
return errors.Wrap(err, "conn.ObjectDelete")
}
// List returns a channel that yields all names of blobs of type t. A
// goroutine is started for this. If the channel done is closed, sending
// stops.
func (be *beSwift) List(ctx context.Context, t restic.FileType) <-chan string {
debug.Log("listing %v", t)
ch := make(chan string)
prefix := be.Filename(restic.Handle{Type: t}) + "/"
go func() {
defer close(ch)
err := be.conn.ObjectsWalk(be.container, &swift.ObjectsOpts{Prefix: prefix},
func(opts *swift.ObjectsOpts) (interface{}, error) {
newObjects, err := be.conn.ObjectNames(be.container, opts)
if err != nil {
return nil, errors.Wrap(err, "conn.ObjectNames")
}
for _, obj := range newObjects {
m := filepath.Base(strings.TrimPrefix(obj, prefix))
if m == "" {
continue
}
select {
case ch <- m:
case <-ctx.Done():
return nil, io.EOF
}
}
return newObjects, nil
})
if err != nil {
debug.Log("ObjectsWalk returned error: %v", err)
}
}()
return ch
}
// Remove keys for a specified backend type.
func (be *beSwift) removeKeys(ctx context.Context, t restic.FileType) error {
for key := range be.List(ctx, t) {
err := be.Remove(ctx, restic.Handle{Type: t, Name: key})
if err != nil {
return err
}
}
return nil
}
// IsNotExist returns true if the error is caused by a not existing file.
func (be *beSwift) IsNotExist(err error) bool {
if e, ok := errors.Cause(err).(*swift.Error); ok {
return e.StatusCode == http.StatusNotFound
}
return false
}
// Delete removes all restic objects in the container.
// It will not remove the container itself.
func (be *beSwift) Delete(ctx context.Context) error {
alltypes := []restic.FileType{
restic.DataFile,
restic.KeyFile,
restic.LockFile,
restic.SnapshotFile,
restic.IndexFile}
for _, t := range alltypes {
err := be.removeKeys(ctx, t)
if err != nil {
return nil
}
}
err := be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
if err != nil && !be.IsNotExist(err) {
return err
}
return nil
}
// Close does nothing
func (be *beSwift) Close() error { return nil }

View File

@@ -0,0 +1,111 @@
package swift_test
import (
"context"
"fmt"
"os"
"restic"
"testing"
"time"
"restic/errors"
. "restic/test"
"restic/backend/swift"
"restic/backend/test"
)
func newSwiftTestSuite(t testing.TB) *test.Suite {
return &test.Suite{
// do not use excessive data
MinimalData: true,
// wait for removals for at least 20s
WaitForDelayedRemoval: 20 * time.Second,
// NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) {
swiftcfg, err := swift.ParseConfig(os.Getenv("RESTIC_TEST_SWIFT"))
if err != nil {
return nil, err
}
cfg := swiftcfg.(swift.Config)
if err = swift.ApplyEnvironment("RESTIC_TEST_", &cfg); err != nil {
return nil, err
}
cfg.Prefix += fmt.Sprintf("/test-%d", time.Now().UnixNano())
t.Logf("using prefix %v", cfg.Prefix)
return cfg, nil
},
// CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) {
cfg := config.(swift.Config)
be, err := swift.Open(cfg)
if err != nil {
return nil, err
}
exists, err := be.Test(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil {
return nil, err
}
if exists {
return nil, errors.New("config already exists")
}
return be, nil
},
// OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) {
cfg := config.(swift.Config)
return swift.Open(cfg)
},
// CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error {
cfg := config.(swift.Config)
be, err := swift.Open(cfg)
if err != nil {
return err
}
if err := be.(restic.Deleter).Delete(context.TODO()); err != nil {
return err
}
return nil
},
}
}
func TestBackendSwift(t *testing.T) {
defer func() {
if t.Skipped() {
SkipDisallowed(t, "restic/backend/swift.TestBackendSwift")
}
}()
if os.Getenv("RESTIC_TEST_SWIFT") == "" {
t.Skip("RESTIC_TEST_SWIFT unset, skipping test")
return
}
t.Logf("run tests")
newSwiftTestSuite(t).RunTests(t)
}
func BenchmarkBackendSwift(t *testing.B) {
if os.Getenv("RESTIC_TEST_SWIFT") == "" {
t.Skip("RESTIC_TEST_SWIFT unset, skipping test")
return
}
t.Logf("run tests")
newSwiftTestSuite(t).RunBenchmarks(t)
}

View File

@@ -2,6 +2,7 @@ package test
import (
"bytes"
"context"
"io"
"restic"
"restic/test"
@@ -12,14 +13,14 @@ func saveRandomFile(t testing.TB, be restic.Backend, length int) ([]byte, restic
data := test.Random(23, length)
id := restic.Hash(data)
handle := restic.Handle{Type: restic.DataFile, Name: id.String()}
if err := be.Save(handle, bytes.NewReader(data)); err != nil {
if err := be.Save(context.TODO(), handle, bytes.NewReader(data)); err != nil {
t.Fatalf("Save() error: %+v", err)
}
return data, handle
}
func remove(t testing.TB, be restic.Backend, h restic.Handle) {
if err := be.Remove(h); err != nil {
if err := be.Remove(context.TODO(), h); err != nil {
t.Fatalf("Remove() returned error: %v", err)
}
}
@@ -40,7 +41,7 @@ func (s *Suite) BenchmarkLoadFile(t *testing.B) {
t.ResetTimer()
for i := 0; i < t.N; i++ {
rd, err := be.Load(handle, 0, 0)
rd, err := be.Load(context.TODO(), handle, 0, 0)
if err != nil {
t.Fatal(err)
}
@@ -82,7 +83,7 @@ func (s *Suite) BenchmarkLoadPartialFile(t *testing.B) {
t.ResetTimer()
for i := 0; i < t.N; i++ {
rd, err := be.Load(handle, testLength, 0)
rd, err := be.Load(context.TODO(), handle, testLength, 0)
if err != nil {
t.Fatal(err)
}
@@ -126,7 +127,7 @@ func (s *Suite) BenchmarkLoadPartialFileOffset(t *testing.B) {
t.ResetTimer()
for i := 0; i < t.N; i++ {
rd, err := be.Load(handle, testLength, int64(testOffset))
rd, err := be.Load(context.TODO(), handle, testLength, int64(testOffset))
if err != nil {
t.Fatal(err)
}
@@ -171,11 +172,11 @@ func (s *Suite) BenchmarkSave(t *testing.B) {
t.Fatal(err)
}
if err := be.Save(handle, rd); err != nil {
if err := be.Save(context.TODO(), handle, rd); err != nil {
t.Fatal(err)
}
if err := be.Remove(handle); err != nil {
if err := be.Remove(context.TODO(), handle); err != nil {
t.Fatal(err)
}
}

View File

@@ -6,10 +6,12 @@ import (
"restic/test"
"strings"
"testing"
"time"
)
// Suite implements a test suite for restic backends.
type Suite struct {
// Config should be used to configure the backend.
Config interface{}
// NewConfig returns a config for a new temporary backend that will be used in tests.
@@ -26,6 +28,11 @@ type Suite struct {
// MinimalData instructs the tests to not use excessive data.
MinimalData bool
// WaitForDelayedRemoval is set to a non-zero value to instruct the test
// suite to wait for this amount of time until a file that was removed
// really disappeared.
WaitForDelayedRemoval time.Duration
}
// RunTests executes all defined tests as subtests of t.

View File

@@ -2,6 +2,7 @@ package test
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
@@ -13,12 +14,19 @@ import (
"sort"
"strings"
"testing"
"time"
"restic/test"
"restic/backend"
)
func seedRand(t testing.TB) {
seed := time.Now().UnixNano()
rand.Seed(seed)
t.Logf("rand initialized with seed %d", seed)
}
// TestCreateWithConfig tests that creating a backend in a location which already
// has a config file fails.
func (s *Suite) TestCreateWithConfig(t *testing.T) {
@@ -27,7 +35,7 @@ func (s *Suite) TestCreateWithConfig(t *testing.T) {
// remove a config if present
cfgHandle := restic.Handle{Type: restic.ConfigFile}
cfgPresent, err := b.Test(cfgHandle)
cfgPresent, err := b.Test(context.TODO(), cfgHandle)
if err != nil {
t.Fatalf("unable to test for config: %+v", err)
}
@@ -46,7 +54,7 @@ func (s *Suite) TestCreateWithConfig(t *testing.T) {
}
// remove config
err = b.Remove(restic.Handle{Type: restic.ConfigFile, Name: ""})
err = b.Remove(context.TODO(), restic.Handle{Type: restic.ConfigFile, Name: ""})
if err != nil {
t.Fatalf("unexpected error removing config: %+v", err)
}
@@ -71,12 +79,12 @@ func (s *Suite) TestConfig(t *testing.T) {
var testString = "Config"
// create config and read it back
_, err := backend.LoadAll(b, restic.Handle{Type: restic.ConfigFile})
_, err := backend.LoadAll(context.TODO(), b, restic.Handle{Type: restic.ConfigFile})
if err == nil {
t.Fatalf("did not get expected error for non-existing config")
}
err = b.Save(restic.Handle{Type: restic.ConfigFile}, strings.NewReader(testString))
err = b.Save(context.TODO(), restic.Handle{Type: restic.ConfigFile}, strings.NewReader(testString))
if err != nil {
t.Fatalf("Save() error: %+v", err)
}
@@ -85,7 +93,7 @@ func (s *Suite) TestConfig(t *testing.T) {
// same config
for _, name := range []string{"", "foo", "bar", "0000000000000000000000000000000000000000000000000000000000000000"} {
h := restic.Handle{Type: restic.ConfigFile, Name: name}
buf, err := backend.LoadAll(b, h)
buf, err := backend.LoadAll(context.TODO(), b, h)
if err != nil {
t.Fatalf("unable to read config with name %q: %+v", name, err)
}
@@ -101,15 +109,20 @@ func (s *Suite) TestConfig(t *testing.T) {
// TestLoad tests the backend's Load function.
func (s *Suite) TestLoad(t *testing.T) {
seedRand(t)
b := s.open(t)
defer s.close(t, b)
_, err := b.Load(restic.Handle{}, 0, 0)
rd, err := b.Load(context.TODO(), restic.Handle{}, 0, 0)
if err == nil {
t.Fatalf("Load() did not return an error for invalid handle")
}
if rd != nil {
_ = rd.Close()
}
_, err = b.Load(restic.Handle{Type: restic.DataFile, Name: "foobar"}, 0, 0)
err = testLoad(b, restic.Handle{Type: restic.DataFile, Name: "foobar"}, 0, 0)
if err == nil {
t.Fatalf("Load() did not return an error for non-existing blob")
}
@@ -120,12 +133,14 @@ func (s *Suite) TestLoad(t *testing.T) {
id := restic.Hash(data)
handle := restic.Handle{Type: restic.DataFile, Name: id.String()}
err = b.Save(handle, bytes.NewReader(data))
err = b.Save(context.TODO(), handle, bytes.NewReader(data))
if err != nil {
t.Fatalf("Save() error: %+v", err)
}
rd, err := b.Load(handle, 100, -1)
t.Logf("saved %d bytes as %v", length, handle)
rd, err = b.Load(context.TODO(), handle, 100, -1)
if err == nil {
t.Fatalf("Load() returned no error for negative offset!")
}
@@ -147,8 +162,8 @@ func (s *Suite) TestLoad(t *testing.T) {
if o < len(d) {
d = d[o:]
} else {
o = len(d)
d = d[:0]
t.Logf("offset == length, skipping test")
continue
}
getlen := l
@@ -160,14 +175,16 @@ func (s *Suite) TestLoad(t *testing.T) {
d = d[:l]
}
rd, err := b.Load(handle, getlen, int64(o))
rd, err := b.Load(context.TODO(), handle, getlen, int64(o))
if err != nil {
t.Logf("Load, l %v, o %v, len(d) %v, getlen %v", l, o, len(d), getlen)
t.Errorf("Load(%d, %d) returned unexpected error: %+v", l, o, err)
continue
}
buf, err := ioutil.ReadAll(rd)
if err != nil {
t.Logf("Load, l %v, o %v, len(d) %v, getlen %v", l, o, len(d), getlen)
t.Errorf("Load(%d, %d) ReadAll() returned unexpected error: %+v", l, o, err)
if err = rd.Close(); err != nil {
t.Errorf("Load(%d, %d) rd.Close() returned error: %+v", l, o, err)
@@ -176,6 +193,7 @@ func (s *Suite) TestLoad(t *testing.T) {
}
if l == 0 && len(buf) != len(d) {
t.Logf("Load, l %v, o %v, len(d) %v, getlen %v", l, o, len(d), getlen)
t.Errorf("Load(%d, %d) wrong number of bytes read: want %d, got %d", l, o, len(d), len(buf))
if err = rd.Close(); err != nil {
t.Errorf("Load(%d, %d) rd.Close() returned error: %+v", l, o, err)
@@ -184,6 +202,7 @@ func (s *Suite) TestLoad(t *testing.T) {
}
if l > 0 && l <= len(d) && len(buf) != l {
t.Logf("Load, l %v, o %v, len(d) %v, getlen %v", l, o, len(d), getlen)
t.Errorf("Load(%d, %d) wrong number of bytes read: want %d, got %d", l, o, l, len(buf))
if err = rd.Close(); err != nil {
t.Errorf("Load(%d, %d) rd.Close() returned error: %+v", l, o, err)
@@ -192,6 +211,7 @@ func (s *Suite) TestLoad(t *testing.T) {
}
if l > len(d) && len(buf) != len(d) {
t.Logf("Load, l %v, o %v, len(d) %v, getlen %v", l, o, len(d), getlen)
t.Errorf("Load(%d, %d) wrong number of bytes read for overlong read: want %d, got %d", l, o, l, len(buf))
if err = rd.Close(); err != nil {
t.Errorf("Load(%d, %d) rd.Close() returned error: %+v", l, o, err)
@@ -200,6 +220,7 @@ func (s *Suite) TestLoad(t *testing.T) {
}
if !bytes.Equal(buf, d) {
t.Logf("Load, l %v, o %v, len(d) %v, getlen %v", l, o, len(d), getlen)
t.Errorf("Load(%d, %d) returned wrong bytes", l, o)
if err = rd.Close(); err != nil {
t.Errorf("Load(%d, %d) rd.Close() returned error: %+v", l, o, err)
@@ -209,18 +230,19 @@ func (s *Suite) TestLoad(t *testing.T) {
err = rd.Close()
if err != nil {
t.Logf("Load, l %v, o %v, len(d) %v, getlen %v", l, o, len(d), getlen)
t.Errorf("Load(%d, %d) rd.Close() returned unexpected error: %+v", l, o, err)
continue
}
}
test.OK(t, b.Remove(handle))
test.OK(t, b.Remove(context.TODO(), handle))
}
type errorCloser struct {
io.Reader
size int64
t testing.TB
l int
t testing.TB
}
func (ec errorCloser) Close() error {
@@ -228,12 +250,14 @@ func (ec errorCloser) Close() error {
return errors.New("forbidden method close was called")
}
func (ec errorCloser) Size() int64 {
return ec.size
func (ec errorCloser) Len() int {
return ec.l
}
// TestSave tests saving data in the backend.
func (s *Suite) TestSave(t *testing.T) {
seedRand(t)
b := s.open(t)
defer s.close(t, b)
var id restic.ID
@@ -253,10 +277,10 @@ func (s *Suite) TestSave(t *testing.T) {
Type: restic.DataFile,
Name: fmt.Sprintf("%s-%d", id, i),
}
err := b.Save(h, bytes.NewReader(data))
err := b.Save(context.TODO(), h, bytes.NewReader(data))
test.OK(t, err)
buf, err := backend.LoadAll(b, h)
buf, err := backend.LoadAll(context.TODO(), b, h)
test.OK(t, err)
if len(buf) != len(data) {
t.Fatalf("number of bytes does not match, want %v, got %v", len(data), len(buf))
@@ -266,14 +290,14 @@ func (s *Suite) TestSave(t *testing.T) {
t.Fatalf("data not equal")
}
fi, err := b.Stat(h)
fi, err := b.Stat(context.TODO(), h)
test.OK(t, err)
if fi.Size != int64(len(data)) {
t.Fatalf("Stat() returned different size, want %q, got %d", len(data), fi.Size)
}
err = b.Remove(h)
err = b.Remove(context.TODO(), h)
if err != nil {
t.Fatalf("error removing item: %+v", err)
}
@@ -301,12 +325,12 @@ func (s *Suite) TestSave(t *testing.T) {
// wrap the tempfile in an errorCloser, so we can detect if the backend
// closes the reader
err = b.Save(h, errorCloser{t: t, size: int64(length), Reader: tmpfile})
err = b.Save(context.TODO(), h, errorCloser{t: t, l: length, Reader: tmpfile})
if err != nil {
t.Fatal(err)
}
err = b.Remove(h)
err = delayedRemove(t, b, h, s.WaitForDelayedRemoval)
if err != nil {
t.Fatalf("error removing item: %+v", err)
}
@@ -316,7 +340,7 @@ func (s *Suite) TestSave(t *testing.T) {
t.Fatal(err)
}
err = b.Save(h, tmpfile)
err = b.Save(context.TODO(), h, tmpfile)
if err != nil {
t.Fatal(err)
}
@@ -325,7 +349,7 @@ func (s *Suite) TestSave(t *testing.T) {
t.Fatal(err)
}
err = b.Remove(h)
err = b.Remove(context.TODO(), h)
if err != nil {
t.Fatalf("error removing item: %+v", err)
}
@@ -354,13 +378,13 @@ func (s *Suite) TestSaveFilenames(t *testing.T) {
for i, test := range filenameTests {
h := restic.Handle{Name: test.name, Type: restic.DataFile}
err := b.Save(h, strings.NewReader(test.data))
err := b.Save(context.TODO(), h, strings.NewReader(test.data))
if err != nil {
t.Errorf("test %d failed: Save() returned %+v", i, err)
continue
}
buf, err := backend.LoadAll(b, h)
buf, err := backend.LoadAll(context.TODO(), b, h)
if err != nil {
t.Errorf("test %d failed: Load() returned %+v", i, err)
continue
@@ -370,7 +394,7 @@ func (s *Suite) TestSaveFilenames(t *testing.T) {
t.Errorf("test %d: returned wrong bytes", i)
}
err = b.Remove(h)
err = b.Remove(context.TODO(), h)
if err != nil {
t.Errorf("test %d failed: Remove() returned %+v", i, err)
continue
@@ -391,11 +415,69 @@ var testStrings = []struct {
func store(t testing.TB, b restic.Backend, tpe restic.FileType, data []byte) restic.Handle {
id := restic.Hash(data)
h := restic.Handle{Name: id.String(), Type: tpe}
err := b.Save(h, bytes.NewReader(data))
err := b.Save(context.TODO(), h, bytes.NewReader(data))
test.OK(t, err)
return h
}
// testLoad loads a blob (but discards its contents).
func testLoad(b restic.Backend, h restic.Handle, length int, offset int64) error {
rd, err := b.Load(context.TODO(), h, 0, 0)
if err != nil {
return err
}
_, err = io.Copy(ioutil.Discard, rd)
cerr := rd.Close()
if err == nil {
err = cerr
}
return err
}
func delayedRemove(t testing.TB, be restic.Backend, h restic.Handle, maxwait time.Duration) error {
// Some backend (swift, I'm looking at you) may implement delayed
// removal of data. Let's wait a bit if this happens.
err := be.Remove(context.TODO(), h)
if err != nil {
return err
}
start := time.Now()
attempt := 0
for time.Since(start) <= maxwait {
found, err := be.Test(context.TODO(), h)
if err != nil {
return err
}
if !found {
break
}
time.Sleep(500 * time.Millisecond)
attempt++
}
return nil
}
func delayedList(t testing.TB, b restic.Backend, tpe restic.FileType, max int, maxwait time.Duration) restic.IDs {
list := restic.NewIDSet()
start := time.Now()
for i := 0; i < max; i++ {
for s := range b.List(context.TODO(), tpe) {
id := restic.TestParseID(s)
list.Insert(id)
}
if len(list) < max && time.Since(start) < maxwait {
time.Sleep(500 * time.Millisecond)
}
}
return list.List()
}
// TestBackend tests all functions of the backend.
func (s *Suite) TestBackend(t *testing.T) {
b := s.open(t)
@@ -412,20 +494,20 @@ func (s *Suite) TestBackend(t *testing.T) {
// test if blob is already in repository
h := restic.Handle{Type: tpe, Name: id.String()}
ret, err := b.Test(h)
ret, err := b.Test(context.TODO(), h)
test.OK(t, err)
test.Assert(t, !ret, "blob was found to exist before creating")
// try to stat a not existing blob
_, err = b.Stat(h)
_, err = b.Stat(context.TODO(), h)
test.Assert(t, err != nil, "blob data could be extracted before creation")
// try to read not existing blob
_, err = b.Load(h, 0, 0)
test.Assert(t, err != nil, "blob reader could be obtained before creation")
err = testLoad(b, h, 0, 0)
test.Assert(t, err != nil, "blob could be read before creation")
// try to get string out, should fail
ret, err = b.Test(h)
ret, err = b.Test(context.TODO(), h)
test.OK(t, err)
test.Assert(t, !ret, "id %q was found (but should not have)", ts.id)
}
@@ -436,7 +518,7 @@ func (s *Suite) TestBackend(t *testing.T) {
// test Load()
h := restic.Handle{Type: tpe, Name: ts.id}
buf, err := backend.LoadAll(b, h)
buf, err := backend.LoadAll(context.TODO(), b, h)
test.OK(t, err)
test.Equals(t, ts.data, string(buf))
@@ -446,7 +528,7 @@ func (s *Suite) TestBackend(t *testing.T) {
length := end - start
buf2 := make([]byte, length)
rd, err := b.Load(h, len(buf2), int64(start))
rd, err := b.Load(context.TODO(), h, len(buf2), int64(start))
test.OK(t, err)
n, err := io.ReadFull(rd, buf2)
test.OK(t, err)
@@ -466,20 +548,20 @@ func (s *Suite) TestBackend(t *testing.T) {
// create blob
h := restic.Handle{Type: tpe, Name: ts.id}
err := b.Save(h, strings.NewReader(ts.data))
err := b.Save(context.TODO(), h, strings.NewReader(ts.data))
test.Assert(t, err != nil, "expected error for %v, got %v", h, err)
// remove and recreate
err = b.Remove(h)
err = delayedRemove(t, b, h, s.WaitForDelayedRemoval)
test.OK(t, err)
// test that the blob is gone
ok, err := b.Test(h)
ok, err := b.Test(context.TODO(), h)
test.OK(t, err)
test.Assert(t, !ok, "removed blob still present")
// create blob
err = b.Save(h, strings.NewReader(ts.data))
err = b.Save(context.TODO(), h, strings.NewReader(ts.data))
test.OK(t, err)
// list items
@@ -491,12 +573,7 @@ func (s *Suite) TestBackend(t *testing.T) {
IDs = append(IDs, id)
}
list := restic.IDs{}
for s := range b.List(tpe, nil) {
list = append(list, restic.TestParseID(s))
}
list := delayedList(t, b, tpe, len(IDs), s.WaitForDelayedRemoval)
if len(IDs) != len(list) {
t.Fatalf("wrong number of IDs returned: want %d, got %d", len(IDs), len(list))
}
@@ -516,15 +593,15 @@ func (s *Suite) TestBackend(t *testing.T) {
h := restic.Handle{Type: tpe, Name: id.String()}
found, err := b.Test(h)
found, err := b.Test(context.TODO(), h)
test.OK(t, err)
test.Assert(t, found, fmt.Sprintf("id %q not found", id))
test.OK(t, b.Remove(h))
test.OK(t, delayedRemove(t, b, h, s.WaitForDelayedRemoval))
found, err = b.Test(h)
found, err = b.Test(context.TODO(), h)
test.OK(t, err)
test.Assert(t, !found, fmt.Sprintf("id %q not found after removal", id))
test.Assert(t, !found, fmt.Sprintf("id %q found after removal", id))
}
}
}
@@ -544,7 +621,7 @@ func (s *Suite) TestDelete(t *testing.T) {
return
}
err := be.Delete()
err := be.Delete(context.TODO())
if err != nil {
t.Fatalf("error deleting backend: %+v", err)
}

View File

@@ -1,6 +1,7 @@
package test_test
import (
"context"
"restic"
"restic/errors"
"testing"
@@ -26,7 +27,7 @@ func newTestSuite(t testing.TB) *test.Suite {
Create: func(cfg interface{}) (restic.Backend, error) {
c := cfg.(*memConfig)
if c.be != nil {
ok, err := c.be.Test(restic.Handle{Type: restic.ConfigFile})
ok, err := c.be.Test(context.TODO(), restic.Handle{Type: restic.ConfigFile})
if err != nil {
return nil, err
}

View File

@@ -1,14 +1,15 @@
package backend
import (
"context"
"io"
"io/ioutil"
"restic"
)
// LoadAll reads all data stored in the backend for the handle.
func LoadAll(be restic.Backend, h restic.Handle) (buf []byte, err error) {
rd, err := be.Load(h, 0, 0)
func LoadAll(ctx context.Context, be restic.Backend, h restic.Handle) (buf []byte, err error) {
rd, err := be.Load(ctx, h, 0, 0)
if err != nil {
return nil, err
}
@@ -28,16 +29,6 @@ func LoadAll(be restic.Backend, h restic.Handle) (buf []byte, err error) {
return ioutil.ReadAll(rd)
}
// Closer wraps an io.Reader and adds a Close() method that does nothing.
type Closer struct {
io.Reader
}
// Close is a no-op.
func (c Closer) Close() error {
return nil
}
// LimitedReadCloser wraps io.LimitedReader and exposes the Close() method.
type LimitedReadCloser struct {
io.ReadCloser

View File

@@ -2,6 +2,7 @@ package backend_test
import (
"bytes"
"context"
"math/rand"
"restic"
"testing"
@@ -21,10 +22,10 @@ func TestLoadAll(t *testing.T) {
data := Random(23+i, rand.Intn(MiB)+500*KiB)
id := restic.Hash(data)
err := b.Save(restic.Handle{Name: id.String(), Type: restic.DataFile}, bytes.NewReader(data))
err := b.Save(context.TODO(), restic.Handle{Name: id.String(), Type: restic.DataFile}, bytes.NewReader(data))
OK(t, err)
buf, err := backend.LoadAll(b, restic.Handle{Type: restic.DataFile, Name: id.String()})
buf, err := backend.LoadAll(context.TODO(), b, restic.Handle{Type: restic.DataFile, Name: id.String()})
OK(t, err)
if len(buf) != len(data) {
@@ -46,10 +47,10 @@ func TestLoadSmallBuffer(t *testing.T) {
data := Random(23+i, rand.Intn(MiB)+500*KiB)
id := restic.Hash(data)
err := b.Save(restic.Handle{Name: id.String(), Type: restic.DataFile}, bytes.NewReader(data))
err := b.Save(context.TODO(), restic.Handle{Name: id.String(), Type: restic.DataFile}, bytes.NewReader(data))
OK(t, err)
buf, err := backend.LoadAll(b, restic.Handle{Type: restic.DataFile, Name: id.String()})
buf, err := backend.LoadAll(context.TODO(), b, restic.Handle{Type: restic.DataFile, Name: id.String()})
OK(t, err)
if len(buf) != len(data) {
@@ -71,10 +72,10 @@ func TestLoadLargeBuffer(t *testing.T) {
data := Random(23+i, rand.Intn(MiB)+500*KiB)
id := restic.Hash(data)
err := b.Save(restic.Handle{Name: id.String(), Type: restic.DataFile}, bytes.NewReader(data))
err := b.Save(context.TODO(), restic.Handle{Name: id.String(), Type: restic.DataFile}, bytes.NewReader(data))
OK(t, err)
buf, err := backend.LoadAll(b, restic.Handle{Type: restic.DataFile, Name: id.String()})
buf, err := backend.LoadAll(context.TODO(), b, restic.Handle{Type: restic.DataFile, Name: id.String()})
OK(t, err)
if len(buf) != len(data) {

View File

@@ -1,6 +1,9 @@
package restic
import "restic/errors"
import (
"context"
"restic/errors"
)
// ErrNoIDPrefixFound is returned by Find() when no ID for the given prefix
// could be found.
@@ -14,13 +17,10 @@ var ErrMultipleIDMatches = errors.New("multiple IDs with prefix found")
// start with prefix. If none is found, nil and ErrNoIDPrefixFound is returned.
// If more than one is found, nil and ErrMultipleIDMatches is returned.
func Find(be Lister, t FileType, prefix string) (string, error) {
done := make(chan struct{})
defer close(done)
match := ""
// TODO: optimize by sorting list etc.
for name := range be.List(t, done) {
for name := range be.List(context.TODO(), t) {
if prefix == name[:len(prefix)] {
if match == "" {
match = name
@@ -42,12 +42,9 @@ const minPrefixLength = 8
// PrefixLength returns the number of bytes required so that all prefixes of
// all names of type t are unique.
func PrefixLength(be Lister, t FileType) (int, error) {
done := make(chan struct{})
defer close(done)
// load all IDs of the given type
list := make([]string, 0, 100)
for name := range be.List(t, done) {
for name := range be.List(context.TODO(), t) {
list = append(list, name)
}

View File

@@ -1,15 +1,16 @@
package restic
import (
"context"
"testing"
)
type mockBackend struct {
list func(FileType, <-chan struct{}) <-chan string
list func(context.Context, FileType) <-chan string
}
func (m mockBackend) List(t FileType, done <-chan struct{}) <-chan string {
return m.list(t, done)
func (m mockBackend) List(ctx context.Context, t FileType) <-chan string {
return m.list(ctx, t)
}
var samples = IDs{
@@ -27,14 +28,14 @@ func TestPrefixLength(t *testing.T) {
list := samples
m := mockBackend{}
m.list = func(t FileType, done <-chan struct{}) <-chan string {
m.list = func(ctx context.Context, t FileType) <-chan string {
ch := make(chan string)
go func() {
defer close(ch)
for _, id := range list {
select {
case ch <- id.String():
case <-done:
case <-ctx.Done():
return
}
}

View File

@@ -1,21 +0,0 @@
package restic
import (
"sync"
"github.com/restic/chunker"
)
var bufPool = sync.Pool{
New: func() interface{} {
return make([]byte, chunker.MinSize)
},
}
func getBuf() []byte {
return bufPool.Get().([]byte)
}
func freeBuf(data []byte) {
bufPool.Put(data)
}

View File

@@ -1,6 +1,7 @@
package checker
import (
"context"
"crypto/sha256"
"fmt"
"io"
@@ -12,7 +13,6 @@ import (
"restic/hashing"
"restic"
"restic/crypto"
"restic/debug"
"restic/pack"
"restic/repository"
@@ -76,7 +76,7 @@ func (err ErrOldIndexFormat) Error() string {
}
// LoadIndex loads all index files.
func (c *Checker) LoadIndex() (hints []error, errs []error) {
func (c *Checker) LoadIndex(ctx context.Context) (hints []error, errs []error) {
debug.Log("Start")
type indexRes struct {
Index *repository.Index
@@ -86,21 +86,21 @@ func (c *Checker) LoadIndex() (hints []error, errs []error) {
indexCh := make(chan indexRes)
worker := func(id restic.ID, done <-chan struct{}) error {
worker := func(ctx context.Context, id restic.ID) error {
debug.Log("worker got index %v", id)
idx, err := repository.LoadIndexWithDecoder(c.repo, id, repository.DecodeIndex)
idx, err := repository.LoadIndexWithDecoder(ctx, c.repo, id, repository.DecodeIndex)
if errors.Cause(err) == repository.ErrOldIndexFormat {
debug.Log("index %v has old format", id.Str())
hints = append(hints, ErrOldIndexFormat{id})
idx, err = repository.LoadIndexWithDecoder(c.repo, id, repository.DecodeOldIndex)
idx, err = repository.LoadIndexWithDecoder(ctx, c.repo, id, repository.DecodeOldIndex)
}
err = errors.Wrapf(err, "error loading index %v", id.Str())
select {
case indexCh <- indexRes{Index: idx, ID: id.String(), err: err}:
case <-done:
case <-ctx.Done():
}
return nil
@@ -109,7 +109,7 @@ func (c *Checker) LoadIndex() (hints []error, errs []error) {
go func() {
defer close(indexCh)
debug.Log("start loading indexes in parallel")
err := repository.FilesInParallel(c.repo.Backend(), restic.IndexFile, defaultParallelism,
err := repository.FilesInParallel(ctx, c.repo.Backend(), restic.IndexFile, defaultParallelism,
repository.ParallelWorkFuncParseID(worker))
debug.Log("loading indexes finished, error: %v", err)
if err != nil {
@@ -141,7 +141,7 @@ func (c *Checker) LoadIndex() (hints []error, errs []error) {
debug.Log("process blobs")
cnt := 0
for blob := range res.Index.Each(done) {
for blob := range res.Index.Each(ctx) {
c.packs.Insert(blob.PackID)
c.blobs.Insert(blob.ID)
c.blobRefs.M[blob.ID] = 0
@@ -183,7 +183,7 @@ func (e PackError) Error() string {
return "pack " + e.ID.String() + ": " + e.Err.Error()
}
func packIDTester(repo restic.Repository, inChan <-chan restic.ID, errChan chan<- error, wg *sync.WaitGroup, done <-chan struct{}) {
func packIDTester(ctx context.Context, repo restic.Repository, inChan <-chan restic.ID, errChan chan<- error, wg *sync.WaitGroup) {
debug.Log("worker start")
defer debug.Log("worker done")
@@ -191,7 +191,7 @@ func packIDTester(repo restic.Repository, inChan <-chan restic.ID, errChan chan<
for id := range inChan {
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
ok, err := repo.Backend().Test(h)
ok, err := repo.Backend().Test(ctx, h)
if err != nil {
err = PackError{ID: id, Err: err}
} else {
@@ -203,7 +203,7 @@ func packIDTester(repo restic.Repository, inChan <-chan restic.ID, errChan chan<
if err != nil {
debug.Log("error checking for pack %s: %v", id.Str(), err)
select {
case <-done:
case <-ctx.Done():
return
case errChan <- err:
}
@@ -218,7 +218,7 @@ func packIDTester(repo restic.Repository, inChan <-chan restic.ID, errChan chan<
// Packs checks that all packs referenced in the index are still available and
// there are no packs that aren't in an index. errChan is closed after all
// packs have been checked.
func (c *Checker) Packs(errChan chan<- error, done <-chan struct{}) {
func (c *Checker) Packs(ctx context.Context, errChan chan<- error) {
defer close(errChan)
debug.Log("checking for %d packs", len(c.packs))
@@ -229,7 +229,7 @@ func (c *Checker) Packs(errChan chan<- error, done <-chan struct{}) {
IDChan := make(chan restic.ID)
for i := 0; i < defaultParallelism; i++ {
workerWG.Add(1)
go packIDTester(c.repo, IDChan, errChan, &workerWG, done)
go packIDTester(ctx, c.repo, IDChan, errChan, &workerWG)
}
for id := range c.packs {
@@ -242,12 +242,12 @@ func (c *Checker) Packs(errChan chan<- error, done <-chan struct{}) {
workerWG.Wait()
debug.Log("workers terminated")
for id := range c.repo.List(restic.DataFile, done) {
for id := range c.repo.List(ctx, restic.DataFile) {
debug.Log("check data blob %v", id.Str())
if !seenPacks.Has(id) {
c.orphanedPacks = append(c.orphanedPacks, id)
select {
case <-done:
case <-ctx.Done():
return
case errChan <- PackError{ID: id, Orphaned: true, Err: errors.New("not referenced in any index")}:
}
@@ -277,8 +277,8 @@ func (e Error) Error() string {
return e.Err.Error()
}
func loadTreeFromSnapshot(repo restic.Repository, id restic.ID) (restic.ID, error) {
sn, err := restic.LoadSnapshot(repo, id)
func loadTreeFromSnapshot(ctx context.Context, repo restic.Repository, id restic.ID) (restic.ID, error) {
sn, err := restic.LoadSnapshot(ctx, repo, id)
if err != nil {
debug.Log("error loading snapshot %v: %v", id.Str(), err)
return restic.ID{}, err
@@ -293,7 +293,7 @@ func loadTreeFromSnapshot(repo restic.Repository, id restic.ID) (restic.ID, erro
}
// loadSnapshotTreeIDs loads all snapshots from backend and returns the tree IDs.
func loadSnapshotTreeIDs(repo restic.Repository) (restic.IDs, []error) {
func loadSnapshotTreeIDs(ctx context.Context, repo restic.Repository) (restic.IDs, []error) {
var trees struct {
IDs restic.IDs
sync.Mutex
@@ -304,7 +304,7 @@ func loadSnapshotTreeIDs(repo restic.Repository) (restic.IDs, []error) {
sync.Mutex
}
snapshotWorker := func(strID string, done <-chan struct{}) error {
snapshotWorker := func(ctx context.Context, strID string) error {
id, err := restic.ParseID(strID)
if err != nil {
return err
@@ -312,7 +312,7 @@ func loadSnapshotTreeIDs(repo restic.Repository) (restic.IDs, []error) {
debug.Log("load snapshot %v", id.Str())
treeID, err := loadTreeFromSnapshot(repo, id)
treeID, err := loadTreeFromSnapshot(ctx, repo, id)
if err != nil {
errs.Lock()
errs.errs = append(errs.errs, err)
@@ -328,7 +328,7 @@ func loadSnapshotTreeIDs(repo restic.Repository) (restic.IDs, []error) {
return nil
}
err := repository.FilesInParallel(repo.Backend(), restic.SnapshotFile, defaultParallelism, snapshotWorker)
err := repository.FilesInParallel(ctx, repo.Backend(), restic.SnapshotFile, defaultParallelism, snapshotWorker)
if err != nil {
errs.errs = append(errs.errs, err)
}
@@ -353,9 +353,9 @@ type treeJob struct {
}
// loadTreeWorker loads trees from repo and sends them to out.
func loadTreeWorker(repo restic.Repository,
func loadTreeWorker(ctx context.Context, repo restic.Repository,
in <-chan restic.ID, out chan<- treeJob,
done <-chan struct{}, wg *sync.WaitGroup) {
wg *sync.WaitGroup) {
defer func() {
debug.Log("exiting")
@@ -371,7 +371,7 @@ func loadTreeWorker(repo restic.Repository,
outCh = nil
for {
select {
case <-done:
case <-ctx.Done():
return
case treeID, ok := <-inCh:
@@ -380,7 +380,7 @@ func loadTreeWorker(repo restic.Repository,
}
debug.Log("load tree %v", treeID.Str())
tree, err := repo.LoadTree(treeID)
tree, err := repo.LoadTree(ctx, treeID)
debug.Log("load tree %v (%v) returned err: %v", tree, treeID.Str(), err)
job = treeJob{ID: treeID, error: err, Tree: tree}
outCh = out
@@ -395,7 +395,7 @@ func loadTreeWorker(repo restic.Repository,
}
// checkTreeWorker checks the trees received and sends out errors to errChan.
func (c *Checker) checkTreeWorker(in <-chan treeJob, out chan<- error, done <-chan struct{}, wg *sync.WaitGroup) {
func (c *Checker) checkTreeWorker(ctx context.Context, in <-chan treeJob, out chan<- error, wg *sync.WaitGroup) {
defer func() {
debug.Log("exiting")
wg.Done()
@@ -410,7 +410,7 @@ func (c *Checker) checkTreeWorker(in <-chan treeJob, out chan<- error, done <-ch
outCh = nil
for {
select {
case <-done:
case <-ctx.Done():
debug.Log("done channel closed, exiting")
return
@@ -458,7 +458,7 @@ func (c *Checker) checkTreeWorker(in <-chan treeJob, out chan<- error, done <-ch
}
}
func filterTrees(backlog restic.IDs, loaderChan chan<- restic.ID, in <-chan treeJob, out chan<- treeJob, done <-chan struct{}) {
func filterTrees(ctx context.Context, backlog restic.IDs, loaderChan chan<- restic.ID, in <-chan treeJob, out chan<- treeJob) {
defer func() {
debug.Log("closing output channels")
close(loaderChan)
@@ -489,7 +489,7 @@ func filterTrees(backlog restic.IDs, loaderChan chan<- restic.ID, in <-chan tree
}
select {
case <-done:
case <-ctx.Done():
return
case loadCh <- nextTreeID:
@@ -549,15 +549,15 @@ func filterTrees(backlog restic.IDs, loaderChan chan<- restic.ID, in <-chan tree
// Structure checks that for all snapshots all referenced data blobs and
// subtrees are available in the index. errChan is closed after all trees have
// been traversed.
func (c *Checker) Structure(errChan chan<- error, done <-chan struct{}) {
func (c *Checker) Structure(ctx context.Context, errChan chan<- error) {
defer close(errChan)
trees, errs := loadSnapshotTreeIDs(c.repo)
trees, errs := loadSnapshotTreeIDs(ctx, c.repo)
debug.Log("need to check %d trees from snapshots, %d errs returned", len(trees), len(errs))
for _, err := range errs {
select {
case <-done:
case <-ctx.Done():
return
case errChan <- err:
}
@@ -570,11 +570,11 @@ func (c *Checker) Structure(errChan chan<- error, done <-chan struct{}) {
var wg sync.WaitGroup
for i := 0; i < defaultParallelism; i++ {
wg.Add(2)
go loadTreeWorker(c.repo, treeIDChan, treeJobChan1, done, &wg)
go c.checkTreeWorker(treeJobChan2, errChan, done, &wg)
go loadTreeWorker(ctx, c.repo, treeIDChan, treeJobChan1, &wg)
go c.checkTreeWorker(ctx, treeJobChan2, errChan, &wg)
}
filterTrees(trees, treeIDChan, treeJobChan1, treeJobChan2, done)
filterTrees(ctx, trees, treeIDChan, treeJobChan1, treeJobChan2)
wg.Wait()
}
@@ -659,11 +659,11 @@ func (c *Checker) CountPacks() uint64 {
}
// checkPack reads a pack and checks the integrity of all blobs.
func checkPack(r restic.Repository, id restic.ID) error {
func checkPack(ctx context.Context, r restic.Repository, id restic.ID) error {
debug.Log("checking pack %v", id.Str())
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
rd, err := r.Backend().Load(h, 0, 0)
rd, err := r.Backend().Load(ctx, h, 0, 0)
if err != nil {
return err
}
@@ -724,7 +724,7 @@ func checkPack(r restic.Repository, id restic.ID) error {
continue
}
n, err := crypto.Decrypt(r.Key(), buf, buf)
n, err := r.Key().Decrypt(buf, buf)
if err != nil {
debug.Log(" error decrypting blob %v: %v", blob.ID.Str(), err)
errs = append(errs, errors.Errorf("blob %v: %v", i, err))
@@ -748,7 +748,7 @@ func checkPack(r restic.Repository, id restic.ID) error {
}
// ReadData loads all data from the repository and checks the integrity.
func (c *Checker) ReadData(p *restic.Progress, errChan chan<- error, done <-chan struct{}) {
func (c *Checker) ReadData(ctx context.Context, p *restic.Progress, errChan chan<- error) {
defer close(errChan)
p.Start()
@@ -761,7 +761,7 @@ func (c *Checker) ReadData(p *restic.Progress, errChan chan<- error, done <-chan
var ok bool
select {
case <-done:
case <-ctx.Done():
return
case id, ok = <-in:
if !ok {
@@ -769,21 +769,21 @@ func (c *Checker) ReadData(p *restic.Progress, errChan chan<- error, done <-chan
}
}
err := checkPack(c.repo, id)
err := checkPack(ctx, c.repo, id)
p.Report(restic.Stat{Blobs: 1})
if err == nil {
continue
}
select {
case <-done:
case <-ctx.Done():
return
case errChan <- err:
}
}
}
ch := c.repo.List(restic.DataFile, done)
ch := c.repo.List(ctx, restic.DataFile)
var wg sync.WaitGroup
for i := 0; i < defaultParallelism; i++ {

View File

@@ -1,6 +1,7 @@
package checker_test
import (
"context"
"io"
"math/rand"
"path/filepath"
@@ -16,13 +17,13 @@ import (
var checkerTestData = filepath.Join("testdata", "checker-test-repo.tar.gz")
func collectErrors(f func(chan<- error, <-chan struct{})) (errs []error) {
done := make(chan struct{})
defer close(done)
func collectErrors(ctx context.Context, f func(context.Context, chan<- error)) (errs []error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errChan := make(chan error)
go f(errChan, done)
go f(ctx, errChan)
for err := range errChan {
errs = append(errs, err)
@@ -32,17 +33,18 @@ func collectErrors(f func(chan<- error, <-chan struct{})) (errs []error) {
}
func checkPacks(chkr *checker.Checker) []error {
return collectErrors(chkr.Packs)
return collectErrors(context.TODO(), chkr.Packs)
}
func checkStruct(chkr *checker.Checker) []error {
return collectErrors(chkr.Structure)
return collectErrors(context.TODO(), chkr.Structure)
}
func checkData(chkr *checker.Checker) []error {
return collectErrors(
func(errCh chan<- error, done <-chan struct{}) {
chkr.ReadData(nil, errCh, done)
context.TODO(),
func(ctx context.Context, errCh chan<- error) {
chkr.ReadData(ctx, nil, errCh)
},
)
}
@@ -54,7 +56,7 @@ func TestCheckRepo(t *testing.T) {
repo := repository.TestOpenLocal(t, repodir)
chkr := checker.New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) > 0 {
t.Fatalf("expected no errors, got %v: %v", len(errs), errs)
}
@@ -77,10 +79,10 @@ func TestMissingPack(t *testing.T) {
Type: restic.DataFile,
Name: "657f7fb64f6a854fff6fe9279998ee09034901eded4e6db9bcee0e59745bbce6",
}
test.OK(t, repo.Backend().Remove(packHandle))
test.OK(t, repo.Backend().Remove(context.TODO(), packHandle))
chkr := checker.New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) > 0 {
t.Fatalf("expected no errors, got %v: %v", len(errs), errs)
}
@@ -113,10 +115,10 @@ func TestUnreferencedPack(t *testing.T) {
Type: restic.IndexFile,
Name: "3f1abfcb79c6f7d0a3be517d2c83c8562fba64ef2c8e9a3544b4edaf8b5e3b44",
}
test.OK(t, repo.Backend().Remove(indexHandle))
test.OK(t, repo.Backend().Remove(context.TODO(), indexHandle))
chkr := checker.New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) > 0 {
t.Fatalf("expected no errors, got %v: %v", len(errs), errs)
}
@@ -147,7 +149,7 @@ func TestUnreferencedBlobs(t *testing.T) {
Type: restic.SnapshotFile,
Name: "51d249d28815200d59e4be7b3f21a157b864dc343353df9d8e498220c2499b02",
}
test.OK(t, repo.Backend().Remove(snapshotHandle))
test.OK(t, repo.Backend().Remove(context.TODO(), snapshotHandle))
unusedBlobsBySnapshot := restic.IDs{
restic.TestParseID("58c748bbe2929fdf30c73262bd8313fe828f8925b05d1d4a87fe109082acb849"),
@@ -161,7 +163,7 @@ func TestUnreferencedBlobs(t *testing.T) {
sort.Sort(unusedBlobsBySnapshot)
chkr := checker.New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) > 0 {
t.Fatalf("expected no errors, got %v: %v", len(errs), errs)
}
@@ -192,7 +194,7 @@ func TestModifiedIndex(t *testing.T) {
Type: restic.IndexFile,
Name: "90f838b4ac28735fda8644fe6a08dbc742e57aaf81b30977b4fefa357010eafd",
}
f, err := repo.Backend().Load(h, 0, 0)
f, err := repo.Backend().Load(context.TODO(), h, 0, 0)
test.OK(t, err)
// save the index again with a modified name so that the hash doesn't match
@@ -201,13 +203,13 @@ func TestModifiedIndex(t *testing.T) {
Type: restic.IndexFile,
Name: "80f838b4ac28735fda8644fe6a08dbc742e57aaf81b30977b4fefa357010eafd",
}
err = repo.Backend().Save(h2, f)
err = repo.Backend().Save(context.TODO(), h2, f)
test.OK(t, err)
test.OK(t, f.Close())
chkr := checker.New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) == 0 {
t.Fatalf("expected errors not found")
}
@@ -230,7 +232,7 @@ func TestDuplicatePacksInIndex(t *testing.T) {
repo := repository.TestOpenLocal(t, repodir)
chkr := checker.New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(hints) == 0 {
t.Fatalf("did not get expected checker hints for duplicate packs in indexes")
}
@@ -259,8 +261,8 @@ type errorBackend struct {
ProduceErrors bool
}
func (b errorBackend) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
rd, err := b.Backend.Load(h, length, offset)
func (b errorBackend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
rd, err := b.Backend.Load(ctx, h, length, offset)
if err != nil {
return rd, err
}
@@ -303,17 +305,17 @@ func TestCheckerModifiedData(t *testing.T) {
defer cleanup()
arch := archiver.New(repo)
_, id, err := arch.Snapshot(nil, []string{"."}, nil, "localhost", nil)
_, id, err := arch.Snapshot(context.TODO(), nil, []string{"."}, nil, "localhost", nil)
test.OK(t, err)
t.Logf("archived as %v", id.Str())
beError := &errorBackend{Backend: repo.Backend()}
checkRepo := repository.New(beError)
test.OK(t, checkRepo.SearchKey(test.TestPassword, 5))
test.OK(t, checkRepo.SearchKey(context.TODO(), test.TestPassword, 5))
chkr := checker.New(checkRepo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) > 0 {
t.Fatalf("expected no errors, got %v: %v", len(errs), errs)
}
@@ -349,7 +351,7 @@ func BenchmarkChecker(t *testing.B) {
repo := repository.TestOpenLocal(t, repodir)
chkr := checker.New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) > 0 {
t.Fatalf("expected no errors, got %v: %v", len(errs), errs)
}

View File

@@ -1,6 +1,7 @@
package checker
import (
"context"
"restic"
"testing"
)
@@ -9,7 +10,7 @@ import (
func TestCheckRepo(t testing.TB, repo restic.Repository) {
chkr := New(repo)
hints, errs := chkr.LoadIndex()
hints, errs := chkr.LoadIndex(context.TODO())
if len(errs) != 0 {
t.Fatalf("errors loading index: %v", errs)
}
@@ -18,12 +19,9 @@ func TestCheckRepo(t testing.TB, repo restic.Repository) {
t.Fatalf("errors loading index: %v", hints)
}
done := make(chan struct{})
defer close(done)
// packs
errChan := make(chan error)
go chkr.Packs(errChan, done)
go chkr.Packs(context.TODO(), errChan)
for err := range errChan {
t.Error(err)
@@ -31,7 +29,7 @@ func TestCheckRepo(t testing.TB, repo restic.Repository) {
// structure
errChan = make(chan error)
go chkr.Structure(errChan, done)
go chkr.Structure(context.TODO(), errChan)
for err := range errChan {
t.Error(err)
@@ -45,7 +43,7 @@ func TestCheckRepo(t testing.TB, repo restic.Repository) {
// read data
errChan = make(chan error)
go chkr.ReadData(nil, errChan, done)
go chkr.ReadData(context.TODO(), nil, errChan)
for err := range errChan {
t.Error(err)

View File

@@ -1,6 +1,7 @@
package restic
import (
"context"
"testing"
"restic/errors"
@@ -23,7 +24,7 @@ const RepoVersion = 1
// JSONUnpackedLoader loads unpacked JSON.
type JSONUnpackedLoader interface {
LoadJSONUnpacked(FileType, ID, interface{}) error
LoadJSONUnpacked(context.Context, FileType, ID, interface{}) error
}
// CreateConfig creates a config file with a randomly selected polynomial and
@@ -57,12 +58,12 @@ func TestCreateConfig(t testing.TB, pol chunker.Pol) (cfg Config) {
}
// LoadConfig returns loads, checks and returns the config for a repository.
func LoadConfig(r JSONUnpackedLoader) (Config, error) {
func LoadConfig(ctx context.Context, r JSONUnpackedLoader) (Config, error) {
var (
cfg Config
)
err := r.LoadJSONUnpacked(ConfigFile, ID{}, &cfg)
err := r.LoadJSONUnpacked(ctx, ConfigFile, ID{}, &cfg)
if err != nil {
return Config{}, err
}

View File

@@ -1,6 +1,7 @@
package restic_test
import (
"context"
"restic"
"testing"
@@ -13,10 +14,10 @@ func (s saver) SaveJSONUnpacked(t restic.FileType, arg interface{}) (restic.ID,
return s(t, arg)
}
type loader func(restic.FileType, restic.ID, interface{}) error
type loader func(context.Context, restic.FileType, restic.ID, interface{}) error
func (l loader) LoadJSONUnpacked(t restic.FileType, id restic.ID, arg interface{}) error {
return l(t, id, arg)
func (l loader) LoadJSONUnpacked(ctx context.Context, t restic.FileType, id restic.ID, arg interface{}) error {
return l(ctx, t, id, arg)
}
func TestConfig(t *testing.T) {
@@ -36,7 +37,7 @@ func TestConfig(t *testing.T) {
_, err = saver(save).SaveJSONUnpacked(restic.ConfigFile, cfg1)
load := func(tpe restic.FileType, id restic.ID, arg interface{}) error {
load := func(ctx context.Context, tpe restic.FileType, id restic.ID, arg interface{}) error {
Assert(t, tpe == restic.ConfigFile,
"wrong backend type: got %v, wanted %v",
tpe, restic.ConfigFile)
@@ -46,7 +47,7 @@ func TestConfig(t *testing.T) {
return nil
}
cfg2, err := restic.LoadConfig(loader(load))
cfg2, err := restic.LoadConfig(context.TODO(), loader(load))
OK(t, err)
Assert(t, cfg1 == cfg2,

View File

@@ -19,7 +19,9 @@ const (
macKeySize = macKeySizeK + macKeySizeR // for Poly1305-AES128
ivSize = aes.BlockSize
macSize = poly1305.TagSize
macSize = poly1305.TagSize
// Extension is the number of bytes a plaintext is enlarged by encrypting it.
Extension = ivSize + macSize
)
@@ -32,11 +34,14 @@ var (
// encrypted and authenticated as a JSON data structure in the Data field of the Key
// structure.
type Key struct {
MAC MACKey `json:"mac"`
Encrypt EncryptionKey `json:"encrypt"`
MACKey `json:"mac"`
EncryptionKey `json:"encrypt"`
}
// EncryptionKey is key used for encryption
type EncryptionKey [32]byte
// MACKey is used to sign (authenticate) data.
type MACKey struct {
K [16]byte // for AES-128
R [16]byte // for Poly1305
@@ -123,22 +128,22 @@ func poly1305Verify(msg []byte, nonce []byte, key *MACKey, mac []byte) bool {
func NewRandomKey() *Key {
k := &Key{}
n, err := rand.Read(k.Encrypt[:])
n, err := rand.Read(k.EncryptionKey[:])
if n != aesKeySize || err != nil {
panic("unable to read enough random bytes for encryption key")
}
n, err = rand.Read(k.MAC.K[:])
n, err = rand.Read(k.MACKey.K[:])
if n != macKeySizeK || err != nil {
panic("unable to read enough random bytes for MAC encryption key")
}
n, err = rand.Read(k.MAC.R[:])
n, err = rand.Read(k.MACKey.R[:])
if n != macKeySizeR || err != nil {
panic("unable to read enough random bytes for MAC key")
}
maskKey(&k.MAC)
maskKey(&k.MACKey)
return k
}
@@ -156,10 +161,12 @@ type jsonMACKey struct {
R []byte `json:"r"`
}
// MarshalJSON converts the MACKey to JSON.
func (m *MACKey) MarshalJSON() ([]byte, error) {
return json.Marshal(jsonMACKey{K: m.K[:], R: m.R[:]})
}
// UnmarshalJSON fills the key m with data from the JSON representation.
func (m *MACKey) UnmarshalJSON(data []byte) error {
j := jsonMACKey{}
err := json.Unmarshal(data, &j)
@@ -194,10 +201,12 @@ func (m *MACKey) Valid() bool {
return false
}
// MarshalJSON converts the EncryptionKey to JSON.
func (k *EncryptionKey) MarshalJSON() ([]byte, error) {
return json.Marshal(k[:])
}
// UnmarshalJSON fills the key k with data from the JSON representation.
func (k *EncryptionKey) UnmarshalJSON(data []byte) error {
d := make([]byte, aesKeySize)
err := json.Unmarshal(data, &d)
@@ -228,8 +237,8 @@ var ErrInvalidCiphertext = errors.New("invalid ciphertext, same slice used for p
// MAC. Encrypt returns the new ciphertext slice, which is extended when
// necessary. ciphertext and plaintext may not point to (exactly) the same
// slice or non-intersecting slices.
func Encrypt(ks *Key, ciphertext []byte, plaintext []byte) ([]byte, error) {
if !ks.Valid() {
func (k *Key) Encrypt(ciphertext []byte, plaintext []byte) ([]byte, error) {
if !k.Valid() {
return nil, errors.New("invalid key")
}
@@ -248,7 +257,7 @@ func Encrypt(ks *Key, ciphertext []byte, plaintext []byte) ([]byte, error) {
}
iv := newIV()
c, err := aes.NewCipher(ks.Encrypt[:])
c, err := aes.NewCipher(k.EncryptionKey[:])
if err != nil {
panic(fmt.Sprintf("unable to create cipher: %v", err))
}
@@ -261,7 +270,7 @@ func Encrypt(ks *Key, ciphertext []byte, plaintext []byte) ([]byte, error) {
// truncate to only cover iv and actual ciphertext
ciphertext = ciphertext[:ivSize+len(plaintext)]
mac := poly1305MAC(ciphertext[ivSize:], ciphertext[:ivSize], &ks.MAC)
mac := poly1305MAC(ciphertext[ivSize:], ciphertext[:ivSize], &k.MACKey)
ciphertext = append(ciphertext, mac...)
return ciphertext, nil
@@ -270,8 +279,8 @@ func Encrypt(ks *Key, ciphertext []byte, plaintext []byte) ([]byte, error) {
// Decrypt verifies and decrypts the ciphertext. Ciphertext must be in the form
// IV || Ciphertext || MAC. plaintext and ciphertext may point to (exactly) the
// same slice.
func Decrypt(ks *Key, plaintext []byte, ciphertextWithMac []byte) (int, error) {
if !ks.Valid() {
func (k *Key) Decrypt(plaintext []byte, ciphertextWithMac []byte) (int, error) {
if !k.Valid() {
return 0, errors.New("invalid key")
}
@@ -291,7 +300,7 @@ func Decrypt(ks *Key, plaintext []byte, ciphertextWithMac []byte) (int, error) {
ciphertextWithIV, mac := ciphertextWithMac[:l], ciphertextWithMac[l:]
// verify mac
if !poly1305Verify(ciphertextWithIV[ivSize:], ciphertextWithIV[:ivSize], &ks.MAC, mac) {
if !poly1305Verify(ciphertextWithIV[ivSize:], ciphertextWithIV[:ivSize], &k.MACKey, mac) {
return 0, ErrUnauthenticated
}
@@ -303,7 +312,7 @@ func Decrypt(ks *Key, plaintext []byte, ciphertextWithMac []byte) (int, error) {
}
// decrypt data
c, err := aes.NewCipher(ks.Encrypt[:])
c, err := aes.NewCipher(k.EncryptionKey[:])
if err != nil {
panic(fmt.Sprintf("unable to create cipher: %v", err))
}
@@ -318,5 +327,5 @@ func Decrypt(ks *Key, plaintext []byte, ciphertextWithMac []byte) (int, error) {
// Valid tests if the key is valid.
func (k *Key) Valid() bool {
return k.Encrypt.Valid() && k.MAC.Valid()
return k.EncryptionKey.Valid() && k.MACKey.Valid()
}

View File

@@ -90,18 +90,18 @@ func TestCrypto(t *testing.T) {
for _, tv := range testValues {
// test encryption
k := &Key{
Encrypt: tv.ekey,
MAC: tv.skey,
EncryptionKey: tv.ekey,
MACKey: tv.skey,
}
msg, err := Encrypt(k, msg, tv.plaintext)
msg, err := k.Encrypt(msg, tv.plaintext)
if err != nil {
t.Fatal(err)
}
// decrypt message
buf := make([]byte, len(tv.plaintext))
n, err := Decrypt(k, buf, msg)
n, err := k.Decrypt(buf, msg)
if err != nil {
t.Fatal(err)
}
@@ -110,7 +110,7 @@ func TestCrypto(t *testing.T) {
// change mac, this must fail
msg[len(msg)-8] ^= 0x23
if _, err = Decrypt(k, buf, msg); err != ErrUnauthenticated {
if _, err = k.Decrypt(buf, msg); err != ErrUnauthenticated {
t.Fatal("wrong MAC value not detected")
}
@@ -120,13 +120,13 @@ func TestCrypto(t *testing.T) {
// tamper with message, this must fail
msg[16+5] ^= 0x85
if _, err = Decrypt(k, buf, msg); err != ErrUnauthenticated {
if _, err = k.Decrypt(buf, msg); err != ErrUnauthenticated {
t.Fatal("tampered message not detected")
}
// test decryption
p := make([]byte, len(tv.ciphertext))
n, err = Decrypt(k, p, tv.ciphertext)
n, err = k.Decrypt(p, tv.ciphertext)
if err != nil {
t.Fatal(err)
}

View File

@@ -26,14 +26,14 @@ func TestEncryptDecrypt(t *testing.T) {
data := Random(42, size)
buf := make([]byte, size+crypto.Extension)
ciphertext, err := crypto.Encrypt(k, buf, data)
ciphertext, err := k.Encrypt(buf, data)
OK(t, err)
Assert(t, len(ciphertext) == len(data)+crypto.Extension,
"ciphertext length does not match: want %d, got %d",
len(data)+crypto.Extension, len(ciphertext))
plaintext := make([]byte, len(ciphertext))
n, err := crypto.Decrypt(k, plaintext, ciphertext)
n, err := k.Decrypt(plaintext, ciphertext)
OK(t, err)
plaintext = plaintext[:n]
Assert(t, len(plaintext) == len(data),
@@ -53,7 +53,7 @@ func TestSmallBuffer(t *testing.T) {
OK(t, err)
ciphertext := make([]byte, size/2)
ciphertext, err = crypto.Encrypt(k, ciphertext, data)
ciphertext, err = k.Encrypt(ciphertext, data)
// this must extend the slice
Assert(t, cap(ciphertext) > size/2,
"expected extended slice, but capacity is only %d bytes",
@@ -61,7 +61,7 @@ func TestSmallBuffer(t *testing.T) {
// check for the correct plaintext
plaintext := make([]byte, len(ciphertext))
n, err := crypto.Decrypt(k, plaintext, ciphertext)
n, err := k.Decrypt(plaintext, ciphertext)
OK(t, err)
plaintext = plaintext[:n]
Assert(t, bytes.Equal(plaintext, data),
@@ -78,11 +78,11 @@ func TestSameBuffer(t *testing.T) {
ciphertext := make([]byte, 0, size+crypto.Extension)
ciphertext, err = crypto.Encrypt(k, ciphertext, data)
ciphertext, err = k.Encrypt(ciphertext, data)
OK(t, err)
// use the same buffer for decryption
n, err := crypto.Decrypt(k, ciphertext, ciphertext)
n, err := k.Decrypt(ciphertext, ciphertext)
OK(t, err)
ciphertext = ciphertext[:n]
Assert(t, bytes.Equal(ciphertext, data),
@@ -94,7 +94,7 @@ func TestCornerCases(t *testing.T) {
// nil plaintext should encrypt to the empty string
// nil ciphertext should allocate a new slice for the ciphertext
c, err := crypto.Encrypt(k, nil, nil)
c, err := k.Encrypt(nil, nil)
OK(t, err)
Assert(t, len(c) == crypto.Extension,
@@ -102,12 +102,12 @@ func TestCornerCases(t *testing.T) {
len(c))
// this should decrypt to nil
n, err := crypto.Decrypt(k, nil, c)
n, err := k.Decrypt(nil, c)
OK(t, err)
Equals(t, 0, n)
// test encryption for same slice, this should return an error
_, err = crypto.Encrypt(k, c, c)
_, err = k.Encrypt(c, c)
Equals(t, crypto.ErrInvalidCiphertext, err)
}
@@ -123,10 +123,10 @@ func TestLargeEncrypt(t *testing.T) {
_, err := io.ReadFull(rand.Reader, data)
OK(t, err)
ciphertext, err := crypto.Encrypt(k, make([]byte, size+crypto.Extension), data)
ciphertext, err := k.Encrypt(make([]byte, size+crypto.Extension), data)
OK(t, err)
plaintext, err := crypto.Decrypt(k, []byte{}, ciphertext)
plaintext, err := k.Decrypt([]byte{}, ciphertext)
OK(t, err)
Equals(t, plaintext, data)
@@ -144,7 +144,7 @@ func BenchmarkEncrypt(b *testing.B) {
b.SetBytes(int64(size))
for i := 0; i < b.N; i++ {
_, err := crypto.Encrypt(k, buf, data)
_, err := k.Encrypt(buf, data)
OK(b, err)
}
}
@@ -158,14 +158,14 @@ func BenchmarkDecrypt(b *testing.B) {
plaintext := make([]byte, size)
ciphertext := make([]byte, size+crypto.Extension)
ciphertext, err := crypto.Encrypt(k, ciphertext, data)
ciphertext, err := k.Encrypt(ciphertext, data)
OK(b, err)
b.ResetTimer()
b.SetBytes(int64(size))
for i := 0; i < b.N; i++ {
_, err = crypto.Decrypt(k, plaintext, ciphertext)
_, err = k.Decrypt(plaintext, ciphertext)
OK(b, err)
}
}

View File

@@ -81,10 +81,10 @@ func KDF(p KDFParams, salt []byte, password string) (*Key, error) {
}
// first 32 byte of scrypt output is the encryption key
copy(derKeys.Encrypt[:], scryptKeys[:aesKeySize])
copy(derKeys.EncryptionKey[:], scryptKeys[:aesKeySize])
// next 32 byte of scrypt output is the mac key, in the form k||r
macKeyFromSlice(&derKeys.MAC, scryptKeys[aesKeySize:])
macKeyFromSlice(&derKeys.MACKey, scryptKeys[aesKeySize:])
return derKeys, nil
}

View File

@@ -52,7 +52,9 @@ func (rd *eofDetectReader) Close() error {
func (tr eofDetectRoundTripper) RoundTrip(req *http.Request) (res *http.Response, err error) {
res, err = tr.RoundTripper.RoundTrip(req)
res.Body = &eofDetectReader{rd: res.Body}
if res != nil && res.Body != nil {
res.Body = &eofDetectReader{rd: res.Body}
}
return res, err
}

View File

@@ -1,12 +1,14 @@
package restic
import "context"
// FindUsedBlobs traverses the tree ID and adds all seen blobs (trees and data
// blobs) to the set blobs. The tree blobs in the `seen` BlobSet will not be visited
// again.
func FindUsedBlobs(repo Repository, treeID ID, blobs BlobSet, seen BlobSet) error {
func FindUsedBlobs(ctx context.Context, repo Repository, treeID ID, blobs BlobSet, seen BlobSet) error {
blobs.Insert(BlobHandle{ID: treeID, Type: TreeBlob})
tree, err := repo.LoadTree(treeID)
tree, err := repo.LoadTree(ctx, treeID)
if err != nil {
return err
}
@@ -26,7 +28,7 @@ func FindUsedBlobs(repo Repository, treeID ID, blobs BlobSet, seen BlobSet) erro
seen.Insert(h)
err := FindUsedBlobs(repo, subtreeID, blobs, seen)
err := FindUsedBlobs(ctx, repo, subtreeID, blobs, seen)
if err != nil {
return err
}

View File

@@ -2,6 +2,7 @@ package restic_test
import (
"bufio"
"context"
"encoding/json"
"flag"
"fmt"
@@ -92,7 +93,7 @@ func TestFindUsedBlobs(t *testing.T) {
for i, sn := range snapshots {
usedBlobs := restic.NewBlobSet()
err := restic.FindUsedBlobs(repo, *sn.Tree, usedBlobs, restic.NewBlobSet())
err := restic.FindUsedBlobs(context.TODO(), repo, *sn.Tree, usedBlobs, restic.NewBlobSet())
if err != nil {
t.Errorf("FindUsedBlobs returned error: %v", err)
continue
@@ -128,7 +129,7 @@ func BenchmarkFindUsedBlobs(b *testing.B) {
for i := 0; i < b.N; i++ {
seen := restic.NewBlobSet()
blobs := restic.NewBlobSet()
err := restic.FindUsedBlobs(repo, *sn.Tree, blobs, seen)
err := restic.FindUsedBlobs(context.TODO(), repo, *sn.Tree, blobs, seen)
if err != nil {
b.Error(err)
}

View File

@@ -0,0 +1,36 @@
// +build !openbsd
// +build !windows
package fuse
import (
"restic"
"golang.org/x/net/context"
)
// BlobSizeCache caches the size of blobs in the repo.
type BlobSizeCache struct {
m map[restic.ID]uint
}
// NewBlobSizeCache returns a new blob size cache containing all entries from midx.
func NewBlobSizeCache(ctx context.Context, idx restic.Index) *BlobSizeCache {
m := make(map[restic.ID]uint, 1000)
for pb := range idx.Each(ctx) {
m[pb.ID] = uint(restic.PlaintextLength(int(pb.Length)))
}
return &BlobSizeCache{
m: m,
}
}
// Lookup returns the size of the blob id.
func (c *BlobSizeCache) Lookup(id restic.ID) (size uint, found bool) {
if c == nil {
return 0, false
}
size, found = c.m[id]
return size, found
}

View File

@@ -19,16 +19,18 @@ var _ = fs.HandleReadDirAller(&dir{})
var _ = fs.NodeStringLookuper(&dir{})
type dir struct {
repo restic.Repository
root *Root
items map[string]*restic.Node
inode uint64
parentInode uint64
node *restic.Node
ownerIsRoot bool
blobsize *BlobSizeCache
}
func newDir(repo restic.Repository, node *restic.Node, ownerIsRoot bool) (*dir, error) {
func newDir(ctx context.Context, root *Root, inode, parentInode uint64, node *restic.Node) (*dir, error) {
debug.Log("new dir for %v (%v)", node.Name, node.Subtree.Str())
tree, err := repo.LoadTree(*node.Subtree)
tree, err := root.repo.LoadTree(ctx, *node.Subtree)
if err != nil {
debug.Log(" error loading tree %v: %v", node.Subtree.Str(), err)
return nil, err
@@ -39,17 +41,17 @@ func newDir(repo restic.Repository, node *restic.Node, ownerIsRoot bool) (*dir,
}
return &dir{
repo: repo,
root: root,
node: node,
items: items,
inode: node.Inode,
ownerIsRoot: ownerIsRoot,
inode: inode,
parentInode: parentInode,
}, nil
}
// replaceSpecialNodes replaces nodes with name "." and "/" by their contents.
// Otherwise, the node is returned.
func replaceSpecialNodes(repo restic.Repository, node *restic.Node) ([]*restic.Node, error) {
func replaceSpecialNodes(ctx context.Context, repo restic.Repository, node *restic.Node) ([]*restic.Node, error) {
if node.Type != "dir" || node.Subtree == nil {
return []*restic.Node{node}, nil
}
@@ -58,7 +60,7 @@ func replaceSpecialNodes(repo restic.Repository, node *restic.Node) ([]*restic.N
return []*restic.Node{node}, nil
}
tree, err := repo.LoadTree(*node.Subtree)
tree, err := repo.LoadTree(ctx, *node.Subtree)
if err != nil {
return nil, err
}
@@ -66,16 +68,16 @@ func replaceSpecialNodes(repo restic.Repository, node *restic.Node) ([]*restic.N
return tree.Nodes, nil
}
func newDirFromSnapshot(repo restic.Repository, snapshot SnapshotWithId, ownerIsRoot bool) (*dir, error) {
debug.Log("new dir for snapshot %v (%v)", snapshot.ID.Str(), snapshot.Tree.Str())
tree, err := repo.LoadTree(*snapshot.Tree)
func newDirFromSnapshot(ctx context.Context, root *Root, inode uint64, snapshot *restic.Snapshot) (*dir, error) {
debug.Log("new dir for snapshot %v (%v)", snapshot.ID().Str(), snapshot.Tree.Str())
tree, err := root.repo.LoadTree(ctx, *snapshot.Tree)
if err != nil {
debug.Log(" loadTree(%v) failed: %v", snapshot.ID.Str(), err)
debug.Log(" loadTree(%v) failed: %v", snapshot.ID().Str(), err)
return nil, err
}
items := make(map[string]*restic.Node)
for _, n := range tree.Nodes {
nodes, err := replaceSpecialNodes(repo, n)
nodes, err := replaceSpecialNodes(ctx, root.repo, n)
if err != nil {
debug.Log(" replaceSpecialNodes(%v) failed: %v", n, err)
return nil, err
@@ -87,7 +89,7 @@ func newDirFromSnapshot(repo restic.Repository, snapshot SnapshotWithId, ownerIs
}
return &dir{
repo: repo,
root: root,
node: &restic.Node{
UID: uint32(os.Getuid()),
GID: uint32(os.Getgid()),
@@ -96,9 +98,8 @@ func newDirFromSnapshot(repo restic.Repository, snapshot SnapshotWithId, ownerIs
ChangeTime: snapshot.Time,
Mode: os.ModeDir | 0555,
},
items: items,
inode: inodeFromBackendID(snapshot.ID),
ownerIsRoot: ownerIsRoot,
items: items,
inode: inode,
}, nil
}
@@ -107,7 +108,7 @@ func (d *dir) Attr(ctx context.Context, a *fuse.Attr) error {
a.Inode = d.inode
a.Mode = os.ModeDir | d.node.Mode
if !d.ownerIsRoot {
if !d.root.cfg.OwnerIsRoot {
a.Uid = d.node.UID
a.Gid = d.node.GID
}
@@ -135,7 +136,19 @@ func (d *dir) calcNumberOfLinks() uint32 {
func (d *dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
debug.Log("called")
ret := make([]fuse.Dirent, 0, len(d.items))
ret := make([]fuse.Dirent, 0, len(d.items)+2)
ret = append(ret, fuse.Dirent{
Inode: d.inode,
Name: ".",
Type: fuse.DT_Dir,
})
ret = append(ret, fuse.Dirent{
Inode: d.parentInode,
Name: "..",
Type: fuse.DT_Dir,
})
for _, node := range d.items {
var typ fuse.DirentType
@@ -149,7 +162,7 @@ func (d *dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
}
ret = append(ret, fuse.Dirent{
Inode: node.Inode,
Inode: fs.GenerateDynamicInode(d.inode, node.Name),
Type: typ,
Name: node.Name,
})
@@ -167,11 +180,11 @@ func (d *dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
}
switch node.Type {
case "dir":
return newDir(d.repo, node, d.ownerIsRoot)
return newDir(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), d.inode, node)
case "file":
return newFile(d.repo, node, d.ownerIsRoot)
return newFile(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), node)
case "symlink":
return newLink(d.repo, node, d.ownerIsRoot)
return newLink(ctx, d.root, fs.GenerateDynamicInode(d.inode, name), node)
default:
debug.Log(" node %v has unknown type %v", name, node.Type)
return nil, fuse.ENOENT

View File

@@ -21,32 +21,26 @@ const blockSize = 512
var _ = fs.HandleReader(&file{})
var _ = fs.HandleReleaser(&file{})
// BlobLoader is an abstracted repository with a reduced set of methods used
// for fuse operations.
type BlobLoader interface {
LookupBlobSize(restic.ID, restic.BlobType) (uint, error)
LoadBlob(restic.BlobType, restic.ID, []byte) (int, error)
}
type file struct {
repo BlobLoader
node *restic.Node
ownerIsRoot bool
root *Root
node *restic.Node
inode uint64
sizes []int
blobs [][]byte
}
const defaultBlobSize = 128 * 1024
func newFile(repo BlobLoader, node *restic.Node, ownerIsRoot bool) (*file, error) {
func newFile(ctx context.Context, root *Root, inode uint64, node *restic.Node) (fusefile *file, err error) {
debug.Log("create new file for %v with %d blobs", node.Name, len(node.Content))
var bytes uint64
sizes := make([]int, len(node.Content))
for i, id := range node.Content {
size, err := repo.LookupBlobSize(id, restic.DataBlob)
if err != nil {
return nil, err
size, ok := root.blobSizeCache.Lookup(id)
if !ok {
size, err = root.repo.LookupBlobSize(id, restic.DataBlob)
if err != nil {
return nil, err
}
}
sizes[i] = int(size)
@@ -59,24 +53,24 @@ func newFile(repo BlobLoader, node *restic.Node, ownerIsRoot bool) (*file, error
}
return &file{
repo: repo,
node: node,
sizes: sizes,
blobs: make([][]byte, len(node.Content)),
ownerIsRoot: ownerIsRoot,
inode: inode,
root: root,
node: node,
sizes: sizes,
blobs: make([][]byte, len(node.Content)),
}, nil
}
func (f *file) Attr(ctx context.Context, a *fuse.Attr) error {
debug.Log("Attr(%v)", f.node.Name)
a.Inode = f.node.Inode
a.Inode = f.inode
a.Mode = f.node.Mode
a.Size = f.node.Size
a.Blocks = (f.node.Size / blockSize) + 1
a.BlockSize = blockSize
a.Nlink = uint32(f.node.Links)
if !f.ownerIsRoot {
if !f.root.cfg.OwnerIsRoot {
a.Uid = f.node.UID
a.Gid = f.node.GID
}
@@ -88,7 +82,7 @@ func (f *file) Attr(ctx context.Context, a *fuse.Attr) error {
}
func (f *file) getBlobAt(i int) (blob []byte, err error) {
func (f *file) getBlobAt(ctx context.Context, i int) (blob []byte, err error) {
debug.Log("getBlobAt(%v, %v)", f.node.Name, i)
if f.blobs[i] != nil {
return f.blobs[i], nil
@@ -100,7 +94,7 @@ func (f *file) getBlobAt(i int) (blob []byte, err error) {
}
buf := restic.NewBlobBuffer(f.sizes[i])
n, err := f.repo.LoadBlob(restic.DataBlob, f.node.Content[i], buf)
n, err := f.root.repo.LoadBlob(ctx, restic.DataBlob, f.node.Content[i], buf)
if err != nil {
debug.Log("LoadBlob(%v, %v) failed: %v", f.node.Name, f.node.Content[i], err)
return nil, err
@@ -137,7 +131,7 @@ func (f *file) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadR
readBytes := 0
remainingBytes := req.Size
for i := startContent; remainingBytes > 0 && i < len(f.sizes); i++ {
blob, err := f.getBlobAt(i)
blob, err := f.getBlobAt(ctx, i)
if err != nil {
return err
}

Some files were not shown because too many files have changed in this diff Show More