Compare commits

...

568 Commits

Author SHA1 Message Date
Alexander Neumann
820c88ea73 Add VERSION file for 0.6.0 2017-05-29 21:12:52 +02:00
Alexander Neumann
17ff41af47 Update version number in CHANGELOG.md 2017-05-25 15:42:44 +02:00
Alexander Neumann
772f647a73 Add VERSION file for 0.6.0-rc.1 2017-05-25 15:27:45 +02:00
Alexander Neumann
5c4facd9dc Update CHANGELOG.md 2017-05-25 15:26:22 +02:00
Alexander Neumann
40ad1df6ee Merge pull request #971 from restic/add-switch-enable-cgo
build.go: Add --enable-cgo
2017-05-25 11:28:40 +02:00
Alexander Neumann
c49da4c6f7 build.go: Add --enable-cgo 2017-05-25 11:03:29 +02:00
Alexander Neumann
d1cc87ba28 Merge pull request #966 from restic/unify-repo-layout
WIP: Unify repository layout
2017-05-22 21:15:38 +02:00
Alexander Neumann
89be1d496a Improve issue template wording 2017-05-21 10:09:49 +02:00
Alexander Neumann
69e114c5c4 Merge pull request #968 from mcastilho/patch-2
Fixing CPUProfile
2017-05-17 21:13:42 +02:00
Alexander Neumann
5d9a4ce90c Merge pull request #967 from middelink/gofmt
Fix a number of warnings from go report
2017-05-17 21:12:59 +02:00
Marcio Castilho
84f1037b8f Update global_debug.go
The variable was wrong and the CPU profile was never being created.
2017-05-17 11:07:33 -04:00
Pauline Middelink
120af801cf Fix golint warnings
(except the exported fields/functions without comments)
2017-05-17 01:39:39 +02:00
Pauline Middelink
f3d09ce7c8 Fix vet warnings 2017-05-17 01:34:33 +02:00
Pauline Middelink
2b9323529f Fix gofmt -s warnings 2017-05-17 01:28:39 +02:00
Pauline Middelink
971ecee171 Fix ineffassign mistakes 2017-05-17 01:25:52 +02:00
Alexander Neumann
4640d1a28e Add codecov.yml 2017-05-16 20:52:42 +02:00
Alexander Neumann
8b461a7456 Make TestFlags less verbose 2017-05-16 20:49:18 +02:00
Alexander Neumann
2fa1238b8a Fix filenames for layout tests 2017-05-16 20:45:17 +02:00
Alexander Neumann
55ae5dab2b design: Add s3.layout option 2017-05-15 23:37:16 +02:00
Alexander Neumann
fa41183a53 s3: Add s3.layout option and layout auto detection 2017-05-15 23:37:02 +02:00
Alexander Neumann
959aa0f595 Simplify layout detection 2017-05-15 23:36:23 +02:00
Alexander Neumann
069752cb42 Make layout default Dirname() consistent
Always return a trailing slash now.
2017-05-15 23:35:52 +02:00
Alexander Neumann
0c537837d9 Rename testdata files 2017-05-15 22:39:50 +02:00
Alexander Neumann
f19852a738 Rename CloudLayout -> RESTLayout
The REST backend uses a special layout without subdirs below data/. This
layout is just used there and nowhere else, and our REST server
implementation uses the default layout for the on disk storage. So we
remove the REST layout from the auto detection code.
2017-05-15 22:05:43 +02:00
Alexander Neumann
61cade6222 Rename layout s3 -> s3legacy 2017-05-15 21:59:28 +02:00
Alexander Neumann
a4e3a0dd97 design: Remove cloud layout 2017-05-15 21:51:19 +02:00
Alexander Neumann
22a6cd3a26 Merge pull request #964 from restic/update-minio-go
Update github.com/minio/minio-go
2017-05-15 21:15:28 +02:00
Alexander Neumann
7f10828dfa Update github.com/minio/minio-go 2017-05-15 20:47:50 +02:00
Alexander Neumann
04264c6338 Remove doc/PKGBUILD 2017-05-15 19:48:57 +02:00
Alexander Neumann
13393c76dc Merge pull request #957 from middelink/fix-953
Change backup policy to be inclusive
2017-05-15 17:19:04 +02:00
Alexander Neumann
13c12ca83d Fix PKGBUILD
Closes #963
2017-05-15 17:17:23 +02:00
Pauline Middelink
7fffd408af Do not use singleton counter.
Revert change for running the numbered tests as subtests.
2017-05-15 08:53:59 +02:00
Alexander Neumann
089b04c8a9 Merge pull request #962 from restic/improve-s3-partial-download
Improve s3 backend
2017-05-14 22:00:16 +02:00
Alexander Neumann
b192dacc13 Add text to the issue template 2017-05-14 21:37:45 +02:00
Alexander Neumann
250a45ab15 s3: Move interfaces to function 2017-05-14 20:36:26 +02:00
Alexander Neumann
3b44b87137 s3: Remove file after usage in test 2017-05-14 20:34:22 +02:00
Alexander Neumann
26c16b9fd3 s3: Correctly return token in Load 2017-05-14 20:19:12 +02:00
Alexander Neumann
0bd40bae6e s3: Prevent net/http client from closing the reader 2017-05-14 20:19:12 +02:00
Alexander Neumann
246ccf09b9 s3: add getRemainingSize 2017-05-14 20:19:12 +02:00
Alexander Neumann
9452f416bf s3: Use low level API for saving files
benchmark                        old ns/op       new ns/op       delta
    BenchmarkBackendMinio/Save-4     184482294       40663344        -77.96%
    BenchmarkBackendS3/Save-4        35030825568     54475455819     +55.51%

    benchmark                        old MB/s     new MB/s     speedup
    BenchmarkBackendMinio/Save-4     90.95        412.64       4.54x
    BenchmarkBackendS3/Save-4        0.48         0.31         0.65x

    benchmark                        old allocs     new allocs     delta
    BenchmarkBackendMinio/Save-4     631            560            -11.25%
    BenchmarkBackendS3/Save-4        646            584            -9.60%

    benchmark                        old bytes     new bytes     delta
    BenchmarkBackendMinio/Save-4     66818060      50735         -99.92%
    BenchmarkBackendS3/Save-4        66834000      73024         -99.89%
2017-05-14 20:19:12 +02:00
Alexander Neumann
1e0e6ee573 s3: Use low-level API with a Range header for Load
benchmark                                         old ns/op      new ns/op      delta
    BenchmarkBackendMinio/LoadFile-4                  9213315        11001787       +19.41%
    BenchmarkBackendMinio/LoadPartialFile-4           4176619        3479707        -16.69%
    BenchmarkBackendMinio/LoadPartialFileOffset-4     4391521        3139214        -28.52%
    BenchmarkBackendS3/LoadFile-4                     2886070905     2505907501     -13.17%
    BenchmarkBackendS3/LoadPartialFile-4              762702722      735694398      -3.54%
    BenchmarkBackendS3/LoadPartialFileOffset-4        789724328      1108989142     +40.43%

    benchmark                                         old MB/s     new MB/s     speedup
    BenchmarkBackendMinio/LoadFile-4                  1821.21      1525.15      0.84x
    BenchmarkBackendMinio/LoadPartialFile-4           1004.49      1205.67      1.20x
    BenchmarkBackendMinio/LoadPartialFileOffset-4     955.34       1336.45      1.40x
    BenchmarkBackendS3/LoadFile-4                     5.81         6.70         1.15x
    BenchmarkBackendS3/LoadPartialFile-4              5.50         5.70         1.04x
    BenchmarkBackendS3/LoadPartialFileOffset-4        5.31         3.78         0.71x

    benchmark                                         old allocs     new allocs     delta
    BenchmarkBackendMinio/LoadFile-4                  406            204            -49.75%
    BenchmarkBackendMinio/LoadPartialFile-4           225            206            -8.44%
    BenchmarkBackendMinio/LoadPartialFileOffset-4     227            207            -8.81%
    BenchmarkBackendS3/LoadFile-4                     600            388            -35.33%
    BenchmarkBackendS3/LoadPartialFile-4              416            302            -27.40%
    BenchmarkBackendS3/LoadPartialFileOffset-4        417            303            -27.34%

    benchmark                                         old bytes     new bytes     delta
    BenchmarkBackendMinio/LoadFile-4                  29475         13904         -52.83%
    BenchmarkBackendMinio/LoadPartialFile-4           4218838       13958         -99.67%
    BenchmarkBackendMinio/LoadPartialFileOffset-4     4219175       14332         -99.66%
    BenchmarkBackendS3/LoadFile-4                     114152        97424         -14.65%
    BenchmarkBackendS3/LoadPartialFile-4              4265416       56212         -98.68%
    BenchmarkBackendS3/LoadPartialFileOffset-4        4266520       56308         -98.68%
2017-05-14 20:19:12 +02:00
Alexander Neumann
be0e53c07b tests: Add test for backend Save() from file 2017-05-14 19:55:32 +02:00
Alexander Neumann
9dd58196e3 Merge pull request #961 from restic/add-backend-benchmarks
Add backend benchmarks
2017-05-14 19:49:33 +02:00
Alexander Neumann
66b4999765 tests: Remove code generation, use reflection
This simplifies the code.
2017-05-14 12:59:07 +02:00
Alexander Neumann
403e201e1a tests: Improve robustness of config tests 2017-05-14 12:50:20 +02:00
Alexander Neumann
77a55fbe5c tests: Add documentation 2017-05-14 11:48:30 +02:00
Alexander Neumann
90c1608d88 sftp: Add Benchmarks 2017-05-14 11:48:30 +02:00
Alexander Neumann
7a51640262 rest: Add benchmarks 2017-05-14 11:48:30 +02:00
Alexander Neumann
13946e7db7 mem: Add benchmarks 2017-05-14 11:48:30 +02:00
Alexander Neumann
e009c002ba local: Add benchmark 2017-05-14 11:48:30 +02:00
Alexander Neumann
d24e0cc6cc s3: Add benchmarks 2017-05-14 11:48:30 +02:00
Alexander Neumann
5b8131e2d3 tests: Add benchmark for Save 2017-05-14 11:48:30 +02:00
Alexander Neumann
8fc25cc567 tests: Add benchmarks for partial file (+offset) 2017-05-14 11:48:30 +02:00
Alexander Neumann
77ebb95d3d tests: Add BenchmarkLoadFile 2017-05-14 11:48:30 +02:00
Alexander Neumann
f142b1c22f tests: Add benchmarks to test suite 2017-05-14 11:48:30 +02:00
Alexander Neumann
1c9159d6a0 Merge pull request #960 from restic/travis-run-tests-against-s3
Enable CI tests against AWS S3
2017-05-14 10:26:16 +02:00
Alexander Neumann
82e15dc6dc Allow skipping the test against s3
If RESTIC_TEST_S3_REPOSITORY is not available it means we're probably
running on Travis for a third-party PR, so the access credentials for S3
are not exposed.
2017-05-14 00:33:19 +02:00
Alexander Neumann
ee68f9298b tests: Ensure that backend tests cannot be skipped on Travis 2017-05-14 00:33:15 +02:00
Alexander Neumann
5c6ec78789 s3: Remove log line 2017-05-13 19:58:25 +02:00
Alexander Neumann
4ac0d3ad40 backend tests load: Use reader with Size() method 2017-05-13 19:56:11 +02:00
Harshavardhana
c7209ef231 api: Failed call retry with region only when http.StatusBadRequest.
Fixes https://github.com/minio/minio-go/issues/677
2017-05-13 19:53:24 +02:00
Alexander Neumann
0249c16b04 Revert "minio: Apply fix"
This reverts commit f43d34899d.
2017-05-13 19:41:55 +02:00
Alexander Neumann
f43d34899d minio: Apply fix
https://github.com/minio/minio-go/issues/677
2017-05-13 13:18:42 +02:00
Alexander Neumann
19daefd04e Merge pull request #938 from restic/rework-backend-tests
WIP: rework backend integration tests
2017-05-12 22:09:57 +02:00
Alexander Neumann
ec5e984ed9 Increase waiting time for network services 2017-05-12 21:45:18 +02:00
Alexander Neumann
88de3cfecc rest: Wait until REST server is started 2017-05-12 21:33:34 +02:00
Alexander Neumann
a654f41ddb Add docstring for package test 2017-05-12 21:19:32 +02:00
Alexander Neumann
fbf2462325 Rename backend test functions 2017-05-12 21:06:49 +02:00
Alexander Neumann
85e4831198 Remove stuff from run_integration_tests.go 2017-05-12 21:04:04 +02:00
Alexander Neumann
091dc29f72 doc: Fix s3 tutorial 2017-05-12 21:04:04 +02:00
Alexander Neumann
59d1986660 s3: Use random prefix for tests 2017-05-12 21:04:04 +02:00
Alexander Neumann
db4fa48f66 rest: Add context 2017-05-12 21:04:04 +02:00
Alexander Neumann
f2b83ece10 s3: Ignore errors for terminated minio server 2017-05-12 21:04:04 +02:00
Alexander Neumann
860b52273e s3: Add tests for AWS 2017-05-12 21:04:04 +02:00
Alexander Neumann
bafceb56fb s3: Convert to test suite, run Minio automatically 2017-05-12 21:04:04 +02:00
Alexander Neumann
9271b3662a sftp: ignore error 2017-05-12 21:04:04 +02:00
Alexander Neumann
554013ca9f rest: Convert to test suite 2017-05-12 21:04:04 +02:00
Alexander Neumann
3250fdc2ca mem: Convert to test suite 2017-05-12 21:04:04 +02:00
Alexander Neumann
3678d34b16 sftp: Convert to test suite 2017-05-12 21:04:04 +02:00
Alexander Neumann
16276853a1 local: Convert to test suite 2017-05-12 21:04:04 +02:00
Alexander Neumann
f10c24e404 Improve test helpers 2017-05-12 21:04:04 +02:00
Alexander Neumann
6f5fd72738 Implement test suite for backend tests 2017-05-12 21:03:55 +02:00
Pauline Middelink
103a491ac0 Make houndci-bot happy. 2017-05-11 22:38:12 +02:00
Pauline Middelink
929f90344e Change backup policy to be inclusive, meaning all given policies
are evaluated for each snapshot, thereby making sure that each
keep-* is able to retain its most recent snapshot. Thereby insuring
that weeklies keep Sundays around and monthlies keep the last day of
the month around.

Added testcase to make sure when multiple --keep-tags are given,
ALL of them need to match.
2017-05-11 22:34:50 +02:00
Alexander Neumann
a963052d64 Add custom HTTP transport 2017-05-11 21:51:30 +02:00
Alexander Neumann
898613e14f debug: Add RoundTripper 2017-05-11 21:51:30 +02:00
Alexander Neumann
b84e63d503 backend.LoadAll: Check errors from io.Copy() 2017-05-11 21:51:30 +02:00
Alexander Neumann
63870d2830 Check error for Close() 2017-05-11 21:51:30 +02:00
Alexander Neumann
54e46f5984 Add MinimalData option for backend tests 2017-05-11 21:51:30 +02:00
Alexander Neumann
0096eca7fe Install log buffer for global logs
This hides logged message from the net/http library and only shows them
in case an error occurs.
2017-05-11 21:51:30 +02:00
Pauline Middelink
ce3acbd30a Remove unused SnapshotFilter and FilterSnapshots(). 2017-05-11 21:07:58 +02:00
Alexander Neumann
c5ae5524ff Remove codecov.yml 2017-05-10 20:52:06 +02:00
Alexander Neumann
fbf5a8123b Merge pull request #952 from restic/remove-temp-files
Unlink tempfiles before using them
2017-05-10 20:25:07 +02:00
Alexander Neumann
edbd6ad584 Add fs.TempFile and fs.RemoveIfExists 2017-05-10 19:48:22 +02:00
Alexander Neumann
faf11c4a46 Fix link in README.rst
closes #950
2017-05-08 21:31:11 +02:00
Alexander Neumann
68a9e5f963 Merge pull request #947 from restic/correct-codecov
codecov.io: Add path fixes
2017-05-07 10:59:20 +02:00
Alexander Neumann
fa7f0ef44f codecov.io: Add path fixes 2017-05-07 10:41:32 +02:00
Alexander Neumann
bcfe7afbfd Merge pull request #945 from middelink/fix-942
Add bash autocompletion generation
2017-05-07 10:30:10 +02:00
Alexander Neumann
46e6e8984d Merge pull request #941 from restic/improve-mkdir
local/sftp: Auto create directories if needed
2017-05-07 10:26:39 +02:00
Alexander Neumann
0f057bd440 Merge pull request #943 from middelink/fix-names
Small changes to cmd_forget and snapshot_filter
2017-05-07 10:15:21 +02:00
Alexander Neumann
ba91a76f5f Merge pull request #946 from restic/update-minio-go
Update github.com/minio/minio-go
2017-05-07 09:54:55 +02:00
Alexander Neumann
c09e005cc8 Update github.com/minio/minio-go
This corrects a severe performance regression when the current minio
server is used as a backend, see
https://github.com/minio/minio/issues/4275
2017-05-07 09:36:49 +02:00
Pauline Middelink
2c42629c51 Add bash autocompletion generation
Fix #942
2017-05-06 12:39:23 +02:00
Pauline Middelink
f93e5a39e5 Small textual changes to apply, to reflect actual meaning.
Added tests for the ExpirePolicy operations.
2017-05-04 16:39:41 +02:00
Pauline Middelink
02eafe94b3 Use global context in cmd_forget, like all other cmds 2017-05-04 16:35:35 +02:00
Alexander Neumann
7d914768bb local/sftp: Auto create directories if needed
As reported in #940
2017-05-03 21:19:22 +02:00
Alexander Neumann
db6541b24c Merge pull request #936 from Thor77/manual-fix-rst-links
Fix links broken during conversion to rst
2017-04-30 19:08:20 +02:00
Thor77
228566c0a1 Fix links broken during conversion to rst 2017-04-30 18:25:45 +02:00
Alexander Neumann
1e4bcf3367 design.rst: Fix tree IDs in example
Closes #913
2017-04-29 20:15:30 +02:00
Alexander Neumann
51e7d26053 Merge pull request #935 from restic/add-glyphcheck
CI: Add glyphcheck, rework REST server install
2017-04-27 16:16:07 +02:00
Alexander Neumann
e42627d2cb CI: Add glyphcheck, rework REST server install 2017-04-27 16:01:09 +02:00
Alexander Neumann
548d4eed95 Correct backend test for len = 0 2017-04-26 20:47:15 +02:00
Alexander Neumann
8a5034d289 Merge pull request #930 from restic/update-cobra
forget: don't sort flags
2017-04-21 20:01:47 +02:00
Alexander Neumann
5a68641f27 Add saythanks.io 2017-04-21 19:32:37 +02:00
Alexander Neumann
ce28584dda Don't sort flags for forget command 2017-04-21 19:25:21 +02:00
Alexander Neumann
ab870dd691 Update cobra und spf13 2017-04-21 19:19:14 +02:00
Alexander Neumann
aed0126102 Merge pull request #927 from restic/open-check-config
Test if a config file on open
2017-04-19 19:29:27 +02:00
Alexander Neumann
d0db1bf9b3 Test if a config file on open 2017-04-19 18:56:01 +02:00
Alexander Neumann
90ba606633 Add console hilighting to README.rst 2017-04-19 18:32:32 +02:00
Alexander Neumann
5b80cb8b6b Merge pull request #922 from restic/update-deps
Update vendored dependencies
2017-04-19 18:30:41 +02:00
Alexander Neumann
e6ca604d24 build.go: Also copy header files 2017-04-19 17:31:07 +02:00
Alexander Neumann
9940b7f853 Update golang.org/x/* 2017-04-18 21:40:17 +02:00
Alexander Neumann
faca9276e9 Update spf13/pflag 2017-04-18 21:40:17 +02:00
Alexander Neumann
5a7e463ef6 Update spf13/cobra 2017-04-18 21:40:17 +02:00
Alexander Neumann
27c5a2825a Update restic/chunker to v0.1.0 2017-04-18 21:40:17 +02:00
Alexander Neumann
e9d939ff2d Update code for pkg/xattr v0.2.0 2017-04-18 21:39:55 +02:00
Alexander Neumann
88e8eaa575 Update pkg/xattr to v0.2.0 2017-04-18 21:32:55 +02:00
Alexander Neumann
3bbcf89105 Update pkg/errors to v0.8.0 2017-04-18 21:30:02 +02:00
Alexander Neumann
1794bdc663 Update minio/minio-go to v2.0.4 2017-04-18 21:29:56 +02:00
Alexander Neumann
bcf97ebf1f Merge pull request #920 from jpmens/patch-2
Small typo in "Frankfurt"
2017-04-18 11:55:04 +02:00
JP Mens
a1496547fd Small typo in Frankfurt 2017-04-18 11:42:40 +02:00
Alexander Neumann
0befa06cd0 Merge pull request #919 from Phaeilo/s3-tutorial
Added AWS S3 tutorial.
2017-04-17 22:44:12 +02:00
Alexander Neumann
407b8c179b doc: Use "code-block console" for session blocks 2017-04-17 22:43:04 +02:00
Alexander Neumann
aafee25d58 Convert AWS S3 tutorial to rst 2017-04-17 22:41:23 +02:00
Philip Huppert
adb602dbfe Added AWS S3 tutorial. 2017-04-17 22:14:39 +02:00
Alexander Neumann
085b6bede8 doc: Add hline 2017-04-17 22:13:43 +02:00
Alexander Neumann
096aab592b doc: Move doc/references/* to doc/ 2017-04-17 22:07:06 +02:00
Alexander Neumann
2f117982e9 Add symlink to make old link to Design.md work 2017-04-17 21:17:15 +02:00
Alexander Neumann
b05603eb6c Merge pull request #913 from Thor77/switch-to-sphinx
Switch documentation to Sphinx
2017-04-17 21:00:22 +02:00
Thor77
9344b0b3a8 Documentation: Refactor and switch to Sphinx
* convert documentation to reStructuredText
* switch from mkdocs to sphinx
* rewrite README.md (now .rst) to include less information and be more
attractive
2017-04-17 20:53:38 +02:00
Alexander Neumann
89ea1171db Merge pull request #918 from restic/fix-917
s3: Correct layout prefix, reduce connections
2017-04-17 20:22:39 +02:00
Alexander Neumann
28968caf33 s3: Correct prefix for layout 2017-04-17 20:06:55 +02:00
Alexander Neumann
16fd1c2352 s3: Correct layout handle url/path 2017-04-17 20:06:55 +02:00
Alexander Neumann
b942f61272 s3: Reduce connection limit, prevents timeouts 2017-04-17 20:06:55 +02:00
Alexander Neumann
e3d1badfcf Add more questions to github issue template 2017-04-17 18:30:20 +02:00
Alexander Neumann
76f6a9e597 Merge pull request #916 from restic/fix-915
Ignore empty lines in excludes file, allow multiple files
2017-04-17 10:20:12 +02:00
Alexander Neumann
69dd1d2544 backup: Allow specifying multiple exclude files 2017-04-16 20:52:41 +02:00
Alexander Neumann
dd65ac56ef filter: Ignore empty patterns 2017-04-16 20:49:20 +02:00
Alexander Neumann
c796d84fca Ignore empty lines in excludes file
Closes #915
2017-04-16 20:46:52 +02:00
Alexander Neumann
525db875b0 Merge pull request #898 from restic/prepare-cloud-backends
Prepare more cloud backends, add backend layouts
2017-04-15 11:30:25 +02:00
Alexander Neumann
e73038cbcb Merge pull request #912 from restic/update-go
Update Go
2017-04-15 11:19:20 +02:00
Alexander Neumann
00a8edb4a0 Merge pull request #911 from restic/fix-735
Implement MkdirAll() for Windows
2017-04-15 11:19:17 +02:00
Alexander Neumann
ee7e0d05f5 Remove old Go versions from run_integration_tests.go 2017-04-15 11:06:50 +02:00
Alexander Neumann
6357dc8a91 Update Go versions 2017-04-15 10:58:43 +02:00
Alexander Neumann
5bd95b3ce1 Implement MkdirAll() for Windows
Closes #735
2017-04-15 10:56:17 +02:00
Alexander Neumann
be06983c80 options: Fix sorting (and test) 2017-04-14 00:45:54 +02:00
Alexander Neumann
55bdc1fa16 Add documentation for new options 2017-04-14 00:14:11 +02:00
Alexander Neumann
a634c22ae0 Add hidden 'options' command to list all opts 2017-04-14 00:00:03 +02:00
Alexander Neumann
859ee23d2e options: Register local and sftp backends 2017-04-14 00:00:03 +02:00
Alexander Neumann
b7671dafc8 options: Allow registering 2017-04-14 00:00:02 +02:00
Alexander Neumann
541484d142 s3: Use Layout 2017-04-14 00:00:02 +02:00
Alexander Neumann
f531ca3b48 layout: Fix corner cases 2017-04-14 00:00:02 +02:00
Alexander Neumann
0da7264e75 rest: Convert to Layout 2017-04-14 00:00:02 +02:00
Alexander Neumann
7f3bcdb4cc layout: prepare use for REST backend 2017-04-14 00:00:02 +02:00
Alexander Neumann
5eaa51eeff Remove unused assignments 2017-04-14 00:00:02 +02:00
Alexander Neumann
7b64b890d7 Simplify code 2017-04-14 00:00:02 +02:00
Alexander Neumann
ccc201ea5f remove unused code 2017-04-14 00:00:02 +02:00
Alexander Neumann
e6578857cf sftp/local: Fix listing files 2017-04-14 00:00:02 +02:00
Alexander Neumann
320c22f1f5 backend/layout: Add Basedir() 2017-04-14 00:00:02 +02:00
Alexander Neumann
e2af5890f3 backend: Add test for listing files with layouts 2017-04-14 00:00:02 +02:00
Alexander Neumann
36b1c0898c sftp: Add OS X sftp-server path 2017-04-13 23:59:59 +02:00
Alexander Neumann
94441dcbee Merge pull request #908 from restic/handle-nested-mountpoints
Check allowed devices per path
2017-04-13 22:00:10 +02:00
Alexander Neumann
c723cdf808 Check allowed devices per path
Closes #645
Closes #702
2017-04-13 21:03:05 +02:00
Alexander Neumann
c2ee0d9c84 sftp: Skip tests if server binary is not available 2017-04-10 22:51:00 +02:00
Alexander Neumann
e8780f1ec6 sftp: Add layout tests 2017-04-10 22:42:33 +02:00
Alexander Neumann
783fd73ea1 local: Rename local_layout_test 2017-04-10 22:42:33 +02:00
Alexander Neumann
74eb293733 sftp: Remove legacy filename/dirname methods 2017-04-10 22:42:33 +02:00
Alexander Neumann
42ea4d257b sftp: first step of conversion to Layout 2017-04-10 22:42:33 +02:00
Alexander Neumann
a849edf19a local: remove double Close() 2017-04-10 22:42:33 +02:00
Alexander Neumann
0cbd59856c layout: Add IsNotExist 2017-04-10 22:42:33 +02:00
Alexander Neumann
698ba57597 backend/tests: Print error stacktrace if available 2017-04-10 22:42:33 +02:00
Alexander Neumann
27ce6a85e9 sftp: Rework Open/Create 2017-04-10 22:42:33 +02:00
Alexander Neumann
ae290ab374 sftp: Rename Dir -> Path 2017-04-10 22:42:33 +02:00
Alexander Neumann
ab602c9d14 sftp: Add Layout 2017-04-10 22:40:24 +02:00
Alexander Neumann
2e53af1b75 sftp: Rename Open/Create 2017-04-10 22:39:13 +02:00
Alexander Neumann
a725e065d9 Merge pull request #903 from restic/fix-hardlinks
Add new field DeviceID and tests
2017-04-07 21:05:20 +02:00
Alexander Neumann
db7e23b423 Skip /dev/null on darwin 2017-04-07 20:37:20 +02:00
Alexander Neumann
dc8493535d Merge pull request #905 from restic/fix-json-output
Remove short option conflict
2017-04-07 20:30:18 +02:00
Alexander Neumann
10a395ca33 Make tests runnable on os x/darwin 2017-04-06 20:36:09 +02:00
Alexander Neumann
522c7ade91 Add test for double defined flags 2017-04-06 19:44:53 +02:00
Alexander Neumann
a3d6099892 Remove short option conflict 2017-04-06 19:14:38 +02:00
Alexander Neumann
280028290e Disable tests on darwin 2017-04-05 21:35:29 +02:00
Alexander Neumann
c195139d31 Only run tests on unix 2017-04-05 20:51:26 +02:00
Alexander Neumann
6f1b03415c Fix hardlinks 2017-04-05 20:45:24 +02:00
Alexander Neumann
ffcb015581 Add new field DeviceID and tests 2017-04-05 20:42:15 +02:00
Alexander Neumann
67a2ac292b Merge pull request #902 from jgfrm/issue893
storage of device id of files
2017-04-05 17:44:03 +02:00
jgfrm
6674b2a70c Moved assignment of device id outside the switch 2017-04-05 11:33:08 +02:00
jgfrm
c855d6bb9a storage of device id of files 2017-04-04 21:28:25 +02:00
Alexander Neumann
1086528ab7 sftp: Fix errors import 2017-04-03 21:42:41 +02:00
Alexander Neumann
c26dd6b76f sftp: Integrate command 2017-04-03 21:05:42 +02:00
Alexander Neumann
d3b6f75848 sftp: Add SplitShellArgs 2017-04-03 08:57:33 +02:00
Alexander Neumann
d1efdcd78e Add integration test for layouts 2017-04-02 20:35:17 +02:00
Alexander Neumann
95ab5adda1 local: Expose layout as extended option 2017-04-02 20:29:00 +02:00
Alexander Neumann
c5eb36fe9d layout: improve error message for ParseLayout 2017-04-02 20:28:42 +02:00
Alexander Neumann
e3e3a8a695 local: Add layout tests 2017-04-02 20:01:40 +02:00
Alexander Neumann
24ebf95f33 local: Automatically detect layout 2017-04-02 20:01:40 +02:00
Alexander Neumann
54465c92cc layout: Allow passing in a default layout 2017-04-02 20:01:36 +02:00
Alexander Neumann
f7c4b3a922 Fix layout detection 2017-04-02 19:18:03 +02:00
Alexander Neumann
50dfa64a54 Add layout name parser 2017-04-02 17:57:28 +02:00
Alexander Neumann
c6b8ffbb61 Add layout auto detection 2017-04-02 17:25:22 +02:00
Alexander Neumann
3e81dcdfc2 Add cloud and s3 layout 2017-03-26 22:20:10 +02:00
Alexander Neumann
782b740c95 local: Remove unused code 2017-03-26 22:14:37 +02:00
Alexander Neumann
3fd6fa6f86 local: Use Layout for filename generation 2017-03-26 21:53:26 +02:00
Alexander Neumann
6a201f7962 backend: Add Layout 2017-03-26 21:52:49 +02:00
Alexander Neumann
80a864c52c test: Add TempDir() helper 2017-03-26 20:40:45 +02:00
Alexander Neumann
c8eea49909 debug: Allow creating insecure repositories
Uses low-security KDF parameters for scrypt(). Do not use in production!
2017-03-26 10:55:08 +02:00
Alexander Neumann
95c354fe81 doc: s3 backend deviations, cloud repo layout 2017-03-26 10:55:08 +02:00
Alexander Neumann
6935f82389 Merge pull request #897 from restic/add-extended-options
Add extended options
2017-03-26 10:53:36 +02:00
Alexander Neumann
719bb18316 Parse extended options 2017-03-25 17:52:16 +01:00
Alexander Neumann
a8a7701f60 options: Add namespace to Apply() 2017-03-25 17:52:16 +01:00
Alexander Neumann
2924ebc124 options: Add Apply() 2017-03-25 17:52:16 +01:00
Alexander Neumann
f587a5f4f0 options: fail when key is configured twice 2017-03-25 17:52:16 +01:00
Alexander Neumann
946b4f4b86 Add extended options via -o/--option 2017-03-25 17:52:12 +01:00
Alexander Neumann
d0a5e86da1 Add free-form options parsing 2017-03-25 17:45:11 +01:00
Alexander Neumann
2e0b19f63f location: Move to backend/location 2017-03-25 17:45:11 +01:00
Alexander Neumann
9861f3d435 location: Reformat tests, use sub tests 2017-03-25 17:45:10 +01:00
Alexander Neumann
482a6e9840 local: Add Config struct 2017-03-25 17:45:10 +01:00
Alexander Neumann
6a8700d86c Merge pull request #896 from restic/fix-895
doc: Add section about how to change the sftp port
2017-03-25 09:31:29 +01:00
Alexander Neumann
1733b24f9d doc: Add section about how to change the sftp port
Closes #895
Related to #785
2017-03-25 09:07:50 +01:00
Alexander Neumann
c85055bb03 Merge pull request #887 from restic/rest-integration-tests
WIP: rest: Make backend honor the REST protocol
2017-03-18 15:21:05 +01:00
Alexander Neumann
280f05b174 Run rest-server 2017-03-18 11:11:31 +01:00
Alexander Neumann
990b0f1c15 Travis: Run rest-server for integration tests 2017-03-16 21:59:21 +01:00
Alexander Neumann
6f76a6db66 rest: Make backend honor the REST protocol 2017-03-16 21:50:26 +01:00
Alexander Neumann
0c2834edb7 Merge pull request #883 from trbs/s3_perf_cache_stat
Cache size of last ReadAt on S3 for performance
2017-03-15 20:59:29 +01:00
trbs
f1ba45723c introduce RWLock for caching obj.Stat() 2017-03-14 23:05:51 +01:00
trbs
b523eef294 Cache size of last ReadAt on S3 for performance
Each obj.Stat() call adds another request to the S3 endpoint
for some commands a lot of ReadAt calls are made for the same
object in S3. This patch essentially cuts the number of calls
to S3 in this case in half. Speeding up the progress and lowering
costs to S3.
2017-03-14 14:02:50 +01:00
Alexander Neumann
bf30b2831b Merge pull request #878 from middelink/fix-825
Fix restoring files ownership as non-root user
2017-03-12 18:20:28 +01:00
Pauline Middelink
642cd3bebf Fix restoring files as non-root user
As we cannot reliably detect in advance if we can set ownership, permissions,
timestamps or ext attributes, execute ALL the requested changes before
returning the first error we found.

Report total errors at end of restore and stop printing entire stacktraces
where just the error message is sufficient.

Fixes #655
2017-03-12 16:51:58 +01:00
Alexander Neumann
057dd6c590 Merge tag 'v0.5.0'
v0.5.0
2017-03-11 21:14:39 +01:00
Alexander Neumann
0bd7db9efe Add VERSION file for 0.5.0 2017-03-11 12:36:35 +01:00
Alexander Neumann
f678c97346 Add VERSION file for 0.5.0 2017-03-11 12:34:27 +01:00
Alexander Neumann
887e81188f Merge pull request #877 from middelink/fix-875
Fix crash at restoring pre-existing hardlinked files
2017-03-11 10:13:43 +01:00
Alexander Neumann
8d0140aabe Merge pull request #876 from middelink/fix-836
Restore does not truncate files it restores
2017-03-11 10:12:53 +01:00
Alexander Neumann
2a8f275a28 Manual: Document TMPDIR env variable
Closes #873
2017-03-11 10:02:33 +01:00
Alexander Neumann
55c3150946 Manual: Fix section about JSON 2017-03-11 09:59:36 +01:00
Pauline Middelink
b56e16acd0 Fix crash at restoring pre-existing hardlinked files
Remove target file, ignore non existing file errors.

Small memory saving: Only keep inodes around for files with a link count > 1.
(We will/can never be asked to restore a hardlinked file with one of the
files having a link count of 1.)

Closes #836
2017-03-11 00:06:58 +01:00
Pauline Middelink
b75186533d Restore does not truncate files it restores
Missing O_TRUNC when doing O_CREAT.

Closes #875
2017-03-10 22:46:13 +01:00
Alexander Neumann
8958efba60 Merge pull request #871 from middelink/fix-869
Refactor output of `find` to allow for json and normal output.
2017-03-10 20:03:26 +01:00
Pauline Middelink
a9707a5728 Refactor output of find to allow for json and normal output.
Rather complicated solution becaused I wanted to retain the streaming
character of the output, which means for json I have to manually add
headers and footers per snapshot scanned + a list around the whole
set.

As the json ouput is now partly handcrafted, add proper testing to catch
unintentional changes to the output, making it non-json compliant.

Closes #869
2017-03-10 16:23:58 +01:00
Alexander Neumann
7635feb591 Add cross-compilation to README and Manual 2017-03-09 21:24:38 +01:00
Alexander Neumann
087c2917aa Check version in build.go
Print a sensible error for Go < 1.7 (the "context" package is missing
there)
2017-03-09 21:24:30 +01:00
Alexander Neumann
deddedd88f Merge pull request #872 from middelink/fix-787
Suppress (non error) output of `forget` during -q
2017-03-09 21:23:36 +01:00
Pauline Middelink
b38294f236 Suppress (non error) output of forget during -q
Closes #787
2017-03-09 20:20:17 +01:00
Alexander Neumann
fdb9872cef Add cross-compilation to README and Manual 2017-03-09 11:37:44 +01:00
Alexander Neumann
f8dd5d5088 Check version in build.go
Print a sensible error for Go < 1.7 (the "context" package is missing
there)
2017-03-09 11:29:24 +01:00
Alexander Neumann
9e0207e534 Add CHANGELOG.md and instructions for contributors 2017-03-09 10:40:24 +01:00
Alexander Neumann
9eae789cd2 Add VERSION file for 0.5.0-rc.1 2017-03-09 10:28:33 +01:00
Alexander Neumann
d6104935d7 Add VERSION file for 0.5.0rc1 2017-03-09 10:20:11 +01:00
Alexander Neumann
406af5916c Also include the source in SHA256SUMS 2017-03-09 10:19:45 +01:00
Alexander Neumann
93e4e4f4fb Merge pull request #866 from middelink/widespread-tags
Add --tag filtering to every command, where applicable
2017-03-09 09:55:26 +01:00
Pauline Middelink
8a05de537f Refactor prune and rebuild_index
Factor out and reuse `rebuildIndex()` in cmd_rebuild_index and cmd_prune.

Use contexts.
2017-03-08 20:30:52 +01:00
Pauline Middelink
8a92687d9a Refactor find and ls commands
Implement filtering by using `FindFilteredSnapshots()` to iterate over the snapshots

Refactor cmd_ls' `PrintNode()` into format.go, reuse its pretty printing in both `find`
and `ls` commands.

Use contexts.
2017-03-08 20:29:31 +01:00
Pauline Middelink
3432e7edcd Refactor tag to use FindFilteredSnapshots() 2017-03-08 20:28:44 +01:00
Pauline Middelink
3c6c17abcd Refactor forget and snapshots command
Implement filtering by using `FindFilteredSnapshots()` to iterate over the snapshots

Refactor cmd_snapshots' `PrintSnapshots()` so its pretty printing can be used from
both `forget` and `snapshots`.

Use contexts.
2017-03-08 20:24:58 +01:00
Pauline Middelink
11d237c252 New helper function FindFilteredSnapshots to iterate over snapshots
This helper function takes a set of filters and/or a list of snapshots
from the commandline. It returns a channel of *Snapshot.

When snapshot ids are given, they are checked for validity and their
corresponding Snapshots returned. The snapshot id "latest" is handled
special to return either the last snapshot (no filters) or the last
snapshot matching the filters.

When no arguments are given, the filters are applied over all available
snapshots and these are returned.
2017-03-08 20:19:12 +01:00
Pauline Middelink
0f7b6ec5ac Adapt key command to context world. 2017-03-08 20:17:30 +01:00
Pauline Middelink
b4526c4e6e Enable the use of context in restic
Set up a cancelble context in global options, hook it into the ctrl-C handler
for proper cancel propegation.

Bump up minimal requirement for Go to version 1.7 in documentation
and test-build files.
2017-03-08 20:12:16 +01:00
Pauline Middelink
3eaaa0f286 Correct some typo's in comments. 2017-03-08 20:09:24 +01:00
Pauline Middelink
b1c8071163 Add filtering to mount command 2017-03-08 19:59:19 +01:00
Pauline Middelink
3468108d4c Implement --tag processing to backup and restore command
Add `tags` argument to `FindLatestSnapshot()`
2017-03-08 19:55:58 +01:00
Alexander Neumann
33c8dd4ee5 Merge pull request #867 from middelink/optimize
Optimize List() pipeline
2017-03-08 11:50:47 +01:00
Alexander Neumann
5e2c4caa32 Merge pull request #861 from middelink/fix-841
Refactor cmd_forget
2017-03-08 09:56:01 +01:00
Pauline Middelink
7c989ca487 Optimize List() pipeline 2017-03-07 20:58:36 +01:00
Pauline Middelink
e9a2982ecd Add --path for snapshot filtering by path.
Add `--group-by-tags` for grouping on host,tags,dirs instead of host,dirs.

Borrow the snapshot printing from cmd_snapshot.

Closes #841
2017-03-07 19:28:42 +01:00
Alexander Neumann
f5a55a81f7 Merge pull request #865 from restic/handle-empty-snapshots
Refuse to create empty snapshots
2017-03-07 11:21:48 +01:00
Alexander Neumann
340f2c80a0 Merge pull request #864 from restic/find-case-insensitive
find: Add option to ignore case
2017-03-07 11:20:33 +01:00
Alexander Neumann
bb144436c7 Add test for empty snapshot 2017-03-07 11:17:15 +01:00
Alexander Neumann
b50d3ba805 Refuse to create empty snapshots
Closes #862
2017-03-07 11:12:34 +01:00
Alexander Neumann
00e7158381 Merge pull request #860 from middelink/factor-out
Create a helper function to get the terminal width
2017-03-07 10:59:39 +01:00
Alexander Neumann
221bef48c0 find: Add option to ignore case
Closes #859
2017-03-07 10:58:09 +01:00
Pauline Middelink
afcc1ba706 Create a helper function to get the terminal width
Rationale: contain terminal access to one file.
2017-03-06 11:23:00 +01:00
Alexander Neumann
bf88a62a16 Merge pull request #858 from middelink/snapshot-layout
Fix layout issue in cmd_snapshot "ascii art"
2017-03-06 09:10:35 +01:00
Alexander Neumann
cc140744d6 Merge pull request #857 from middelink/fix-856
Fix SamePaths() and make it into a receiver function
2017-03-06 09:10:33 +01:00
Pauline Middelink
354e8ffb82 Fix layout issue in cmd_snapshot "ascii art"
The layouter does not account for multi tags when determining the
need for ascii art.

36fd8178  2017-03-03 21:35:04  abuseio.polyware.nl    NL          /
                                                      A       └──

vs

36fd8178  2017-03-03 21:35:04  abuseio.polyware.nl    NL      ┌── /
                                                      A       └──
2017-03-06 02:49:15 +01:00
Pauline Middelink
e1c828be3e Fix SamePaths() and make it into a receiver function
Add `HasPath(paths []string) bool` to Snapshot for testing if the
snapshot has at least the paths given to the function.

Reimplemented SamePaths(paths []string) so it does what the name implies,
compare if all given paths are in the snapshot.
2017-03-06 02:21:58 +01:00
Alexander Neumann
d50dc9f649 Merge pull request #855 from middelink/fix-851
Add `tag` command to restic cli to manipulate tags on existing snapshots.
2017-03-05 20:20:20 +01:00
Alexander Neumann
07695b3622 Documentation fixes 2017-03-05 20:12:25 +01:00
Pauline Middelink
be15a9261a Add design and user documentation for the restic tag command 2017-03-05 19:55:23 +01:00
Pauline Middelink
1fa2313aef Snapshot: Add Original ID
The Original ID is used when the snapshot is modified (e.g. by `tag`
command). Adjust integration testing to assert correctness.
2017-03-05 19:55:23 +01:00
Pauline Middelink
26e266a951 Fix type of ID field in cmd_snapshots type Snapshot 2017-03-05 19:55:22 +01:00
Pauline Middelink
208edaa3d1 Snapshot: Add AddTags() and RemoveTags()
Both prevent duplicate tags.
2017-03-05 19:55:22 +01:00
Pauline Middelink
f6a258b4a8 Add tag: Manipulate tags on existing snapshots
Add integration testing.
2017-03-05 19:55:22 +01:00
Alexander Neumann
db08581352 Add hint for other backend URI formats 2017-03-05 16:58:24 +01:00
Alexander Neumann
0afeb68e6c Merge pull request #854 from middelink/dynamic-host,tag-columns-in-cmd_snapshots
Dynamic host,tag columns widths in snapshots command
2017-03-05 16:11:38 +01:00
Alexander Neumann
a809c9ac5f Merge pull request #853 from middelink/cmd_backup-tag-integration-test
restic backup --tag integration test
2017-03-05 16:08:02 +01:00
Pauline Middelink
45e9f35654 Make columns for host and tags size width dynamicly on their content. 2017-03-05 05:32:01 +01:00
Pauline Middelink
edd5c8b44d Add integration test to make sure cmd_backup adds tags when required. 2017-03-05 05:24:40 +01:00
Pauline Middelink
7238a3ee89 Changed cmd_snapshots to be testable (no more using os.Stdout) 2017-03-05 05:24:11 +01:00
Alexander Neumann
bbcab800c9 Merge pull request #850 from middelink/fix-848
Add progressbar to repack and blob remove phases of prune cmd.
2017-03-04 18:47:03 +01:00
Pauline Middelink
5564c78e53 English typo: rewriten > rewritten. 2017-03-04 17:43:58 +01:00
Pauline Middelink
792b81725e Add progressbar to repack and blob remove phases of prune cmd. 2017-03-04 17:38:34 +01:00
Alexander Neumann
b101efe26e Merge pull request #845 from restic/update-pkg-xattr
Update github.com/pkg/xattr
2017-03-04 16:49:07 +01:00
Alexander Neumann
becc34a159 Merge pull request #847 from middelink/fix-846
Display the proper amount of bytes we will be pruning from the repo.
2017-03-04 16:47:20 +01:00
Pauline Middelink
1273c6f3d4 Display the proper amount of bytes we will be pruning from the repo. 2017-03-04 15:17:44 +01:00
Alexander Neumann
82458d4de0 Update github.com/pkg/xattr
Closes #843
2017-03-04 14:23:15 +01:00
Alexander Neumann
7066cc17bb Merge pull request #844 from welpo/patch-1
Fix Minio Server URL
2017-03-04 13:51:05 +01:00
welpo
12ed2f65e3 Fix Minio Server URL
The previous link gave a 404
2017-03-04 13:19:50 +01:00
Alexander Neumann
90bc187355 Merge pull request #840 from middelink/master
Display absolute paths when displaying the output of ls and find.
2017-03-03 12:09:20 +01:00
Pauline Middelink
5ecaaea90b Really use absolute pathnames, not all systems use /. 2017-03-03 11:14:39 +01:00
Alexander Neumann
f9fc8674eb Merge pull request #837 from restic/index
Misc improvements
2017-03-03 09:44:06 +01:00
Pauline Middelink
039e81b04b Fix unit test, we need to check for absolute paths now. 2017-03-02 23:30:56 +01:00
Pauline Middelink
efb4315a1e Display absolute paths when displaying the output of ls and find. 2017-03-02 22:41:11 +01:00
Alexander Neumann
f53d33ba34 Make ArchiveReader a struct 2017-03-02 15:45:35 +01:00
Alexander Neumann
9b776dc7ab Use new Index implementation for rebuild-index 2017-03-02 15:23:59 +01:00
Alexander Neumann
1d64a1dcbb Merge pull request #835 from restic/fix-834
Allow filtering absolute paths
2017-03-02 15:22:17 +01:00
Alexander Neumann
7c92994f10 Clarify variable name 2017-03-02 14:52:18 +01:00
Alexander Neumann
bf97cc7efa Allow filtering absolute paths
Before, the restorer called the filter function with a relative path,
this prevented anchoring absolute patterns (which just never matched).
Now call the restore function with an absolute virtual path, starting at
the filepath separator.

Closes #834
2017-03-02 14:50:54 +01:00
Alexander Neumann
4f5e9e939b Merge pull request #829 from restic/fix-822
Ignore empty lines in --files-from
2017-02-27 21:15:35 +01:00
Alexander Neumann
92ad35848a Fix Travis Go versions 2017-02-27 20:40:10 +01:00
Alexander Neumann
bb69b20aff Travis: Configure Go versions 2017-02-27 20:39:00 +01:00
Alexander Neumann
80e93621e1 Travis: Configure Go versions 2017-02-27 20:37:47 +01:00
Alexander Neumann
31ff506309 Ignore empty lines in --files-from
Closes #822
2017-02-27 19:42:00 +01:00
Alexander Neumann
4a51ddf741 Add section to the manual about same directory names
Related to #652
2017-02-21 20:46:26 +01:00
Alexander Neumann
3d1dc636d0 Merge pull request #817 from restic/add-forget-prune
Add `--prune` switch to `forget`
2017-02-21 11:40:30 +01:00
Alexander Neumann
8609ba28d0 Add News section 2017-02-21 11:12:07 +01:00
Alexander Neumann
685f5ebbd1 Add --prune switch to forget 2017-02-21 10:58:30 +01:00
Alexander Neumann
4e2f8145f5 Update Appveyor 2017-02-18 19:09:21 +01:00
Alexander Neumann
11b63d3417 Update Travis 2017-02-18 19:08:11 +01:00
Alexander Neumann
2c81bc35dc Merge pull request #814 from restic/fix-archiver
Correct archiver behavior in case of errors
2017-02-18 19:00:33 +01:00
Alexander Neumann
b8ce1b4e69 Correct archiver behavior in case of errors 2017-02-18 17:46:06 +01:00
Alexander Neumann
23c2717ab2 Merge pull request #809 from restic/fix-xattr
WIP: fix panic
2017-02-18 15:00:49 +01:00
Alexander Neumann
132afbe83b Correct error check for ENOTSUP, add errors.Wrap() 2017-02-18 14:36:37 +01:00
Alexander Neumann
ef52d15edd Continue if extended attribute cannot be read 2017-02-18 14:35:11 +01:00
Alexander Neumann
6df2f9e5ba Add support for extended attributes on FreeBSD 2017-02-18 14:35:11 +01:00
Alexander Neumann
eb9be4e884 Use github.com/pkg/xattr for extended attributes 2017-02-18 14:35:06 +01:00
Alexander Neumann
0674f32d79 Merge pull request #766 from jgfrm/nissue25
Add support for extended attributes (e.g. ACL)

Closes #766
2017-02-16 11:45:24 +01:00
Jaap Gordijn
49cae0904f Add support for extended attributes (e.g. ACL) 2017-02-16 11:44:51 +01:00
Alexander Neumann
40685a0e61 Merge pull request #803 from ibib/adds-json-support
Adds JSON support for the snapshots command
2017-02-16 11:26:14 +01:00
Alexander Neumann
4772a4986b Merge pull request #808 from restic/fix-807
restore: Make sure buffer is large enough
2017-02-15 19:58:03 +01:00
Alexander Neumann
c973a1f875 Merge pull request #806 from oysols/consistent-cli
Fix inconsistencies in CLI
2017-02-15 15:26:27 +01:00
Alexander Neumann
50d066befb restore: Make sure buffer is large enough 2017-02-15 15:19:28 +01:00
Øystein Olsen
c35e48291d Fix correct number of arguments for key command 2017-02-13 16:16:34 +01:00
Øystein Olsen
511278b66a Fix wrong description of rebuild-index command in help text 2017-02-13 16:07:29 +01:00
Øystein Olsen
514a11346d Add long description of list command in help text 2017-02-13 16:06:27 +01:00
Øystein Olsen
2eb75bb941 Consistently refer to 'the' instead of 'a' repository in help text 2017-02-13 16:05:25 +01:00
Øystein Olsen
9922ce97bf Use lowercase consistently in help text 2017-02-13 16:02:47 +01:00
ibib
3ed4127297 Adds JSON support for the snapshots command 2017-02-12 21:43:39 +01:00
Alexander Neumann
c83e608cce Merge pull request #800 from restic/jgfrm-hardlinks-fuse
fuse: correct nlink count for directories
2017-02-11 22:09:33 +01:00
Jaap Gordijn
3e2ae15882 Manual: Add section about hard links for fuse 2017-02-11 21:54:54 +01:00
Jaap Gordijn
3047702ded Correct hardlinks for fuse directories 2017-02-11 21:54:23 +01:00
Alexander Neumann
05cae4911d Merge pull request #798 from restic/fix-797
Checker: Propagate errors properly
2017-02-11 14:55:50 +01:00
Alexander Neumann
8c34eaad15 Improve error message 2017-02-11 14:28:15 +01:00
Alexander Neumann
0492eabff1 Improve error messages 2017-02-11 14:24:11 +01:00
Alexander Neumann
7797e084f9 checker: Pass on error loading an index 2017-02-11 14:22:14 +01:00
Alexander Neumann
b40aa66985 errors: Add method Wrapf 2017-02-11 14:22:04 +01:00
Alexander Neumann
76c06c5f2a Add check for modified index 2017-02-11 14:13:58 +01:00
Alexander Neumann
83538c745a Merge pull request #792 from restic/fix-791
s3: Increase MaxIdleConnsPerHost
2017-02-11 11:20:20 +01:00
Alexander Neumann
f266741f40 s3/rest: raise connection limit to 40 2017-02-11 10:40:51 +01:00
Alexander Neumann
4795a5c5d1 Merge pull request #795 from restic/fix-767
fuse: Add options --allow-other and --allow-root
2017-02-11 10:38:33 +01:00
Alexander Neumann
c14cb62cd7 fuse: Add options --allow-other and --allow-root
Closes #767
2017-02-10 21:58:10 +01:00
Alexander Neumann
b3ec01521e Merge pull request #794 from restic/fix-789
Use non-formatting functions of errors for strings
2017-02-10 21:42:59 +01:00
Alexander Neumann
6483df5ee4 Merge pull request #793 from restic/add-host-parameter
backup: Add `--hostname` parameter
2017-02-10 21:42:56 +01:00
Alexander Neumann
6275d69a36 fuse: Show link count 2017-02-10 21:16:48 +01:00
Alexander Neumann
6300c8df56 Merge pull request #763 from jgfrm/issue25
Support hard links
2017-02-10 20:58:39 +01:00
Jaap Gordijn
366bf4eb0c Support hard links
Closes #152
2017-02-10 20:58:19 +01:00
Alexander Neumann
21b358c742 backend tests: Always close reader 2017-02-10 20:49:46 +01:00
Alexander Neumann
7a0303f7ae s3: Make sure to return connection token 2017-02-10 20:49:37 +01:00
Alexander Neumann
64165ea4c8 s3: Hold connection semaphore until Close() 2017-02-10 20:17:52 +01:00
Alexander Neumann
c8fc789393 Use non-formatting functions of errors for strings
Commands used:

    $ gofmt -w -r 'errors.Fatalf(x) -> errors.Fatal(x)' src
    $ gofmt -w -r 'errors.Errorf(x) -> errors.New(x)' src

Closes #789
2017-02-10 19:39:49 +01:00
Alexander Neumann
f145e1de0f backup: Add --hostname parameter 2017-02-10 19:37:33 +01:00
Alexander Neumann
36dee7d892 s3: Increase MaxIdleConnsPerHost 2017-02-10 19:25:42 +01:00
Alexander Neumann
98ae30b513 Update Dockerfile 2017-02-10 17:25:48 +01:00
Alexander Neumann
073edd914d Merge pull request #783 from opennota/master
Fix some typos
2017-02-09 10:43:27 +01:00
Alexander Neumann
316b520ffb Merge pull request #782 from restic/fix-backup-stdin
Make sure backups read from stdin have a file name
2017-02-09 10:42:49 +01:00
opennota
25e459659a Fix some typos 2017-02-09 06:43:10 +07:00
Alexander Neumann
fdebb022e4 Make sure backups read from stdin have a file name 2017-02-08 22:37:02 +01:00
Alexander Neumann
ed1739acbd Merge pull request #779 from restic/benchmark-checker
checker: Reduce memory usage
2017-02-07 11:03:49 +01:00
Alexander Neumann
1f81919d4a checker: Reduce memory usage
benchmark              old bytes     new bytes     delta
    BenchmarkChecker-4     25551348      4288037       -83.22%
2017-02-06 21:19:27 +01:00
Alexander Neumann
436b5dc20c Add Blob.String() 2017-02-06 19:50:27 +01:00
Alexander Neumann
0c867b21ff Add benchmark for checker 2017-02-06 19:34:40 +01:00
Alexander Neumann
4cacb622eb Merge pull request #773 from opennota/master
Allow --files-from to take a dash for stdin (fixes #769)
2017-02-06 14:36:52 +01:00
opennota
24acb09a2a Allow --files-from to take a dash for stdin (fixes #769) 2017-02-06 18:43:44 +07:00
Alexander Neumann
ec45cdba84 Merge pull request #778 from restic/fix-777
prune: Close backend reader after download
2017-02-05 20:01:44 +01:00
Alexander Neumann
4ca134a41c prune: Close backend reader after download
Closes #777
2017-02-05 15:40:30 +01:00
Alexander Neumann
b85eae2aea Merge pull request #775 from restic/improve-backup-stdin
Add more tests for reading backups from stdin
2017-02-05 15:29:29 +01:00
Alexander Neumann
aee58a8c17 Add more tests for reading backups from stdin 2017-02-04 16:38:33 +01:00
Alexander Neumann
22f3e21266 Start error message with a lower case character 2017-02-03 17:06:06 +01:00
Alexander Neumann
d0de1ed2e4 Merge pull request #771 from restic/fix-770
Return an error if password is not set for stdin
2017-02-03 16:47:00 +01:00
Alexander Neumann
807fcf07d9 Return an error if password is not set for stdin
Closes #770
2017-02-03 15:53:07 +01:00
Alexander Neumann
f5faff9020 Merge pull request #768 from brikou/download_link
Add note about pre-compiled binaries
2017-02-03 11:11:05 +01:00
Brikou Carré
769a52df16 Bring back "Build restic" paragraph 2017-02-03 10:41:54 +01:00
Brikou Carré
dfb94290ae Add link to release page in doc 2017-02-03 09:09:49 +01:00
Alexander Neumann
f366a636e6 Fix build_release_binaries.sh 2017-02-02 10:23:18 +01:00
Alexander Neumann
a7c2f28f06 Add VERSION file for 0.4.0 2017-02-02 10:21:29 +01:00
Alexander Neumann
79e198451c Don't set GOMAXPROCS
This was a temporary fix for Go earlier than 1.5 to run code on all
avaialble cores. We don't need that any more since we require at least
Go 1.6.
2017-02-02 10:18:07 +01:00
Alexander Neumann
844c959912 Merge pull request #750 from restic/document-rest-create-repo
Document creating a new repo via REST
2017-02-02 10:03:07 +01:00
Alexander Neumann
19ec8f4a77 Add paragraph about code contributions 2017-02-01 12:00:38 +01:00
Alexander Neumann
f5aeda359c Merge pull request #764 from olgeni/typos
Fix typo.
2017-02-01 10:53:46 +01:00
olgeni
5a5687a506 Fix typo. 2017-01-31 10:09:59 +01:00
Alexander Neumann
7e9bfa51e9 REST: Make create repository idempotent 2017-01-30 19:33:17 +01:00
Alexander Neumann
c7d60279f7 Merge pull request #762 from restic/fix-759
Add 'index' to list of options for 'cat' command
2017-01-30 18:26:32 +01:00
Alexander Neumann
7d49c65dd0 Merge pull request #761 from restic/fix-758
Remove inconsistencies regarding the `cat` command
2017-01-30 18:25:49 +01:00
Alexander Neumann
32dfbad414 Merge pull request #760 from restic/fix-756
Allow listing blobs
2017-01-30 18:25:47 +01:00
Alexander Neumann
abd44ca7c5 Add 'index' to list of options for 'cat' command 2017-01-30 10:53:17 +01:00
Alexander Neumann
44f4ff9d37 Remove 'tree' from help text for 'cat' command 2017-01-30 10:50:52 +01:00
Alexander Neumann
658bee17e9 Clean up documentation, remove inconsistencies 2017-01-30 10:48:59 +01:00
Alexander Neumann
524ce01423 Remove 'cat tree' command 2017-01-30 10:48:50 +01:00
Alexander Neumann
41f59ffc78 Fix 'cat' command for tree blobs 2017-01-30 10:48:21 +01:00
Alexander Neumann
d7f52fd7e5 Allow listing blobs
Closes #756
2017-01-30 10:28:17 +01:00
Alexander Neumann
1f9db97d33 Merge pull request #728 from cit/improve-snapshot-output
Add box-drawing character to the snapshots command
2017-01-29 11:00:13 +01:00
Alexander Neumann
f806d8818d Add DELETE 2017-01-29 10:55:47 +01:00
Alexander Neumann
1a538509d0 Document creating a new repo via REST
Closes #736
2017-01-28 10:31:44 +01:00
Alexander Neumann
b2d00b2a86 Merge pull request #749 from restic/normalise-backend-api
Normalise the backend API
2017-01-27 13:30:30 +01:00
Alexander Neumann
31f6093513 Fix tests 2017-01-27 12:47:34 +01:00
Alexander Neumann
b9bddeff39 Normalise the backend API
This makes the following changes, before:

    type backend interface {
        // Test a boolean value whether a File with the name and type exists.
        Test(t FileType, name string) (bool, error)

        // Remove removes a File with type t and name.
        Remove(t FileType, name string) error
    }

After:

    type backend interface {
        // Test a boolean value whether a File with the name and type exists.
        Test(h Handle) (bool, error)

        // Remove removes a File with type t and name.
        Remove(h Handle) error
    }
2017-01-26 22:02:22 +01:00
Florian Adamsky
54c2f622a4 Add box-drawing character to the snapshots command
Remove underscore of variable name

Format code with gofmt

Change snapshot output according to the discussion
2017-01-26 21:54:27 +01:00
Alexander Neumann
d55b56edd3 Merge pull request #748 from restic/fix-747
rest backend: Do not close the reader
2017-01-26 20:20:55 +01:00
Alexander Neumann
8d6fdb7a3e Fix packer manager test on Windows 2017-01-25 17:51:34 +01:00
Alexander Neumann
dceaae33ed PackerManager: Remove unused Finalize() method 2017-01-25 17:12:06 +01:00
Alexander Neumann
e8995b85b8 rest backend: Do not close the reader
Closes #747
2017-01-25 17:12:06 +01:00
Alexander Neumann
925a3cfad7 backend: Check that backends do not close the reader 2017-01-25 17:12:03 +01:00
Alexander Neumann
c99a44b122 Fix Random() function 2017-01-25 13:26:16 +01:00
Alexander Neumann
439d3107f9 Merge pull request #746 from restic/improve-fuse-memory
fuse: Improve memory usage
2017-01-24 14:13:01 +01:00
Alexander Neumann
afc593676a fuse: Improve memory usage
Discard blobs that aren't in use any more. This greatly reduces memory
usage and will probably only trigger on sequential read (e.g. for
restore via fuse).

Closes #480
2017-01-24 12:38:44 +01:00
Alexander Neumann
17d7af6ccc Merge pull request #745 from restic/fix-742
Fix restore/fuse with larger files
2017-01-24 12:12:35 +01:00
Alexander Neumann
0b982d3316 Add restore test with larger files 2017-01-24 11:51:21 +01:00
Alexander Neumann
26da14f315 fuse: improve tests 2017-01-24 11:42:50 +01:00
Alexander Neumann
2de7e03698 repository.LoadBlob: Read correct number of bytes 2017-01-24 11:42:50 +01:00
Alexander Neumann
3d2fbed55f Add test for LoadBlob 2017-01-24 11:42:50 +01:00
Alexander Neumann
0e445ec0f5 checker: Use TestRepository 2017-01-24 11:42:50 +01:00
Alexander Neumann
31055d88a5 Add debug messages 2017-01-24 11:42:42 +01:00
Alexander Neumann
0d125725bc Merge pull request #741 from restic/rework-backend-api
Rework backend API
2017-01-23 20:18:31 +01:00
Alexander Neumann
8b09b5b3cd Merge pull request #740 from restic/add-debug-profiles
Add debug memory/cpu profile options
2017-01-23 20:18:28 +01:00
Alexander Neumann
2d8a699515 Merge pull request #739 from restic/fix-archiver-test
Fix Archiver test: Clean up temp files
2017-01-23 20:18:25 +01:00
Alexander Neumann
0d95507909 Fix test for PackerManager 2017-01-23 19:00:15 +01:00
Alexander Neumann
8e722d8fee Fix saving pack: close temp file before removing 2017-01-23 18:45:15 +01:00
Alexander Neumann
03292d10cc backend: Rename Get() -> Load() 2017-01-23 18:11:10 +01:00
Alexander Neumann
cfc9e8b2fa backends: Remove Load() 2017-01-23 17:54:12 +01:00
Alexander Neumann
f382696ccf repository: Use ReadAt() instead of Load() 2017-01-23 17:54:12 +01:00
Alexander Neumann
e8fcc7e74c repack: Use Get() instead of Load()
In addition, use a tempfile instead of a buffer.
2017-01-23 17:54:12 +01:00
Alexander Neumann
2bd9c9247c checker: Remove Load() from test error backend 2017-01-23 17:54:12 +01:00
Alexander Neumann
fc235317fe backend: Use Get instead of Load for ReaderAt 2017-01-23 17:54:12 +01:00
Alexander Neumann
82d9163955 backend: Ensure Reader is closed on error 2017-01-23 17:54:12 +01:00
Alexander Neumann
4a354befe5 Fix checker test 2017-01-23 17:54:12 +01:00
Alexander Neumann
212936eb52 Make backend.LoadAll() similar to ioutil.ReadAll() 2017-01-23 17:54:12 +01:00
Alexander Neumann
05afedd950 Add backend.Get() 2017-01-23 17:54:11 +01:00
Alexander Neumann
a36c01372d Use streaming functions for saving data in repo 2017-01-23 17:54:11 +01:00
Alexander Neumann
9b48da5b4e Change backend Save() function signature 2017-01-23 17:54:11 +01:00
Alexander Neumann
c93f79f0f3 Add hashing package 2017-01-23 17:54:11 +01:00
Alexander Neumann
89a5152f7d Fix Archiver test: Clean up temp files 2017-01-23 17:53:59 +01:00
Alexander Neumann
47bd9cdf2f Add options for creating a memory or CPU profile 2017-01-23 17:52:26 +01:00
Alexander Neumann
84255f4f4f Vendor github.com/pkg/profile 2017-01-23 17:41:07 +01:00
Alexander Neumann
668a36a652 Add option for debug pprof service 2017-01-23 17:27:42 +01:00
Alexander Neumann
30ff7413be Merge pull request #737 from restic/fix-734
Index: Store pack ID
2017-01-22 22:41:16 +01:00
Alexander Neumann
af1cc0717b Add integration test for forget and prune 2017-01-22 22:23:30 +01:00
Alexander Neumann
5e3365d233 Index: Store pack ID 2017-01-22 22:10:36 +01:00
Alexander Neumann
4f780a01f9 Index: Test pack ID 2017-01-22 22:09:56 +01:00
Alexander Neumann
dc6a832cc3 Correct BenchmarkIndexSave 2017-01-22 09:59:19 +01:00
Alexander Neumann
164ba823e5 Merge pull request #731 from restic/improve-memory-usage
Improve memory usage
2017-01-20 15:56:31 +01:00
Alexander Neumann
8dd7fe82ff Add TestIndexSave 2017-01-20 14:46:14 +01:00
Alexander Neumann
6c1032548b Correct FAQ 2017-01-19 11:47:33 +01:00
Alexander Neumann
f0eeb16b33 Merge pull request #729 from restic/add-faq
Add FAQ document
2017-01-19 11:42:38 +01:00
Alexander Neumann
c88b0d20e8 Correct typo 2017-01-18 21:57:59 +01:00
Alexander Neumann
fbecae7362 Add FAQ document 2017-01-18 21:48:39 +01:00
Alexander Neumann
38ba5fbceb Merge pull request #727 from MirkoDziadzka/fix-typo
fix typo: outputcomprehensive
2017-01-18 11:46:56 +01:00
Mirko Dziadzka
d47758a540 fix typo 2017-01-18 10:46:04 +01:00
Alexander Neumann
dac18e3bf8 Improve BenchmarkIndexSave 2017-01-17 13:00:59 +01:00
Alexander Neumann
c4f44c7bcb Reduce memory consuption of TestCreateSnapshot 2017-01-17 12:56:20 +01:00
Alexander Neumann
73ad3d418d Index: Remove unneeded allocation 2017-01-17 12:46:41 +01:00
Alexander Neumann
36276c41b2 Add Benchmark for IndexSave 2017-01-17 10:40:58 +01:00
Alexander Neumann
d40f566e41 Index: Use slices instead of maps, reduce data 2017-01-17 10:40:58 +01:00
Alexander Neumann
cd9b526203 Preallocate pack entries list 2017-01-17 10:40:58 +01:00
Alexander Neumann
caabc4ec44 Reduce memory usage while decoding index 2017-01-17 10:40:58 +01:00
Alexander Neumann
73e7a2bea8 Add BenchmarkLoadIndex 2017-01-17 10:40:58 +01:00
Alexander Neumann
e463587bad Add BenchmarkDecodeIndex 2017-01-17 10:40:57 +01:00
Alexander Neumann
e571b6a656 Use the same buffer for decryption 2017-01-17 10:40:57 +01:00
Alexander Neumann
710499cf46 Add benchmark for LoadAndDecrypt 2017-01-17 10:40:57 +01:00
Alexander Neumann
32a5c2c1f6 Add a few functions to calculate Blob buffer len 2017-01-17 10:40:57 +01:00
Alexander Neumann
91dcb958e0 Fix tests 2017-01-17 10:40:57 +01:00
Alexander Neumann
9a5b9253c4 LoadBlob: use buffer as scratch space
benchmark               old bytes     new bytes     delta
    BenchmarkLoadBlob-4     1010128       2256          -99.78%
2017-01-17 10:40:57 +01:00
Alexander Neumann
215af5c60a Add LoadBlob benchmark 2017-01-17 10:40:57 +01:00
Alexander Neumann
8734c2466c Fix call to debug.Log() 2017-01-17 10:40:57 +01:00
Alexander Neumann
0556687584 Reduce memory usage for prune 2017-01-17 10:40:57 +01:00
Alexander Neumann
094e80f4a4 Merge pull request #723 from ulziibuyan/ls-latest-cmd
Added latest keyword in ls command.
2017-01-15 12:40:58 +01:00
Uzi
c13a0953c8 User interface inconsistency fixed 2017-01-14 11:19:47 +08:00
Alexander Neumann
9f1f174c99 Add script to build release binaries 2017-01-12 19:52:03 +01:00
Alexander Neumann
7e0a4c66e7 build.go: Make binaries completely static (disables cgo) 2017-01-12 19:51:26 +01:00
Alexander Neumann
62b1056860 build.go: Allow setting the output file name 2017-01-12 19:51:08 +01:00
Alexander Neumann
a3181dbead CI: cross-compile for OpenBSD 2017-01-12 19:50:41 +01:00
Uzi
02c02283cf Added latest keyword in ls command. 2017-01-12 19:24:08 +08:00
Alexander Neumann
4d93da9f68 Add VERSION file for 0.3.3 2017-01-08 10:46:43 +01:00
Alexander Neumann
4a6086a14b Merge pull request #718 from mholt/flag-priority
CLI options now override env vars
2017-01-02 20:31:20 +01:00
Matthew Holt
0a34a2d5d8 Consider the environment 2017-01-02 12:21:30 -07:00
Matthew Holt
a394b675b0 CLI options now override env vars 2017-01-02 11:14:22 -07:00
Alexander Neumann
04846b10bc Merge pull request #717 from restic/fix-367
Only add entries to indexes inside PackerManager
2017-01-02 17:18:59 +01:00
Alexander Neumann
f9501e97a2 Only add entries to indexes inside PackerManager
This was a nasty bug. Users reported that restic aborts with panic:

    panic: store new item in finalized index

The code calling panic() is in the Store() method of an index and guards
the failure case that an index is to be modified while it has already
been saved in the repo.

What happens here (at least that's what I suspect): PackerManager calls
Current() on a MasterIndex, which yields one index A. Concurrently,
another goroutine calls Repository.SaveFullIndex(), which in turn calls
MasterIndex.FullIndexes(), which (among others) yields the index A. Then
all indexes are marked as final. Then the other goroutine is executed
which adds an entry to the index A, which is now marked as final. Then
the panic occurs.

The commit solves this by removing MasterIndex.Current() and adding a
Store() method that stores the entry in one non-finalized index. This
method uses the same RWMutex as the other methods (e.g. FullIndexes()),
thereby ensuring that the full indexes can only be processed before or
after Store() is called.

Closes #367
2017-01-02 14:14:51 +01:00
Alexander Neumann
3ef788765a Merge pull request #715 from zcalusic/master
Document REST backend
2017-01-02 11:13:35 +01:00
Alexander Neumann
8e16931949 Merge pull request #716 from zcalusic/rest-server-new-location
Rest server moved to https://github.com/restic/rest-server
2017-01-02 11:12:37 +01:00
Zlatko Čalušić
2267aca296 Rest server moved to https://github.com/restic/rest-server 2017-01-01 16:22:46 +01:00
Zlatko Čalušić
c70bc7ed0b Document REST backend
Closes #644
2016-12-31 13:14:44 +01:00
Alexander Neumann
8e3b81c5ec Merge pull request #713 from restic/update-travis
Update .travis.yml
2016-12-30 17:21:27 +01:00
Alexander Neumann
30975f7116 Update appveyor configuration 2016-12-30 17:07:42 +01:00
Alexander Neumann
0ef463d56a Update .travis.yml 2016-12-30 15:21:49 +01:00
Alexander Neumann
5132f5bfe6 Merge pull request #709 from restic/fix-708
Make sure cleanup is executed before exiting
2016-12-28 18:28:07 +01:00
Alexander Neumann
80457018d7 Make sure cleanup is executed before exiting
Closes #708
2016-12-28 10:53:31 +01:00
Alexander Neumann
b0997d05fb Merge pull request #704 from restic/remove-timestamp
Remove timestamp from `version` command
2016-12-19 22:22:43 +01:00
Alexander Neumann
3add2f0acb Merge pull request #703 from sjoerdsimons/master
Avoid duplicate backup paths
2016-12-19 22:21:18 +01:00
Alexander Neumann
166d1811a1 Remove timestamp from version command
This enables reproducible builds, for details see
https://reproducible-builds.org/docs/timestamps/
2016-12-19 21:14:12 +01:00
Sjoerd Simons
e1fc455079 Avoid duplicate backup paths
Target directories from the from-files argument get added to the command
line args, after which all command line args were appended to the same
variable again causing duplicates. Split the used variables to avoid
this.

Signed-off-by: Sjoerd Simons <sjoerd@luon.net>
2016-12-18 23:23:57 +01:00
Alexander Neumann
98237bf942 Add VERSION file for 0.3.2 2016-12-18 18:53:03 +01:00
Alexander Neumann
75f21f23ff Merge pull request #700 from restic/debug-panic
Make sure SaveFile always returns a node
2016-12-14 21:29:04 +01:00
Alexander Neumann
9885aeac3b Make sure SaveFile always returns a node 2016-12-14 18:56:11 +01:00
Alexander Neumann
85c87b9ab9 Add VERSION file for 0.3.1 2016-12-13 21:36:22 +01:00
Alexander Neumann
51cd78e16c Merge pull request #691 from restic/fix-604
Correctly save modified files
2016-12-10 17:31:20 +01:00
Alexander Neumann
e6a40af06d Treat changed files as a warning, not an error 2016-12-10 17:14:13 +01:00
Alexander Neumann
3fcbb4ac25 Use new Node if file has changed
Closes #604
2016-12-10 16:54:20 +01:00
Alexander Neumann
7d71bad4eb Test if modified files are correctly saved 2016-12-10 16:36:58 +01:00
Alexander Neumann
dbdfed6343 Merge pull request #690 from zcalusic/master
Even if file changes size during backup, still save it
2016-12-10 12:36:56 +01:00
Alexander Neumann
5e48c1fadc Merge pull request #688 from restic/fix-686
Save snapshot after saving all pack files
2016-12-10 12:33:58 +01:00
Zlatko Čalušić
deb6dd7f72 Even if file changes size during backup, still save it
Previously such files (typically log files) wouldn't be backed up at
all!

The proper behaviour is to backup what we can, and warn the operator
that file is possibly not complete. But it is a warning, not an error.

Closes #689
2016-12-10 12:24:45 +01:00
Alexander Neumann
c265673c8e Save snapshot after saving all pack files
Closes #686
2016-12-10 11:49:09 +01:00
Alexander Neumann
0fceeb20f1 Merge pull request #685 from jannic/patch-1
Update debug message
2016-12-06 08:16:33 +01:00
Jan Niehusmann
c5897e0d62 Update debug message
Since client.BucketExists was changed to return a separate 'found' value, instead of reporting an error when the bucket doesn't exist, the error code path does no longer imply a call to client.MakeBucket. So the second part of the debug message, "...trying to create the bucket" doesn't apply any more.
Also, changed the name of the return value from 'ok' to 'found', matching the API documentation at https://docs.minio.io/docs/golang-client-api-reference#BucketExists.
2016-12-05 23:12:30 +01:00
Alexander Neumann
8d13f22c50 Merge pull request #683 from jannic/pr1
Omit "archived as %v" messages in quiet mode.
2016-12-03 11:15:24 +01:00
Alexander Neumann
1815536534 Update build.go 2016-12-03 11:14:30 +01:00
Jan Niehusmann
9267c25aa0 Omit "archived as %v" messages in quiet mode. 2016-12-03 10:28:49 +01:00
Alexander Neumann
281cbbdf2e Merge pull request #682 from jpmens/patch-1
Small typo in dry-run of remove snapshot
2016-12-03 10:11:58 +01:00
JP Mens
5996d671a0 Small typo in dry-run of remove snapshot 2016-12-02 17:33:05 +01:00
Alexander Neumann
ef9b974bcd Merge pull request #681 from zcalusic/master
Stop trying to detect Go version
2016-12-02 11:15:29 +01:00
Zlatko Čalušić
7e66b73ce0 Stop trying to detect Go version
It fails on pre-release versions, anyway.  It's enough to mention the oldest
supported version in README.md.  Anything older than two latest Go releases
is bad idea, anyway, 'cause it's unsupported by Go development team.

Closes #680
2016-12-01 20:06:23 +01:00
Alexander Neumann
505a2097ad Manual: Add note about s3 bucket locations 2016-11-27 20:18:57 +01:00
Alexander Neumann
07380878fb Merge pull request #678 from restic/fix-676
Update github.com/elithrar/simple-scrypt
2016-11-19 19:22:44 +01:00
Alexander Neumann
3b29ae3c99 Update github.com/elithrar/simple-scrypt
Closes #676
2016-11-19 17:13:13 +01:00
Alexander Neumann
e5617b5fd1 Merge pull request #675 from restic/parent-check-hostname
Use the hostname filter to find a parent snasphot
2016-11-19 12:42:40 +01:00
Alexander Neumann
11f23ae663 Merge pull request #673 from Novex/restore-directory-metadata-for-existing-directories
Don't consider a pre-existing directory in the restore path to be a failure
2016-11-19 12:42:31 +01:00
Alexander Neumann
2828003d60 Test that existing files and dirs are restored 2016-11-15 21:41:41 +01:00
Alexander Neumann
16cef3b4c6 Use the hostname filter to find a parent snasphot
Closes #674
2016-11-15 21:04:51 +01:00
Alexander Neumann
699f39e3cf FindLatestSnapshot: Rename parameter to clarify meaning 2016-11-15 21:03:54 +01:00
Seb Patane
33b6a7381b Don't consider a pre-existing directory in the restore path to be a failure
* When a directory already exists, CreateDirAt returns an error stating so
  * This means that the restoreMetadata step is skipped, so for directories which already exist no file permissions, owners, groups, etc will be restored on them
* Not returning the error if it's a "directory exists" error means the metadata will get restored
  * It also removes the superfluous "error for ...: mkdir ...: file exists" messages
* This makes the behaviour of directories consistent with that of files (which always have their content & metadata restored, regardless of whether they existed or not)
2016-11-14 17:53:09 +10:00
Alexander Neumann
190673b24a Merge pull request #657 from AlexanderThaller/read_backup_files_from_file
Read files to backup from a file
2016-11-12 21:47:11 +01:00
Alexander Thaller
b7b03dbd4a Added new flag to backup subcommand that reads the files to backup from a file 2016-11-12 15:45:32 +01:00
Alexander Neumann
56009dd16e Merge pull request #670 from restic/remove-fadvise
Remove fadvise
2016-11-10 23:42:21 +01:00
Alexander Neumann
b56bde3f61 Remove fadvise
This commit removes the use of FADV_DONTNEED, which also purges active
cached pages for other processes.
2016-11-10 22:21:22 +01:00
Alexander Neumann
b1ed74eb43 Merge pull request #669 from zcalusic/master
Fix REST backend HTTP keepalive
2016-11-10 21:05:14 +01:00
Zlatko Čalušić
d8f0e7cbd1 Fix REST backend HTTP keepalive
This is subtle.  A combination od fast client disk (read: SSD) with lots
of files and fast network connection to restic-server would suddenly
start getting lots of "dial tcp: connect: cannot assign requested
address" errors during backup stage.  Further inspection revealed that
client machine was plagued with TCP sockets in TIME_WAIT state.  When
ephemeral port range was finally exhausted, no more sockets could be
opened, so restic would freak out.

To understand the magnitude of this problem, with ~18k ports and default
timeout of 60 seconds, it means more than 300 HTTP connections per
seconds were created and teared down.  Yeah, restic-server is that
fast. :)

As it turns out, this behavior was product of 2 subtle issues:

1) The body of HTTP response wasn't read completely with io.ReadFull()
   at the end of the Load() function.  This deactivated HTTP keepalive,
   so already open connections were not reused, but closed instead, and
   new ones opened for every new request.  io.Copy(ioutil.Discard,
   resp.Body) before resp.Body.Close() remedies this.

2) Even with the above fix, somehow having MaxIdleConnsPerHost at its
   default value of 2 wasn't enough to stop reconnecting.  It is hard to
   understand why this would be so detrimental, it could even be some
   subtle Go runtime bug.  Anyhow, setting this value to match the
   connection limit, as set by connLimit global variable, finally nails
   this ugly bug.

I fixed several other places where the response body wasn't read in
full (or at all).  For example, json.NewDecoder() is also known not to
read the whole body of response.

Unfortunately, this is not over yet. :( The check command is firing up
to 40 simultaneous connections to the restic-server.  Then, once again,
MaxIdleConnsPerHost is too low to support keepalive, and sockets in the
TIME_WAIT state pile up.  But, as this kind of concurrency absolutely
kill the poor disk on the server side, this is a completely different
bug then.
2016-11-10 09:32:07 +01:00
Alexander Neumann
5e721afb5d doc/mkdocs: Improve code hilighting
Additionally, refresh the restic sample output.
2016-11-08 20:23:39 +01:00
Alexander Neumann
149c01a86a Merge pull request #659 from restic/device-freebsd
fs.DeviceID(): Return errors whehn fi is nil
2016-11-05 13:35:16 +01:00
Alexander Neumann
51322a1055 selectFunc: handle nil 2016-11-05 12:38:33 +01:00
Alexander Neumann
c5bc802ff0 fs.DeviceID(): Return errors when fi is nil 2016-11-05 12:38:17 +01:00
Alexander Neumann
6b88d3b5d0 Merge pull request #651 from justinclift/issue649v1
Remove redundant check of error var e
2016-10-26 16:06:14 +02:00
Justin Clift
ecc1f92787 Remove redundant check of error var e
As per #649
2016-10-25 18:10:53 +01:00
Alexander Neumann
d4f76fbe26 Merge pull request #650 from restic/forget-remove-index-load
forget: do not load index
2016-10-24 14:33:11 +02:00
Alexander Neumann
1dd72693f9 forget: Remove unneeded index loading 2016-10-24 14:01:23 +02:00
Alexander Neumann
fe1013e779 cmds/ls: Format timestamp 2016-10-19 22:11:37 +02:00
Alexander Neumann
84ca5172f0 Remove Debian UID from GPG key printout 2016-10-17 13:10:16 +02:00
Alexander Neumann
7c49255c2a Add hints how to use the go tool and direnv 2016-10-17 13:09:56 +02:00
Alexander Neumann
a5a9c42185 Merge pull request #646 from stakewinner00/master
don't print status info when running in the background
2016-10-15 20:09:42 +02:00
David
5f8a6cea6f don't print status info if running in the background
clean

fix OS issues & format code

fix issues
2016-10-15 18:12:19 +00:00
Alexander Neumann
50212805aa Merge pull request #643 from restic/update-poly1305
Update golang.org/x/crypto/poly1305
2016-10-14 15:51:57 +02:00
Alexander Neumann
cd7feb0148 Update golang.org/x/crypto/poly1305 2016-10-14 12:44:06 +02:00
Alexander Neumann
974f2f78a9 Merge pull request #641 from restic/fix-640
Improve error message for 'forget'
2016-10-13 20:41:29 +02:00
Alexander Neumann
250b36eeb1 Improve error message for 'forget'
$ bin/restic forget /d 7 /w 4 /m 12
    argument "/d" is not a snapshot ID, ignoring
    argument "7" is not a snapshot ID, ignoring
    argument "/w" is not a snapshot ID, ignoring
    argument "4" is not a snapshot ID, ignoring
    argument "/m" is not a snapshot ID, ignoring
    cound not find a snapshot for ID "12", ignoring
2016-10-10 20:55:02 +02:00
Alexander Neumann
6f72164bbe Merge pull request #638 from hmsdao/patch-fixpath
Added long paths fix for samba network shares
2016-10-05 17:07:53 +02:00
Daniel Örn
ba8d960c8f using backtics instead of doublequotes 2016-10-05 08:26:32 +02:00
Daniel Örn
84421a7c68 structured file with gofmt 2016-10-05 07:30:46 +02:00
Daniel Örn
5c7325f44a Added long paths fix for samba network shares 2016-10-05 07:09:56 +02:00
Alexander Neumann
c45b498a8b Merge pull request #637 from ckemper67/s3-join
Use path.Join to create the s3 object name within the bucket.
2016-10-03 22:38:28 +02:00
Christian Kemper
a4261dcc9c Use path.Join to create the s3 object name within the bucket.
path.Join already automatically skips empty path segments when
joining, so this simplifies the s3Path code.
2016-10-02 16:56:07 -07:00
589 changed files with 51821 additions and 15103 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
GOPATH=$PWD:$PWD/vendor

View File

@@ -1,6 +1,18 @@
<!--
NOTE: Not filling out the issue template needs a good reason, otherwise the
issue may be closed instantly! Please take the time to help us debugging the
problem by collecting information, even if it seems irrelevant to you. Thanks!
-->
## Output of `restic version`
## How did you start restic exactly? (Include the complete command line)
## What backend/server/service did you use?
## Expected behavior
@@ -10,3 +22,4 @@
## Steps to reproduce the behavior
## Do you have any idea what may have caused this?

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
/restic
/.vagrant
/vendor/pkg
/doc/_build

View File

@@ -2,8 +2,9 @@ language: go
sudo: false
go:
- 1.6.3
- 1.7.1
- 1.7.5
- 1.8.1
- tip
os:
- linux
@@ -16,16 +17,23 @@ env:
matrix:
exclude:
- os: osx
go: 1.6.3
go: 1.7.5
- os: osx
go: tip
- os: linux
go: 1.7.1
go: 1.8.1
include:
- os: linux
go: 1.7.1
go: 1.8.1
sudo: true
env:
RESTIC_TEST_FUSE=1
allow_failures:
- go: tip
branches:
only:
- master
notifications:
irc:
@@ -40,11 +48,9 @@ install:
- export GOBIN="$GOPATH/bin"
- export PATH="$PATH:$GOBIN"
- go env
- ulimit -n 2048
script:
- go run run_integration_tests.go
after_success:
- GOPATH=$PWD:$PWD/vendor goveralls -coverprofile=all.cov -service=travis-ci -repotoken "$COVERALLS_TOKEN"
- bash <(curl -s https://codecov.io/bash) -f all.cov

45
CHANGELOG.md Normal file
View File

@@ -0,0 +1,45 @@
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.6.0
==========================
Consistent forget policy
------------------------
The `forget` command was corrected to be more consistent in which snapshots are
to be forgotten. It is possible that the new code removes more snapshots than
before, so please review what would be deleted by using the `--dry-run` option.
https://github.com/restic/restic/pull/957
https://github.com/restic/restic/issues/953
Unified repository layout
-------------------------
Up to now the s3 backend used a special repository layout. We've decided to
unify the repository layout and implemented the default layout also for the s3
backend. For creating a new repository on s3 with the default layout, use
`restic -o s3.layout=default init`. For further commands the option is not
necessary any more, restic will automatically detect the correct layout to use.
A future version will switch to the default layout for new repositories.
https://github.com/restic/restic/pull/966
https://github.com/restic/restic/issues/965
Memory and time improvements for the s3 backend
-----------------------------------------------
We've updated the library used for accessing s3, switched to using a lower
level API and added caching for some requests. This lead to a decrease in
memory usage and a great speedup. In addition, we added benchmark functions for
all backends, so we can track improvements over time. The Continuous
Integration test service we're using (Travis) now runs the s3 backend tests not
only against a Minio server, but also against the Amazon s3 live service, so we
should be notified of any regressions much sooner.
https://github.com/restic/restic/pull/962
https://github.com/restic/restic/pull/960
https://github.com/restic/restic/pull/946
https://github.com/restic/restic/pull/938
https://github.com/restic/restic/pull/883

View File

@@ -3,7 +3,10 @@ This document describes the way you can contribute to the restic project.
Ways to Help Out
================
Thank you for your contribution!
Thank you for your contribution! Please **open an issue first** (or add a
comment to an existing issue) if you plan to work on any code or add a new
feature. This way, duplicate work is prevented and we can discuss your ideas
and design first.
There are several ways you can help us out. First of all code contributions and
bug fixes are most welcome. However even "minor" details as fixing spelling
@@ -74,7 +77,7 @@ Just clone the repository, `cd` to it and run `gb build` to build the binary:
[...]
$ bin/restic version
restic compiled manually
compiled at unknown time with go1.6
compiled at unknown time with go1.7
The following commands can be used to run all the tests:
@@ -83,7 +86,7 @@ The following commands can be used to run all the tests:
[...]
If you want to run your tests on Linux, OpenBSD or FreeBSD, you can use
[vagrant](https://www.vagrantup.com/) with the proveded `Vagrantfile` to
[vagrant](https://www.vagrantup.com/) with the provided `Vagrantfile` to
quickly set up VMs and run the tests, e.g.:
$ vagrant up freebsd
@@ -92,6 +95,16 @@ quickly set up VMs and run the tests, e.g.:
$ vagrant ssh freebsd -c 'cd restic/restic; go test -v ./...'
[...]
The default `go` tool can also be used by setting the environment variable
`GOPATH` to the following value while being in the top level directory in the
git repository:
$ export GOPATH=$PWD:$PWD/vendor
The file `.envrc` allows automatic `GOPATH` configuration with
[direnv](https://direnv.net/), inspect the file and then allow automatic
configuration by running `direnv allow`.
Providing Patches
=================
@@ -124,7 +137,13 @@ down to the following steps:
commits to the branch you created for the pull request, they will be
automatically added to the pull request.
7. Once your code looks good and passes all the tests, we'll merge it. Thanks
7. If your pull request changes anything that users should be aware of (a
bugfix, a new feature, ...) please add an entry to the file
['CHANGELOG.md'](CHANGELOG.md). It will be used in the announcement of the
next stable release. While writing, ask yourself: If I were the user, what
would I need to be aware of with this change.
8. Once your code looks good and passes all the tests, we'll merge it. Thanks
a low for your contribution!
Please provide the patches for each bug or feature in a separate branch and

View File

@@ -14,11 +14,11 @@
# docker run --rm -v $PWD:/home/travis/restic restic/test gb test -v ./backend
#
# build the image for an older version of Go:
# docker build --build-arg GOVERSION=1.3.3 -t restic/test:go1.3.3 .
# docker build --build-arg GOVERSION=1.6.4 -t restic/test:go1.6.4 .
FROM ubuntu:14.04
ARG GOVERSION=1.7
ARG GOVERSION=1.7.5
ARG GOARCH=amd64
# install dependencies

View File

@@ -1,78 +0,0 @@
[![Documentation](https://readthedocs.org/projects/restic/badge/?version=latest)](https://restic.readthedocs.io/en/latest/?badge=latest)
[![Build Status](https://travis-ci.org/restic/restic.svg?branch=master)](https://travis-ci.org/restic/restic)
[![Build status](https://ci.appveyor.com/api/projects/status/nuy4lfbgfbytw92q/branch/master?svg=true)](https://ci.appveyor.com/project/fd0/restic/branch/master)
[![Report Card](http://goreportcard.com/badge/github.com/restic/restic)](http://goreportcard.com/report/github.com/restic/restic)
Introduction
============
restic is a backup program that is fast, efficient and secure. Detailed
information can be found in [the documentation](doc/index.md) and [the user
manual](doc/Manual.md). The [design document](doc/Design.md) lists the
technical background and gives detailed information about the structure of the
repository and the data saved therein.
The latest documentation can be viewed online at
<https://restic.readthedocs.io/en/latest>. On the bottom left corner there is
a menu that allows switching to the documentation and user manual for the
latest released version.
Build restic
============
Install Go/Golang (at least version 1.6), then run `go run build.go`,
afterwards you'll find the binary in the current directory:
$ go run build.go
$ ./restic --help
Usage:
restic [OPTIONS] <command>
[...]
More documentation can be found in the [user manual](doc/Manual.md).
At the moment, the only tested compiler for restic is the official Go compiler.
Building restic with gccgo may work, but is not supported.
Contribute and Documentation
============================
Contributions are welcome! More information and a description of the
development environment can be found in [`CONTRIBUTING.md`](CONTRIBUTING.md). A
document describing the design of restic and the data structures stored on the
back end is contained in [`doc/Design.md`](doc/Design.md).
If you'd like to start contributing to restic, but don't know exactly what do
to, have a look at this great article by Dave Cheney:
[Suggestions for contributing to an Open Source project](http://dave.cheney.net/2016/03/12/suggestions-for-contributing-to-an-open-source-project)
A few issues have been tagged with the label `help wanted`, you can start
looking at those: https://github.com/restic/restic/labels/help%20wanted
Contact
=======
If you discover a bug, find something surprising or if you would like to
discuss or ask something, please [open a github issue](https://github.com/restic/restic/issues/new).
If you would like to chat about restic, there is also the IRC channel #restic
on irc.freenode.net.
**Important**: If you discover something that you believe to be a possible critical
security problem, please do *not* open a GitHub issue but send an email directly to
alexander@bumpern.de. If possible, please encrypt your email using the following PGP key
([0x91A6868BD3F7A907](https://pgp.mit.edu/pks/lookup?op=get&search=0xCF8F18F2844575973F79D4E191A6868BD3F7A907)):
```
pub 4096R/91A6868BD3F7A907 2014-11-01
Key fingerprint = CF8F 18F2 8445 7597 3F79 D4E1 91A6 868B D3F7 A907
uid Alexander Neumann <alexander@bumpern.de>
uid Alexander Neumann <alexander@debian.org>
sub 4096R/D5FC2ACF4043FDF1 2014-11-01
```
License
=======
Restic is licensed under "BSD 2-Clause License". You can find the complete text
in the file `LICENSE`.

92
README.rst Normal file
View File

@@ -0,0 +1,92 @@
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks|
Introduction
------------
restic is a backup program that is fast, efficient and secure.
For detailed usage and installation instructions check out the `documentation <https://restic.readthedocs.io/en/latest>`__.
Quick start
-----------
Once you've `installed
<https://restic.readthedocs.io/en/latest/installation.html>`__ restic, start
off with creating a repository for your backups:
.. code-block:: console
$ restic init --repo /tmp/backup
enter password for new backend:
enter password again:
created restic backend 085b3c76b9 at /tmp/backup
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
and add some data:
.. code-block:: console
$ restic -r /tmp/backup backup ~/work
enter password for repository:
scan [/home/user/work]
scanned 764 directories, 1816 files in 0:00
[0:29] 100.00% 54.732 MiB/s 1.582 GiB / 1.582 GiB 2580 / 2580 items 0 errors ETA 0:00
duration: 0:29, 54.47MiB/s
snapshot 40dc1520 saved
For more options check out the `manual guide <https://restic.readthedocs.io/en/latest/manual.html>`__.
Design Principles
-----------------
Restic is a program that does backups right and was designed with the
following principles in mind:
- **Easy:** Doing backups should be a frictionless process, otherwise
you might be tempted to skip it. Restic should be easy to configure
and use, so that, in the event of a data loss, you can just restore
it. Likewise, restoring data should not be complicated.
- **Fast**: Backing up your data with restic should only be limited by
your network or hard disk bandwidth so that you can backup your files
every day. Nobody does backups if it takes too much time. Restoring
backups should only transfer data that is needed for the files that
are to be restored, so that this process is also fast.
- **Verifiable**: Much more important than backup is restore, so restic
enables you to easily verify that all data can be restored.
- **Secure**: Restic uses cryptography to guarantee confidentiality and
integrity of your data. The location the backup data is stored is
assumed not to be a trusted environment (e.g. a shared space where
others like system administrators are able to access your backups).
Restic is built to secure your data against such attackers.
- **Efficient**: With the growth of data, additional snapshots should
only take the storage of the actual increment. Even more, duplicate
data should be de-duplicated before it is actually written to the
storage back end to save precious backup space.
News
----
You can follow the restic project on Twitter `@resticbackup <https://twitter.com/resticbackup>`__ or by subscribing to
the `development blog <https://restic.github.io/blog/>`__.
License
-------
Restic is licensed under "BSD 2-Clause License". You can find the
complete text in ``LICENSE``.
.. |Documentation| image:: https://readthedocs.org/projects/restic/badge/?version=latest
:target: https://restic.readthedocs.io/en/latest/?badge=latest
.. |Build Status| image:: https://travis-ci.org/restic/restic.svg?branch=master
:target: https://travis-ci.org/restic/restic
.. |Build status| image:: https://ci.appveyor.com/api/projects/status/nuy4lfbgfbytw92q/branch/master?svg=true
:target: https://ci.appveyor.com/project/fd0/restic/branch/master
.. |Report Card| image:: http://goreportcard.com/badge/github.com/restic/restic
:target: http://goreportcard.com/report/github.com/restic/restic
.. |Say Thanks| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
:target: https://saythanks.io/to/restic

View File

@@ -1 +1 @@
0.3.0
0.6.0

2
Vagrantfile vendored
View File

@@ -1,7 +1,7 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
GO_VERSION = '1.6'
GO_VERSION = '1.7'
def packages_freebsd
return <<-EOF

View File

@@ -3,6 +3,10 @@ clone_folder: c:\restic
environment:
GOPATH: c:\gopath
branches:
only:
- master
init:
- ps: >-
$app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'"
@@ -13,8 +17,8 @@ init:
install:
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.7.windows-amd64.msi
- msiexec /i go1.7.windows-amd64.msi /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.8.1.windows-amd64.msi
- msiexec /i go1.8.1.windows-amd64.msi /q
- go version
- go env
- appveyor DownloadFile http://sourceforge.netcologne.de/project/gnuwin32/tar/1.13-1/tar-1.13-1-bin.zip -FileName tar.zip

106
build.go
View File

@@ -12,16 +12,26 @@ import (
"path/filepath"
"runtime"
"strings"
"time"
)
var (
verbose bool
keepGopath bool
runTests bool
enableCGO bool
)
const timeFormat = "2006-01-02 15:04:05"
var config = struct {
Name string
Namespace string
Main string
Tests []string
}{
Name: "restic", // name of the program executable and directory
Namespace: "", // subdir of GOPATH, e.g. "github.com/foo/bar"
Main: "cmds/restic", // package name for the main package
Tests: []string{"restic/...", "cmds/..."}, // tests to run
}
// specialDir returns true if the file begins with a special character ('.' or '_').
func specialDir(name string) bool {
@@ -40,7 +50,7 @@ func specialDir(name string) bool {
// excludePath returns true if the file should not be copied to the new GOPATH.
func excludePath(name string) bool {
ext := path.Ext(name)
if ext == ".go" || ext == ".s" {
if ext == ".go" || ext == ".s" || ext == ".h" {
return false
}
@@ -96,6 +106,15 @@ func updateGopath(dst, src, prefix string) error {
})
}
func directoryExists(dirname string) bool {
stat, err := os.Stat(dirname)
if err != nil && os.IsNotExist(err) {
return false
}
return stat.IsDir()
}
// copyFile creates dst from src, preserving file attributes and timestamps.
func copyFile(dst, src string) error {
fi, err := os.Stat(src)
@@ -107,6 +126,7 @@ func copyFile(dst, src string) error {
if err != nil {
return err
}
defer fsrc.Close()
if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
fmt.Printf("MkdirAll(%v)\n", filepath.Dir(dst))
@@ -117,28 +137,17 @@ func copyFile(dst, src string) error {
if err != nil {
return err
}
defer fdst.Close()
if _, err = io.Copy(fdst, fsrc); err != nil {
return err
}
if err == nil {
err = fsrc.Close()
}
if err == nil {
err = fdst.Close()
}
_, err = io.Copy(fdst, fsrc)
if err == nil {
err = os.Chmod(dst, fi.Mode())
}
if err == nil {
err = os.Chtimes(dst, fi.ModTime(), fi.ModTime())
}
return nil
return err
}
// die prints the message with fmt.Fprintf() to stderr and exits with an error
@@ -156,6 +165,8 @@ func showUsage(output io.Writer) {
fmt.Fprintf(output, " -t --tags specify additional build tags\n")
fmt.Fprintf(output, " -k --keep-gopath do not remove the GOPATH after build\n")
fmt.Fprintf(output, " -T --test run tests\n")
fmt.Fprintf(output, " -o --output set output file name\n")
fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n")
fmt.Fprintf(output, " --goos value set GOOS for cross-compilation\n")
fmt.Fprintf(output, " --goarch value set GOARCH for cross-compilation\n")
}
@@ -187,6 +198,10 @@ func build(cwd, goos, goarch, gopath string, args ...string) error {
args = append([]string{"build"}, args...)
cmd := exec.Command("go", args...)
cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos)
if !enableCGO {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
}
cmd.Dir = cwd
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -264,22 +279,18 @@ type Constants map[string]string
func (cs Constants) LDFlags() string {
l := make([]string, 0, len(cs))
if runtime.Version() < "go1.5" {
for k, v := range cs {
l = append(l, fmt.Sprintf(`-X %q %q`, k, v))
}
} else {
for k, v := range cs {
l = append(l, fmt.Sprintf(`-X "%s=%s"`, k, v))
}
for k, v := range cs {
l = append(l, fmt.Sprintf(`-X "%s=%s"`, k, v))
}
return strings.Join(l, " ")
}
func main() {
if runtime.Version() < "go1.6" {
fmt.Fprintf(os.Stderr, "old version of Go detected (%v), I'll try but no guarantees\n", runtime.Version())
ver := runtime.Version()
if strings.HasPrefix(ver, "go1") && ver < "go1.7" {
fmt.Fprintf(os.Stderr, "Go version %s detected, restic requires at least Go 1.7\n", ver)
os.Exit(1)
}
buildTags := []string{}
@@ -290,6 +301,8 @@ func main() {
targetGOOS := runtime.GOOS
targetGOARCH := runtime.GOARCH
var outputFilename string
for i, arg := range params {
if skipNext {
skipNext = false
@@ -302,10 +315,18 @@ func main() {
case "-k", "--keep-gopath":
keepGopath = true
case "-t", "-tags", "--tags":
if i+1 >= len(params) {
die("-t given but no tag specified")
}
skipNext = true
buildTags = strings.Split(params[i+1], " ")
case "-o", "--output":
skipNext = true
outputFilename = params[i+1]
case "-T", "--test":
runTests = true
case "--enable-cgo":
enableCGO = true
case "--goos":
skipNext = true
targetGOOS = params[i+1]
@@ -338,23 +359,21 @@ func main() {
die("Getwd(): %v\n", err)
}
gopath, err := ioutil.TempDir("", "restic-build-")
gopath, err := ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
if err != nil {
die("TempDir(): %v\n", err)
}
verbosePrintf("create GOPATH at %v\n", gopath)
if err = updateGopath(gopath, filepath.Join(root, "src", "restic"), "restic"); err != nil {
die("copying files from %v/src/restic to %v/src/restic failed: %v\n", root, gopath, err)
}
if err = updateGopath(gopath, filepath.Join(root, "src", "cmds"), "cmds"); err != nil {
die("copying files from %v/src/cmds to %v/src/restic/cmds failed: %v\n", root, gopath, err)
if err = updateGopath(gopath, filepath.Join(root, "src"), config.Namespace); err != nil {
die("copying files from %v/src to %v/src failed: %v\n", root, gopath, err)
}
vendor := filepath.Join(root, "vendor", "src")
if err = updateGopath(gopath, vendor, ""); err != nil {
die("copying files from %v to %v/src failed: %v\n", vendor, gopath, err)
if directoryExists(vendor) {
if err = updateGopath(gopath, vendor, ""); err != nil {
die("copying files from %v to %v failed: %v\n", root, gopath, err)
}
}
defer func() {
@@ -368,9 +387,11 @@ func main() {
}
}()
outputFilename := "restic"
if targetGOOS == "windows" {
outputFilename = "restic.exe"
if outputFilename == "" {
outputFilename = config.Name
if targetGOOS == "windows" {
outputFilename += ".exe"
}
}
cwd, err := os.Getwd()
@@ -380,8 +401,7 @@ func main() {
output := filepath.Join(cwd, outputFilename)
version := getVersion()
compileTime := time.Now().Format(timeFormat)
constants := Constants{`main.compiledAt`: compileTime}
constants := Constants{}
if version != "" {
constants["main.version"] = version
}
@@ -391,7 +411,7 @@ func main() {
args := []string{
"-tags", strings.Join(buildTags, " "),
"-ldflags", ldflags,
"-o", output, "cmds/restic",
"-o", output, config.Main,
}
err = build(filepath.Join(gopath, "src"), targetGOOS, targetGOARCH, gopath, args...)
@@ -402,7 +422,7 @@ func main() {
if runTests {
verbosePrintf("running tests\n")
err = test(filepath.Join(gopath, "src"), gopath, "restic/...")
err = test(cwd, gopath, config.Tests...)
if err != nil {
die("running tests failed: %v\n", err)
}

64
build_release_binaries.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/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}"

2
codecov.yml Normal file
View File

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

View File

@@ -1,489 +0,0 @@
This document gives a high-level overview of the design and repository layout
of the restic backup program.
Terminology
===========
This section introduces terminology used in this document.
*Repository*: All data produced during a backup is sent to and stored in a
repository in a structured form, for example in a file system hierarchy with
several subdirectories. A repository implementation must be able to fulfill a
number of operations, e.g. list the contents.
*Blob*: A Blob combines a number of data bytes with identifying information
like the SHA-256 hash of the data and its length.
*Pack*: A Pack combines one or more Blobs, e.g. in a single file.
*Snapshot*: A Snapshot stands for the state of a file or directory that has
been backed up at some point in time. The state here means the content and meta
data like the name and modification time for the file or the directory and its
contents.
*Storage ID*: A storage ID is the SHA-256 hash of the content stored in the
repository. This ID is required in order to load the file from the repository.
Repository Format
=================
All data is stored in a restic repository. A repository is able to store data
of several different types, which can later be requested based on an ID. This
so-called "storage ID" is the SHA-256 hash of the content of a file. All files
in a repository are only written once and never modified afterwards. This
allows accessing and even writing to the repository with multiple clients in
parallel. Only the delete operation removes data from the repository.
At the time of writing, the only implemented repository type is based on
directories and files. Such repositories can be accessed locally on the same
system or via the integrated SFTP client (or any other storage back end).
The directory layout is the same for both access methods.
This repository type is described in the following section.
Repositories consist of several directories and a file called `config`. For
all other files stored in the repository, the name for the file is the lower
case hexadecimal representation of the storage ID, which is the SHA-256 hash of
the file's contents. This allows for easy verification of files for accidental
modifications, like disk read errors, by simply running the program `sha256sum`
and comparing its output to the file name. If the prefix of a filename is
unique amongst all the other files in the same directory, the prefix may be
used instead of the complete filename.
Apart from the files stored within the `keys` directory, all files are encrypted
with AES-256 in counter mode (CTR). The integrity of the encrypted data is
secured by a Poly1305-AES message authentication code (sometimes also referred
to as a "signature").
In the first 16 bytes of each encrypted file the initialisation vector (IV) is
stored. It is followed by the encrypted data and completed by the 16 byte
MAC. The format is: `IV || CIPHERTEXT || MAC`. The complete encryption
overhead is 32 bytes. For each file, a new random IV is selected.
The file `config` is encrypted this way and contains a JSON document like the
following:
{
"version": 1,
"id": "5956a3f67a6230d4a92cefb29529f10196c7d92582ec305fd71ff6d331d6271b",
"chunker_polynomial": "25b468838dcb75"
}
After decryption, restic first checks that the version field contains a version
number that it understands, otherwise it aborts. At the moment, the version is
expected to be 1. The field `id` holds a unique ID which consists of 32
random bytes, encoded in hexadecimal. This uniquely identifies the repository,
regardless if it is accessed via SFTP or locally. The field
`chunker_polynomial` contains a parameter that is used for splitting large
files into smaller chunks (see below).
The basic layout of a sample restic repository is shown here:
/tmp/restic-repo
├── config
├── data
│ ├── 21
│ │ └── 2159dd48f8a24f33c307b750592773f8b71ff8d11452132a7b2e2a6a01611be1
│ ├── 32
│ │ └── 32ea976bc30771cebad8285cd99120ac8786f9ffd42141d452458089985043a5
│ ├── 59
│ │ └── 59fe4bcde59bd6222eba87795e35a90d82cd2f138a27b6835032b7b58173a426
│ ├── 73
│ │ └── 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c
│ [...]
├── index
│ ├── c38f5fb68307c6a3e3aa945d556e325dc38f5fb68307c6a3e3aa945d556e325d
│ └── ca171b1b7394d90d330b265d90f506f9984043b342525f019788f97e745c71fd
├── keys
│ └── b02de829beeb3c01a63e6b25cbd421a98fef144f03b9a02e46eff9e2ca3f0bd7
├── locks
├── snapshots
│ └── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec
└── tmp
A repository can be initialized with the `restic init` command, e.g.:
$ restic -r /tmp/restic-repo init
Pack Format
-----------
All files in the repository except Key and Pack files just contain raw data,
stored as `IV || Ciphertext || MAC`. Pack files may contain one or more Blobs
of data.
A Pack's structure is as follows:
EncryptedBlob1 || ... || EncryptedBlobN || EncryptedHeader || Header_Length
At the end of the Pack file is a header, which describes the content. The
header is encrypted and authenticated. `Header_Length` is the length of the
encrypted header encoded as a four byte integer in little-endian encoding.
Placing the header at the end of a file allows writing the blobs in a
continuous stream as soon as they are read during the backup phase. This
reduces code complexity and avoids having to re-write a file once the pack is
complete and the content and length of the header is known.
All the blobs (`EncryptedBlob1`, `EncryptedBlobN` etc.) are authenticated and
encrypted independently. This enables repository reorganisation without having
to touch the encrypted Blobs. In addition it also allows efficient indexing,
for only the header needs to be read in order to find out which Blobs are
contained in the Pack. Since the header is authenticated, authenticity of the
header can be checked without having to read the complete Pack.
After decryption, a Pack's header consists of the following elements:
Type_Blob1 || Length(EncryptedBlob1) || Hash(Plaintext_Blob1) ||
[...]
Type_BlobN || Length(EncryptedBlobN) || Hash(Plaintext_Blobn) ||
This is enough to calculate the offsets for all the Blobs in the Pack. Length
is the length of a Blob as a four byte integer in little-endian format. The
type field is a one byte field and labels the content of a blob according to
the following table:
Type | Meaning
-----|---------
0 | data
1 | tree
All other types are invalid, more types may be added in the future.
For reconstructing the index or parsing a pack without an index, first the last
four bytes must be read in order to find the length of the header. Afterwards,
the header can be read and parsed, which yields all plaintext hashes, types,
offsets and lengths of all included blobs.
Indexing
--------
Index files contain information about Data and Tree Blobs and the Packs they
are contained in and store this information in the repository. When the local
cached index is not accessible any more, the index files can be downloaded and
used to reconstruct the index. The files are encrypted and authenticated like
Data and Tree Blobs, so the outer structure is `IV || Ciphertext || MAC` again.
The plaintext consists of a JSON document like the following:
{
"supersedes": [
"ed54ae36197f4745ebc4b54d10e0f623eaaaedd03013eb7ae90df881b7781452"
],
"packs": [
{
"id": "73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c",
"blobs": [
{
"id": "3ec79977ef0cf5de7b08cd12b874cd0f62bbaf7f07f3497a5b1bbcc8cb39b1ce",
"type": "data",
"offset": 0,
"length": 25
},{
"id": "9ccb846e60d90d4eb915848add7aa7ea1e4bbabfc60e573db9f7bfb2789afbae",
"type": "tree",
"offset": 38,
"length": 100
},
{
"id": "d3dc577b4ffd38cc4b32122cabf8655a0223ed22edfd93b353dc0c3f2b0fdf66",
"type": "data",
"offset": 150,
"length": 123
}
]
}, [...]
]
}
This JSON document lists Packs and the blobs contained therein. In this
example, the Pack `73d04e61` contains two data Blobs and one Tree blob, the
plaintext hashes are listed afterwards.
The field `supersedes` lists the storage IDs of index files that have been
replaced with the current index file. This happens when index files are
repacked, for example when old snapshots are removed and Packs are recombined.
There may be an arbitrary number of index files, containing information on
non-disjoint sets of Packs. The number of packs described in a single file is
chosen so that the file size is kept below 8 MiB.
Keys, Encryption and MAC
------------------------
All data stored by restic in the repository is encrypted with AES-256 in
counter mode and authenticated using Poly1305-AES. For encrypting new data first
16 bytes are read from a cryptographically secure pseudorandom number generator
as a random nonce. This is used both as the IV for counter mode and the nonce
for Poly1305. This operation needs three keys: A 32 byte for AES-256 for
encryption, a 16 byte AES key and a 16 byte key for Poly1305. For details see
the original paper [The Poly1305-AES message-authentication
code](http://cr.yp.to/mac/poly1305-20050329.pdf) by Dan Bernstein.
The data is then encrypted with AES-256 and afterwards a message authentication
code (MAC) is computed over the ciphertext, everything is then stored as
IV || CIPHERTEXT || MAC.
The directory `keys` contains key files. These are simple JSON documents which
contain all data that is needed to derive the repository's master encryption and
message authentication keys from a user's password. The JSON document from the
repository can be pretty-printed for example by using the Python module `json`
(shortened to increase readability):
$ python -mjson.tool /tmp/restic-repo/keys/b02de82*
{
"hostname": "kasimir",
"username": "fd0"
"kdf": "scrypt",
"N": 65536,
"r": 8,
"p": 1,
"created": "2015-01-02T18:10:13.48307196+01:00",
"data": "tGwYeKoM0C4j4/9DFrVEmMGAldvEn/+iKC3te/QE/6ox/V4qz58FUOgMa0Bb1cIJ6asrypCx/Ti/pRXCPHLDkIJbNYd2ybC+fLhFIJVLCvkMS+trdywsUkglUbTbi+7+Ldsul5jpAj9vTZ25ajDc+4FKtWEcCWL5ICAOoTAxnPgT+Lh8ByGQBH6KbdWabqamLzTRWxePFoYuxa7yXgmj9A==",
"salt": "uW4fEI1+IOzj7ED9mVor+yTSJFd68DGlGOeLgJELYsTU5ikhG/83/+jGd4KKAaQdSrsfzrdOhAMftTSih5Ux6w==",
}
When the repository is opened by restic, the user is prompted for the
repository password. This is then used with `scrypt`, a key derivation function
(KDF), and the supplied parameters (`N`, `r`, `p` and `salt`) to derive 64 key
bytes. The first 32 bytes are used as the encryption key (for AES-256) and the
last 32 bytes are used as the message authentication key (for Poly1305-AES).
These last 32 bytes are divided into a 16 byte AES key `k` followed by 16 bytes
of secret key `r`. The key `r` is then masked for use with Poly1305 (see the
paper for details).
Those message authentication keys (`k` and `r`) are used to compute a MAC over
the bytes contained in the JSON field `data` (after removing the Base64
encoding and not including the last 32 byte). If the password is incorrect or
the key file has been tampered with, the computed MAC will not match the last
16 bytes of the data, and restic exits with an error. Otherwise, the data is
decrypted with the encryption key derived from `scrypt`. This yields a JSON
document which contains the master encryption and message authentication keys
for this repository (encoded in Base64). The command `restic cat masterkey` can
be used as follows to decrypt and pretty-print the master key:
$ restic -r /tmp/restic-repo cat masterkey
{
"mac": {
"k": "evFWd9wWlndL9jc501268g==",
"r": "E9eEDnSJZgqwTOkDtOp+Dw=="
},
"encrypt": "UQCqa0lKZ94PygPxMRqkePTZnHRYh1k1pX2k2lM2v3Q=",
}
All data in the repository is encrypted and authenticated with these master keys.
For encryption, the AES-256 algorithm in Counter mode is used. For message
authentication, Poly1305-AES is used as described above.
A repository can have several different passwords, with a key file for each.
This way, the password can be changed without having to re-encrypt all data.
Snapshots
---------
A snapshots represents a directory with all files and sub-directories at a
given point in time. For each backup that is made, a new snapshot is created. A
snapshot is a JSON document that is stored in an encrypted file below the
directory `snapshots` in the repository. The filename is the storage ID. This
string is unique and used within restic to uniquely identify a snapshot.
The command `restic cat snapshot` can be used as follows to decrypt and
pretty-print the contents of a snapshot file:
$ restic -r /tmp/restic-repo cat snapshot 22a5af1b
enter password for repository:
{
"time": "2015-01-02T18:10:50.895208559+01:00",
"tree": "2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf",
"dir": "/tmp/testdata",
"hostname": "kasimir",
"username": "fd0",
"uid": 1000,
"gid": 100
}
Here it can be seen that this snapshot represents the contents of the directory
`/tmp/testdata`. The most important field is `tree`.
All content within a restic repository is referenced according to its SHA-256
hash. Before saving, each file is split into variable sized Blobs of data. The
SHA-256 hashes of all Blobs are saved in an ordered list which then represents
the content of the file.
In order to relate these plaintext hashes to the actual location within a Pack
file , an index is used. If the index is not available, the header of all data
Blobs can be read.
Trees and Data
--------------
A snapshot references a tree by the SHA-256 hash of the JSON string
representation of its contents. Trees and data are saved in pack files in a
subdirectory of the directory `data`.
The command `restic cat tree` can be used to inspect the tree referenced above:
$ restic -r /tmp/restic-repo cat tree b8138ab08a4722596ac89c917827358da4672eac68e3c03a8115b88dbf4bfb59
enter password for repository:
{
"nodes": [
{
"name": "testdata",
"type": "dir",
"mode": 493,
"mtime": "2014-12-22T14:47:59.912418701+01:00",
"atime": "2014-12-06T17:49:21.748468803+01:00",
"ctime": "2014-12-22T14:47:59.912418701+01:00",
"uid": 1000,
"gid": 100,
"user": "fd0",
"inode": 409704562,
"content": null,
"subtree": "b26e315b0988ddcd1cee64c351d13a100fedbc9fdbb144a67d1b765ab280b4dc"
}
]
}
A tree contains a list of entries (in the field `nodes`) which contain meta
data like a name and timestamps. When the entry references a directory, the
field `subtree` contains the plain text ID of another tree object.
When the command `restic cat tree` is used, the storage hash is needed to print
a tree. The tree referenced above can be dumped as follows:
$ restic -r /tmp/restic-repo cat tree 8b238c8811cc362693e91a857460c78d3acf7d9edb2f111048691976803cf16e
enter password for repository:
{
"nodes": [
{
"name": "testfile",
"type": "file",
"mode": 420,
"mtime": "2014-12-06T17:50:23.34513538+01:00",
"atime": "2014-12-06T17:50:23.338468713+01:00",
"ctime": "2014-12-06T17:50:23.34513538+01:00",
"uid": 1000,
"gid": 100,
"user": "fd0",
"inode": 416863351,
"size": 1234,
"links": 1,
"content": [
"50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d"
]
},
[...]
]
}
This tree contains a file entry. This time, the `subtree` field is not present
and the `content` field contains a list with one plain text SHA-256 hash.
The command `restic cat data` can be used to extract and decrypt data given a
plaintext ID, e.g. for the data mentioned above:
$ restic -r /tmp/restic-repo cat blob 50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d | sha256sum
enter password for repository:
50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d -
As can be seen from the output of the program `sha256sum`, the hash matches the
plaintext hash from the map included in the tree above, so the correct data has
been returned.
Locks
-----
The restic repository structure is designed in a way that allows parallel
access of multiple instance of restic and even parallel writes. However, there
are some functions that work more efficient or even require exclusive access of
the repository. In order to implement these functions, restic processes are
required to create a lock on the repository before doing anything.
Locks come in two types: Exclusive and non-exclusive locks. At most one
process can have an exclusive lock on the repository, and during that time
there must not be any other locks (exclusive and non-exclusive). There may be
multiple non-exclusive locks in parallel.
A lock is a file in the subdir `locks` whose filename is the storage ID of
the contents. It is encrypted and authenticated the same way as other files
in the repository and contains the following JSON structure:
{
"time": "2015-06-27T12:18:51.759239612+02:00",
"exclusive": false,
"hostname": "kasimir",
"username": "fd0",
"pid": 13607,
"uid": 1000,
"gid": 100
}
The field `exclusive` defines the type of lock. When a new lock is to be
created, restic checks all locks in the repository. When a lock is found, it
is tested if the lock is stale, which is the case for locks with timestamps
older than 30 minutes. If the lock was created on the same machine, even for
younger locks it is tested whether the process is still alive by sending a
signal to it. If that fails, restic assumes that the process is dead and
considers the lock to be stale.
When a new lock is to be created and no other conflicting locks are
detected, restic creates a new lock, waits, and checks if other locks
appeared in the repository. Depending on the type of the other locks and the
lock to be created, restic either continues or fails.
Backups and Deduplication
=========================
For creating a backup, restic scans the source directory for all files,
sub-directories and other entries. The data from each file is split into
variable length Blobs cut at offsets defined by a sliding window of 64 byte.
The implementation uses Rabin Fingerprints for implementing this Content
Defined Chunking (CDC). An irreducible polynomial is selected at random and
saved in the file `config` when a repository is initialized, so that watermark
attacks are much harder.
Files smaller than 512 KiB are not split, Blobs are of 512 KiB to 8 MiB in
size. The implementation aims for 1 MiB Blob size on average.
For modified files, only modified Blobs have to be saved in a subsequent
backup. This even works if bytes are inserted or removed at arbitrary positions
within the file.
Threat Model
============
The design goals for restic include being able to securely store backups in a
location that is not completely trusted, e.g. a shared system where others can
potentially access the files or (in the case of the system administrator) even
modify or delete them.
General assumptions:
* The host system a backup is created on is trusted. This is the most basic
requirement, and essential for creating trustworthy backups.
The restic backup program guarantees the following:
* Accessing the unencrypted content of stored files and metadata should not
be possible without a password for the repository. Everything except the
metadata included for informational purposes in the key files is encrypted and
authenticated.
* Modifications (intentional or unintentional) can be detected automatically
on several layers:
1. For all accesses of data stored in the repository it is checked whether
the cryptographic hash of the contents matches the storage ID (the
file's name). This way, modifications (bad RAM, broken harddisk) can be
detected easily.
2. Before decrypting any data, the MAC on the encrypted data is
checked. If there has been a modification, the MAC check will
fail. This step happens even before the data is decrypted, so data that
has been tampered with is not decrypted at all.
However, the restic backup program is not designed to protect against attackers
deleting files at the storage location. There is nothing that can be done about
this. If this needs to be guaranteed, get a secure location without any access
from third parties. If you assume that attackers have write access to your
files at the storage location, attackers are able to figure out (e.g. based on
the timestamps of the stored files) which files belong to what snapshot. When
only these files are deleted, the particular snapshot vanished and all
snapshots depending on data that has been added in the snapshot cannot be
restored completely. Restic is not designed to detect this attack.

1
doc/Design.md Symbolic link
View File

@@ -0,0 +1 @@
design.rst

20
doc/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = restic
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -1,600 +0,0 @@
Thanks for using restic. This document will give you an overview of the basic
functionality provided by restic.
# Building/installing restic
If you are using Mac OS X, you can install restic using the
[homebrew](http://brew.sh/) packet manager:
$ brew tap restic/restic
$ brew install restic
On archlinux, there is a package called `restic-git` which can be installed from AUR, e.g. with `pacaur`:
$ pacaur -S restic-git
At debian stable you can install 'go' directly from the repositories (as root):
$ apt-get install golang-go
after installation of 'go' go straight forward to 'git clone [...]'
If you are using Linux, BSD or Windows, the only way to install restic on your
system right now is to compile it from source. restic is written in the Go
programming language and you need at least Go version 1.6. Building restic may
also work with older versions of Go, but that's not supported. See the [Getting
started](https://golang.org/doc/install) guide of the Go project for
instructions how to install Go.
In order to build restic from source, execute the following steps:
$ git clone https://github.com/restic/restic
[...]
$ cd restic
$ go run build.go
At the moment, the only tested compiler for restic is the official Go compiler.
Building restic with gccgo may work, but is not supported.
Usage help is available:
$ ./restic --help
Usage:
restic [OPTIONS] <command>
Application Options:
-r, --repo= Repository directory to backup to/restore from
--cache-dir= Directory to use as a local cache
-q, --quiet Do not output comprehensive progress report (false)
--no-lock Do not lock the repo, this allows some operations on read-only repos. (false)
-o, --option= Specify options in the form 'foo.key=value'
Help Options:
-h, --help Show this help message
Available commands:
backup save file/directory
cat dump something
check check the repository
find find a file/directory
forget removes snapshots from a repository
init create repository
key manage keys
list lists data
ls list files
mount mount a repository
prune removes content from a repository
rebuild-index rebuild the index
restore restore a snapshot
snapshots show snapshots
unlock remove locks
version display version
Similar to programs such as `git`, restic has a number of sub-commands. You can
see these commands in the listing above. Each sub-command may have own
command-line options, and there is a help option for each command which lists
them, e.g. for the `backup` command:
$ ./restic backup --help
Usage:
restic [OPTIONS] backup DIR/FILE [DIR/FILE] [...]
The backup command creates a snapshot of a file or directory
Application Options:
-r, --repo= Repository directory to backup to/restore from (/tmp/repo)
-p, --password-file= Read the repository password from a file
--cache-dir= Directory to use as a local cache
-q, --quiet Do not output comprehensive progress report (false)
--no-lock Do not lock the repo, this allows some operations on read-only repos. (false)
-o, --option= Specify options in the form 'foo.key=value'
Help Options:
-h, --help Show this help message
[backup command options]
-p, --parent= use this parent snapshot (default: last snapshot in repo that has the same target)
-f, --force Force re-reading the target. Overrides the "parent" flag
-e, --exclude= Exclude a pattern (can be specified multiple times)
--exclude-file= Read exclude-patterns from file
--stdin read backup data from stdin
--stdin-filename= file name to use when reading from stdin (stdin)
--tag= Add a tag (can be specified multiple times)
Subcommand that support showing progress information such as `backup`, `check` and `prune` will do so unless
the quiet flag `-q` or `--quiet` is set. When running from a non-interactive console progress reporting will
be limited to once every 10 seconds to not fill your logs.
Additionally on Unix systems if `restic` receives a SIGUSR signal the current progress will written to the
standard output so you can check up on the status at will.
# Initialize a repository
First, we need to create a "repository". This is the place where your backups
will be saved at.
In order to create a repository at `/tmp/backup`, run the following command and
enter the same password twice:
$ restic init --repo /tmp/backup
enter password for new backend:
enter password again:
created restic backend 085b3c76b9 at /tmp/backup
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
Remembering your password is important! If you lose it, you won't be able to
access data stored in the repository.
For automated backups, restic accepts the repository location in the
environment variable `RESTIC_REPOSITORY`. The password can be read from a file
(via the option `--password-file`) or the environment variable
`RESTIC_PASSWORD`.
## Password prompt on Windows
At the moment, restic only supports the default Windows console interaction.
If you use emulation environments like [MSYS2](https://msys2.github.io/) or
[Cygwin](https://www.cygwin.com/), which use terminals like `Mintty` or `rxvt`,
you may get a password error:
You can workaround this by using a special tool called `winpty` (look
[here](https://sourceforge.net/p/msys2/wiki/Porting/) and
[here](https://github.com/rprichard/winpty) for detail information). On MSYS2,
you can install `winpty` as follows:
$ pacman -S winpty
$ winpty restic -r /tmp/backup init
# Create a snapshot
Now we're ready to backup some data. The contents of a directory at a specific
point in time is called a "snapshot" in restic. Run the following command and
enter the repository password you chose above again:
$ restic -r /tmp/backup backup ~/work
enter password for repository:
scan [/home/user/work]
scanned 764 directories, 1816 files in 0:00
[0:29] 100.00% 54.732 MiB/s 1.582 GiB / 1.582 GiB 2580 / 2580 items 0 errors ETA 0:00
duration: 0:29, 54.47MiB/s
snapshot 40dc1520 saved
As you can see, restic created a backup of the directory and was pretty fast!
The specific snapshot just created is identified by a sequence of hexadecimal
characters, `40dc1520` in this case.
If you run the command again, restic will create another snapshot of your data,
but this time it's even faster. This is de-duplication at work!
$ restic -r /tmp/backup backup ~/shared/work/web
enter password for repository:
using parent snapshot 40dc1520aa6a07b7b3ae561786770a01951245d2367241e71e9485f18ae8228c
scan [/home/user/work]
scanned 764 directories, 1816 files in 0:00
[0:00] 100.00% 0B/s 1.582 GiB / 1.582 GiB 2580 / 2580 items 0 errors ETA 0:00
duration: 0:00, 6572.38MiB/s
snapshot 79766175 saved
You can even backup individual files in the same repository.
$ restic -r /tmp/backup backup ~/work.txt
scan [~/work.txt]
scanned 0 directories, 1 files in 0:00
[0:00] 100.00% 0B/s 220B / 220B 1 / 1 items 0 errors ETA 0:00
duration: 0:00, 0.03MiB/s
snapshot 31f7bd63 saved
In fact several hosts may use the same repository to backup directories and
files leading to a greater de-duplication.
You can exclude folders and files by specifying exclude-patterns.
Either specify them with multiple `--exclude`'s or one `--exclude-file`
$ cat exclude
# exclude go-files
*.go
# exclude foo/x/y/z/bar foo/x/bar foo/bar
foo/**/bar
$ restic -r /tmp/backup backup ~/work --exclude=*.c --exclude-file=exclude
Patterns use [`filepath.Glob`](https://golang.org/pkg/path/filepath/#Glob) internally,
see [`filepath.Match`](https://golang.org/pkg/path/filepath/#Match) for syntax.
Additionally `**` exludes arbitrary subdirectories.
Environment-variables in exclude-files are expanded with [`os.ExpandEnv`](https://golang.org/pkg/os/#ExpandEnv).
By specifying the option `--one-file-system` you can instruct restic to only
backup files from the file systems the initially specified files or directories
reside on. For example, calling restic like this won't backup `/sys` or
`/dev` on a Linux system:
$ restic -r /tmp/backup backup --one-file-system /
## Reading data from stdin
Sometimes it can be nice to directly save the output of a program, e.g.
`mysqldump` so that the SQL can later be restored. Restic supports this mode of
operation, just supply the option `--stdin` to the `backup` command like this:
$ mysqldump [...] | restic -r /tmp/backup backup --stdin
This creates a new snapshot of the output of `mysqldump`. You can then use e.g.
the fuse mounting option (see below) to mount the repository and read the file.
By default, the file name `stdin` is used, a different name can be specified
with `--stdin-filename`, e.g. like this:
$ mysqldump [...] | restic -r /tmp/backup backup --stdin --stdin-filename production.sql
## Tags
Snapshots can have one or more tags, short strings which add identifying
information. Just specify the tags for a snapshot with `--tag`:
$ restic -r /tmp/backup backup --tag projectX ~/shared/work/web
[...]
The tags can later be used to keep (or forget) snapshots.
# List all snapshots
Now, you can list all the snapshots stored in the repository:
$ restic -r /tmp/backup snapshots
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
40dc1520 2015-05-08 21:38:30 kasimir /home/user/work
79766175 2015-05-08 21:40:19 kasimir /home/user/work
bdbd3439 2015-05-08 21:45:17 luigi /home/art
590c8fc8 2015-05-08 21:47:38 kazik /srv
9f0bc19e 2015-05-08 21:46:11 luigi /srv
You can filter the listing by directory path:
$ restic -r /tmp/backup snapshots --path="/srv"
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
590c8fc8 2015-05-08 21:47:38 kazik /srv
9f0bc19e 2015-05-08 21:46:11 luigi /srv
Or filter by host:
$ restic -r /tmp/backup snapshots --host luigi
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
bdbd3439 2015-05-08 21:45:17 luigi /home/art
9f0bc19e 2015-05-08 21:46:11 luigi /srv
Combining filters is also possible.
# Restore a snapshot
Restoring a snapshot is as easy as it sounds, just use the following command to
restore the contents of the latest snapshot to `/tmp/restore-work`:
$ restic -r /tmp/backup restore 79766175 --target ~/tmp/restore-work
enter password for repository:
restoring <Snapshot of [/home/user/work] at 2015-05-08 21:40:19.884408621 +0200 CEST> to /tmp/restore-work
Use the word `latest` to restore the last backup. You can also combine `latest`
with the `--host` and `--path` filters to choose the last backup for a specific
host, path or both.
$ restic -r /tmp/backup restore latest --target ~/tmp/restore-work --path "/home/art" --host luigi
enter password for repository:
restoring <Snapshot of [/home/art] at 2015-05-08 21:45:17.884408621 +0200 CEST> to /tmp/restore-work
# Manage repository keys
The `key` command allows you to set multiple access keys or passwords per
repository. In fact, you can use the `list`, `add`, `remove` and `passwd`
sub-commands to manage these keys very precisely:
$ restic -r /tmp/backup key list
enter password for repository:
ID User Host Created
----------------------------------------------------------------------
*eb78040b username kasimir 2015-08-12 13:29:57
$ restic -r /tmp/backup key add
enter password for repository:
enter password for new key:
enter password again:
saved new key as <Key of username@kasimir, created on 2015-08-12 13:35:05.316831933 +0200 CEST>
$ restic -r backup key list
enter password for repository:
ID User Host Created
----------------------------------------------------------------------
5c657874 username kasimir 2015-08-12 13:35:05
*eb78040b username kasimir 2015-08-12 13:29:57
# Check integrity and consistency
Imagine your repository is saved on a server that has a faulty hard drive, or
even worse, attackers get privileged access and modify your backup with the
intention to make you restore malicious data:
$ sudo echo "boom" >> backup/index/d795ffa99a8ab8f8e42cec1f814df4e48b8f49129360fb57613df93739faee97
In order to detect these things, it is a good idea to regularly use the `check`
command to test whether everything is alright, your precious backup data is
consistent and the integrity is unharmed:
$ restic -r /tmp/backup check
Load indexes
ciphertext verification failed
Trying to restore a snapshot which has been modified as shown above will yield
the same error:
$ restic -r /tmp/backup restore 79766175 --target ~/tmp/restore-work
Load indexes
ciphertext verification failed
# Mount a repository
Browsing your backup as a regular file system is also very easy. First, create
a mount point such as `/mnt/restic` and then use the following 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
Don't forget to umount after quitting!
Mounting repositories via FUSE is not possible on Windows and OpenBSD.
# Create an SFTP repository
In order to backup data via SFTP, you must first set up a server with SSH and
let it know your public key. Passwordless login is really important since
restic fails to connect to the repository if the server prompts for
credentials.
Once the server is configured, the setup of the SFTP repository can simply be
achieved by changing the URL scheme in the `init` command:
$ restic -r sftp:user@host:/tmp/backup init
enter password for new backend:
enter password again:
created restic backend f1c6108821 at sftp:user@host:/tmp/backup
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
You can also specify a relative (read: no slash (`/`) character at the
beginning) directory, in this case the dir is relative to the remote user's
home directory.
# Create an Amazon S3 repository
Restic can backup data to any Amazon S3 bucket. However, in this case, changing the URL scheme is not enough since Amazon uses special security credentials to sign HTTP requests. By consequence, you must first setup the following environment variables with the credentials you obtained while creating the bucket.
$ export AWS_ACCESS_KEY_ID=<MY_ACCESS_KEY>
$ export AWS_SECRET_ACCESS_KEY=<MY_SECRET_ACCESS_KEY>
You can then easily initialize a repository that uses your Amazon S3 as a backend.
$ restic -r s3:eu-central-1/bucket_name init
enter password for new backend:
enter password again:
created restic backend eefee03bbd at s3:eu-central-1/bucket_name
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
Fro an s3-compatible server that is not Amazon (like Minio, see below), or is
only available via HTTP, you can specify the URL to the server like this:
`s3:http://server:port/bucket_name`.
## Create a Minio Server repository
[Minio](https://www.minio.io) is an Open Source Object Storage, written in Go and compatible with AWS S3 API.
### Pre-Requisites
* Download and Install [Minio Server](https://minio.io/download/).
* You can also refer to [https://docs.minio.io](https://docs.minio.io) for step by step guidance on installation and getting started on Minio CLient and Minio Server.
You must first setup the following environment variables with the credentials of your running Minio Server.
$ export AWS_ACCESS_KEY_ID=<YOUR-MINIO-ACCESS-KEY-ID>
$ export AWS_SECRET_ACCESS_KEY= <YOUR-MINIO-SECRET-ACCESS-KEY>
Now you can easily initialize restic to use Minio server as backend with this command.
$ ./restic -r s3:http://localhost:9000/restic init
enter password for new backend:
enter password again:
created restic backend 6ad29560f5 at s3:http://localhost:9000/restic1
Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is irrecoverably lost.
# Removing old snapshots
All backup space is finite, so restic allows removing old snapshots. This can
be done either manually (by specifying a snapshot ID to remove) or by using a
policy that describes which snapshots to forget. For all remove operations, two
commands need to be called in sequence: `forget` to remove a snapshot and
`prune` to actually remove the data that was referenced by the snapshot from
the repository.
## Remove a single snapshot
The command `snapshots` can be used to list all snapshots in a repository like this:
$ restic -r /tmp/backup snapshots
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
40dc1520 2015-05-08 21:38:30 kasimir /home/user/work
79766175 2015-05-08 21:40:19 kasimir /home/user/work
bdbd3439 2015-05-08 21:45:17 luigi /home/art
590c8fc8 2015-05-08 21:47:38 kazik /srv
9f0bc19e 2015-05-08 21:46:11 luigi /srv
In order to remove the snapshot of `/home/art`, use the `forget` command and
specify the snapshot ID on the command line:
$ restic -r /tmp/backup forget bdbd3439
enter password for repository:
removed snapshot d3f01f63
Afterwards this snapshot is removed:
$ restic -r /tmp/backup snapshots
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
40dc1520 2015-05-08 21:38:30 kasimir /home/user/work
79766175 2015-05-08 21:40:19 kasimir /home/user/work
590c8fc8 2015-05-08 21:47:38 kazik /srv
9f0bc19e 2015-05-08 21:46:11 luigi /srv
But the data that was referenced by files in this snapshot is still stored in
the repository. To cleanup unreferenced data, the `prune` command must be run:
$ restic -r /tmp/backup prune
enter password for repository:
counting files in repo
building new index for repo
[0:00] 100.00% 22 / 22 files
repository contains 22 packs (8512 blobs) with 100.092 MiB bytes
processed 8512 blobs: 0 duplicate blobs, 0B duplicate
load all snapshots
find data that is still in use for 1 snapshots
[0:00] 100.00% 1 / 1 snapshots
found 8433 of 8512 data blobs still in use
will rewrite 3 packs
creating new index
[0:00] 86.36% 19 / 22 files
saved new index as 544a5084
done
Afterwards the repository is smaller.
## Removing snapshots according to a policy
Removing snapshots manually is tedious and error-prone, therefore restic allows
specifying which snapshots should be removed automatically according to a
policy. You can specify how many hourly, daily, weekly, monthly and yearly
snapshots to keep, any other snapshots are removed. The most important
command-line parameter here is `--dry-run` which instructs restic to not remove
anything but print which snapshots would be removed.
When `forget` is run with a policy, restic loads the list of all snapshots,
then groups these by host name and list of directories. The policy is then
applied to each group of snapshots separately. This is a safety feature.
The `forget` command accepts the following parameters:
* `--keep-last n` never delete the `n` last (most recent) snapshots
* `--keep-hourly n` for the last `n` hours in which a snapshot was made, keep
only the last snapshot for each hour.
* `--keep-daily n` for the last `n` days which have one or more snapshots, only
keep the last one for that day.
* `--keep-weekly n` for the last `n` weeks which have one or more snapshots, only
keep the last one for that week.
* `--keep-monthly n` for the last `n` months which have one or more snapshots, only
keep the last one for that month.
* `--keep-yearly n` for the last `n` years which have one or more snapshots, only
keep the last one for that year.
* `--keep-tag` keep all snapshots which have all tags specified by this option
(can be specified multiple times).
Additionally, you can restrict removing snapshots to those which have a
particular hostname with the `--hostname` parameter, or tags with the `--tag`
option. When multiple tags are specified, only the snapshots which have all the
tags are considered.
All the `--keep-*` options above only count hours/days/weeks/months/years which
have a snapshot, so those without a snapshot are ignored.
Let's explain this with an example: Suppose you have only made a backup on each
Sunday for 12 weeks. Then `forget --keep-daily 4` will keep the last four snapshots
for the last four Sundays, but remove the rest. Only counting the days which
have a backup and ignore the ones without is a safety feature: it prevents
restic from removing many snapshots when no new ones are created. If it was
implemented otherwise, running `forget --keep-daily 4` on a Friday would remove
all snapshots!
# Debugging restic
The program can be built with debug support like this:
$ go run build.go -tags debug
Afterwards, extensive debug messages are written to the file in environment
variable `DEBUG_LOG`, e.g.:
$ DEBUG_LOG=/tmp/restic-debug.log restic backup ~/work
If you suspect that there is a bug, you can have a look at the debug log.
Please be aware that the debug log might contain sensitive information such as
file and directory names.
The debug log will always contain all log messages restic generates. You can
also instruct restic to print some or all debug messages to stderr. These can
also be limited to e.g. a list of source files or a list of patterns for
function names. The patterns are globbing patterns (see the documentation for
[`path.Glob`](https://golang.org/pkg/path/#Glob)), multiple patterns are
separated by commas. Patterns are case sensitive.
Printing all log messages to the console can be achieved by setting the file
filter to `*`:
$ DEBUG_FILES=* restic check
If you want restic to just print all debug log messages from the files
`main.go` and `lock.go`, set the environment variable `DEBUG_FILES` like this:
$ DEBUG_FILES=main.go,lock.go restic check
The following command line instructs restic to only print debug statements
originating in functions that match the pattern `*unlock*` (case sensitive):
$ DEBUG_FUNCS=*unlock* restic check
# Under the hood: Browse repository objects
Internally, a repository stores data of several different types described in the [design documentation](https://github.com/restic/restic/blob/master/doc/Design.md). You can `list` objects such as blobs, packs, index, snapshots, keys or locks with the following command:
```shell
$ restic -r /tmp/backup list snapshots
d369ccc7d126594950bf74f0a348d5d98d9e99f3215082eb69bf02dc9b3e464c
```
The `find` command searches for a given
[pattern](http://golang.org/pkg/path/filepath/#Match) in the repository.
$ restic -r backup find test.txt
debug log file restic.log
debug enabled
enter password for repository:
found 1 matching entries in snapshot 196bc5760c909a7681647949e80e5448e276521489558525680acf1bd428af36
-rw-r--r-- 501 20 5 2015-08-26 14:09:57 +0200 CEST path/to/test.txt
The `cat` command allows you to display the JSON representation of the objects
or its raw content.
$ restic -r /tmp/backup cat snapshot d369ccc7d126594950bf74f0a348d5d98d9e99f3215082eb69bf02dc9b3e464c
enter password for repository:
{
"time": "2015-08-12T12:52:44.091448856+02:00",
"tree": "05cec17e8d3349f402576d02576a2971fc0d9f9776ce2f441c7010849c4ff5af",
"paths": [
"/home/user/work"
],
"hostname": "kasimir",
"username": "username",
"uid": 501,
"gid": 20
}

View File

@@ -1,38 +0,0 @@
# Maintainer: Florian Daniel <fd@noxa.de>
# Contributor: Eldar Tsraev <elts@culab.org>
# Contributor: Andreas Guth <andreas.guth@rwth-aachen.de>
# Contributor: Alexander Neumann <alexander@bumpern.de>
options=(!strip)
pkgname=restic-git
pkgver=v0.1.0.r172.g1f1b8e1
pkgrel=1
pkgdesc="restic is a program that does backups right."
arch=('i686' 'x86_64')
url="https://github.com/restic/restic"
license=('BSD')
depends=('glibc')
makedepends=('git' 'go>=1.3')
provides=('restic')
conflicts=('restic')
source=("${pkgname}::git+https://github.com/restic/restic")
md5sums=('SKIP')
importpath='github.com/restic/restic'
pkgver() {
cd "$pkgname"
git describe --long | sed 's/\([^-]*-g\)/r\1/;s/-/./g'
}
build() {
cd "$pkgname"
go run build.go
}
package() {
install -Dm755 "$pkgname/restic" "$pkgdir/usr/bin/restic"
install -Dm644 "$pkgname/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -Dm644 "$pkgname/README.md" "$pkgdir/usr/share/doc/$pkgname/README"
}
# vim:set ts=2 sw=2 et:

View File

@@ -1,59 +0,0 @@
REST Backend
============
Restic can interact with HTTP Backend that respects the following REST API. The
following values are valid for `{type}`: `data`, `keys`, `locks`, `snapshots`,
`index`, `config`. `{path}` is a path to the repository, so that multiple
different repositories can be accessed. The default path is `/`.
## HEAD {path}/config
Returns "200 OK" if the repository has a configuration,
an HTTP error otherwise.
## GET {path}/config
Returns the content of the configuration file if the repository has a configuration,
an HTTP error otherwise.
Response format: binary/octet-stream
## POST {path}/config
Returns "200 OK" if the configuration of the request body has been saved,
an HTTP error otherwise.
## GET {path}/{type}/
Returns a JSON array containing the names of all the blobs stored for a given type.
Response format: JSON
## HEAD {path}/{type}/{name}
Returns "200 OK" if the blob with the given name and type is stored in the repository,
"404 not found" otherwise. If the blob exists, the HTTP header `Content-Length`
is set to the file size.
## GET {path}/{type}/{name}
Returns the content of the blob with the given name and type if it is stored in the repository,
"404 not found" otherwise.
If the request specifies a partial read with a Range header field,
then the status code of the response is 206 instead of 200
and the response only contains the specified range.
Response format: binary/octet-stream
## POST {path}/{type}/{name}
Saves the content of the request body as a blob with the given name and type,
an HTTP error otherwise.
Request format: binary/octet-stream
## DELETE {path}/{type}/{name}
Returns "200 OK" if the blob with the given name and type has been deleted from the repository,
an HTTP error otherwise.

10
doc/_static/css/restic.css vendored Normal file
View File

@@ -0,0 +1,10 @@
@import url('theme.css');
.wy-side-nav-search {
background-color: #0000b4;
}
.logo {
height: 50% !important;
width: 50% !important;
}

BIN
doc/_static/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

106
doc/conf.py Normal file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# restic documentation build configuration file, created by
# sphinx-quickstart on Fri Apr 14 22:44:43 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import os
# -- General configuration ------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'restic'
copyright = '2017, restic authors'
author = 'fd0'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
# read version from ../VERSION
version = open('../VERSION').readlines()[0]
# The full version, including alpha/beta/rc tags.
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
if os.environ.get('READTHEDOCS') == 'True':
html_context = {
'css_files': [
'https://media.readthedocs.org/css/sphinx_rtd_theme.css',
'https://media.readthedocs.org/css/readthedocs-doc-embed.css',
'_static/css/restic.css',
]
}
else:
# we're not built by rtd => add rtd-theme
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
html_style = 'css/restic.css'
html_logo = 'logo/logo.png'
html_favicon = '_static/favicon.ico'
html_show_version = False
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'resticdoc'

610
doc/design.rst Normal file
View File

@@ -0,0 +1,610 @@
Design
======
Terminology
-----------
This section introduces terminology used in this document.
*Repository*: All data produced during a backup is sent to and stored in
a repository in a structured form, for example in a file system
hierarchy with several subdirectories. A repository implementation must
be able to fulfill a number of operations, e.g. list the contents.
*Blob*: A Blob combines a number of data bytes with identifying
information like the SHA-256 hash of the data and its length.
*Pack*: A Pack combines one or more Blobs, e.g. in a single file.
*Snapshot*: A Snapshot stands for the state of a file or directory that
has been backed up at some point in time. The state here means the
content and meta data like the name and modification time for the file
or the directory and its contents.
*Storage ID*: A storage ID is the SHA-256 hash of the content stored in
the repository. This ID is required in order to load the file from the
repository.
Repository Format
-----------------
All data is stored in a restic repository. A repository is able to store
data of several different types, which can later be requested based on
an ID. This so-called "storage ID" is the SHA-256 hash of the content of
a file. All files in a repository are only written once and never
modified afterwards. This allows accessing and even writing to the
repository with multiple clients in parallel. Only the delete operation
removes data from the repository.
Repositories consist of several directories and a top-level file called
``config``. For all other files stored in the repository, the name for
the file is the lower case hexadecimal representation of the storage ID,
which is the SHA-256 hash of the file's contents. This allows for easy
verification of files for accidental modifications, like disk read
errors, by simply running the program ``sha256sum`` on the file and
comparing its output to the file name. If the prefix of a filename is
unique amongst all the other files in the same directory, the prefix may
be used instead of the complete filename.
Apart from the files stored within the ``keys`` directory, all files are
encrypted with AES-256 in counter mode (CTR). The integrity of the
encrypted data is secured by a Poly1305-AES message authentication code
(sometimes also referred to as a "signature").
In the first 16 bytes of each encrypted file the initialisation vector
(IV) is stored. It is followed by the encrypted data and completed by
the 16 byte MAC. The format is: ``IV || CIPHERTEXT || MAC``. The
complete encryption overhead is 32 bytes. For each file, a new random IV
is selected.
The file ``config`` is encrypted this way and contains a JSON document
like the following:
.. code:: json
{
"version": 1,
"id": "5956a3f67a6230d4a92cefb29529f10196c7d92582ec305fd71ff6d331d6271b",
"chunker_polynomial": "25b468838dcb75"
}
After decryption, restic first checks that the version field contains a
version number that it understands, otherwise it aborts. At the moment,
the version is expected to be 1. The field ``id`` holds a unique ID
which consists of 32 random bytes, encoded in hexadecimal. This uniquely
identifies the repository, regardless if it is accessed via SFTP or
locally. The field ``chunker_polynomial`` contains a parameter that is
used for splitting large files into smaller chunks (see below).
Repository Layout
~~~~~~~~~~~~~~~~~
The ``local`` and ``sftp`` backends are implemented using files and
directories stored in a file system. The directory layout is the same
for both backend types.
The basic layout of a repository stored in a ``local`` or ``sftp``
backend is shown here:
::
/tmp/restic-repo
├── config
├── data
│ ├── 21
│ │ └── 2159dd48f8a24f33c307b750592773f8b71ff8d11452132a7b2e2a6a01611be1
│ ├── 32
│ │ └── 32ea976bc30771cebad8285cd99120ac8786f9ffd42141d452458089985043a5
│ ├── 59
│ │ └── 59fe4bcde59bd6222eba87795e35a90d82cd2f138a27b6835032b7b58173a426
│ ├── 73
│ │ └── 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c
│ [...]
├── index
│ ├── c38f5fb68307c6a3e3aa945d556e325dc38f5fb68307c6a3e3aa945d556e325d
│ └── ca171b1b7394d90d330b265d90f506f9984043b342525f019788f97e745c71fd
├── keys
│ └── b02de829beeb3c01a63e6b25cbd421a98fef144f03b9a02e46eff9e2ca3f0bd7
├── locks
├── snapshots
│ └── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec
└── tmp
A local repository can be initialized with the ``restic init`` command,
e.g.:
.. code-block:: console
$ restic -r /tmp/restic-repo init
The local and sftp backends will auto-detect and accept all layouts described
in the following sections, so that remote repositories mounted locally e.g. via
fuse can be accessed. The layout auto-detection can be overridden by specifying
the option ``-o local.layout=default``, valid values are ``default`` and
``s3legacy``. The option for the sftp backend is named ``sftp.layout``, for the
s3 backend ``s3.layout``.
S3 Legacy Layout
~~~~~~~~~~~~~~~~
Unfortunately during development the AWS S3 backend uses slightly different
paths (directory names use singular instead of plural for ``key``,
``lock``, and ``snapshot`` files), and the data files are stored directly below
the ``data`` directory. The S3 Legacy repository layout looks like this:
::
/config
/data
├── 2159dd48f8a24f33c307b750592773f8b71ff8d11452132a7b2e2a6a01611be1
├── 32ea976bc30771cebad8285cd99120ac8786f9ffd42141d452458089985043a5
├── 59fe4bcde59bd6222eba87795e35a90d82cd2f138a27b6835032b7b58173a426
├── 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c
[...]
/index
├── c38f5fb68307c6a3e3aa945d556e325dc38f5fb68307c6a3e3aa945d556e325d
└── ca171b1b7394d90d330b265d90f506f9984043b342525f019788f97e745c71fd
/key
└── b02de829beeb3c01a63e6b25cbd421a98fef144f03b9a02e46eff9e2ca3f0bd7
/lock
/snapshot
└── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec
The S3 backend understands and accepts both forms, new backends are
always created with the default layout for compatibility reasons.
Pack Format
-----------
All files in the repository except Key and Pack files just contain raw
data, stored as ``IV || Ciphertext || MAC``. Pack files may contain one
or more Blobs of data.
A Pack's structure is as follows:
::
EncryptedBlob1 || ... || EncryptedBlobN || EncryptedHeader || Header_Length
At the end of the Pack file is a header, which describes the content.
The header is encrypted and authenticated. ``Header_Length`` is the
length of the encrypted header encoded as a four byte integer in
little-endian encoding. Placing the header at the end of a file allows
writing the blobs in a continuous stream as soon as they are read during
the backup phase. This reduces code complexity and avoids having to
re-write a file once the pack is complete and the content and length of
the header is known.
All the blobs (``EncryptedBlob1``, ``EncryptedBlobN`` etc.) are
authenticated and encrypted independently. This enables repository
reorganisation without having to touch the encrypted Blobs. In addition
it also allows efficient indexing, for only the header needs to be read
in order to find out which Blobs are contained in the Pack. Since the
header is authenticated, authenticity of the header can be checked
without having to read the complete Pack.
After decryption, a Pack's header consists of the following elements:
::
Type_Blob1 || Length(EncryptedBlob1) || Hash(Plaintext_Blob1) ||
[...]
Type_BlobN || Length(EncryptedBlobN) || Hash(Plaintext_Blobn) ||
This is enough to calculate the offsets for all the Blobs in the Pack.
Length is the length of a Blob as a four byte integer in little-endian
format. The type field is a one byte field and labels the content of a
blob according to the following table:
+--------+-----------+
| Type | Meaning |
+========+===========+
| 0 | data |
+--------+-----------+
| 1 | tree |
+--------+-----------+
All other types are invalid, more types may be added in the future.
For reconstructing the index or parsing a pack without an index, first
the last four bytes must be read in order to find the length of the
header. Afterwards, the header can be read and parsed, which yields all
plaintext hashes, types, offsets and lengths of all included blobs.
Indexing
--------
Index files contain information about Data and Tree Blobs and the Packs
they are contained in and store this information in the repository. When
the local cached index is not accessible any more, the index files can
be downloaded and used to reconstruct the index. The files are encrypted
and authenticated like Data and Tree Blobs, so the outer structure is
``IV || Ciphertext || MAC`` again. The plaintext consists of a JSON
document like the following:
.. code:: json
{
"supersedes": [
"ed54ae36197f4745ebc4b54d10e0f623eaaaedd03013eb7ae90df881b7781452"
],
"packs": [
{
"id": "73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c",
"blobs": [
{
"id": "3ec79977ef0cf5de7b08cd12b874cd0f62bbaf7f07f3497a5b1bbcc8cb39b1ce",
"type": "data",
"offset": 0,
"length": 25
},{
"id": "9ccb846e60d90d4eb915848add7aa7ea1e4bbabfc60e573db9f7bfb2789afbae",
"type": "tree",
"offset": 38,
"length": 100
},
{
"id": "d3dc577b4ffd38cc4b32122cabf8655a0223ed22edfd93b353dc0c3f2b0fdf66",
"type": "data",
"offset": 150,
"length": 123
}
]
}, [...]
]
}
This JSON document lists Packs and the blobs contained therein. In this
example, the Pack ``73d04e61`` contains two data Blobs and one Tree
blob, the plaintext hashes are listed afterwards.
The field ``supersedes`` lists the storage IDs of index files that have
been replaced with the current index file. This happens when index files
are repacked, for example when old snapshots are removed and Packs are
recombined.
There may be an arbitrary number of index files, containing information
on non-disjoint sets of Packs. The number of packs described in a single
file is chosen so that the file size is kept below 8 MiB.
Keys, Encryption and MAC
------------------------
All data stored by restic in the repository is encrypted with AES-256 in
counter mode and authenticated using Poly1305-AES. For encrypting new
data first 16 bytes are read from a cryptographically secure
pseudorandom number generator as a random nonce. This is used both as
the IV for counter mode and the nonce for Poly1305. This operation needs
three keys: A 32 byte for AES-256 for encryption, a 16 byte AES key and
a 16 byte key for Poly1305. For details see the original paper `The
Poly1305-AES message-authentication
code <http://cr.yp.to/mac/poly1305-20050329.pdf>`__ by Dan Bernstein.
The data is then encrypted with AES-256 and afterwards a message
authentication code (MAC) is computed over the ciphertext, everything is
then stored as IV \|\| CIPHERTEXT \|\| MAC.
The directory ``keys`` contains key files. These are simple JSON
documents which contain all data that is needed to derive the
repository's master encryption and message authentication keys from a
user's password. The JSON document from the repository can be
pretty-printed for example by using the Python module ``json``
(shortened to increase readability):
::
$ python -mjson.tool /tmp/restic-repo/keys/b02de82*
{
"hostname": "kasimir",
"username": "fd0"
"kdf": "scrypt",
"N": 65536,
"r": 8,
"p": 1,
"created": "2015-01-02T18:10:13.48307196+01:00",
"data": "tGwYeKoM0C4j4/9DFrVEmMGAldvEn/+iKC3te/QE/6ox/V4qz58FUOgMa0Bb1cIJ6asrypCx/Ti/pRXCPHLDkIJbNYd2ybC+fLhFIJVLCvkMS+trdywsUkglUbTbi+7+Ldsul5jpAj9vTZ25ajDc+4FKtWEcCWL5ICAOoTAxnPgT+Lh8ByGQBH6KbdWabqamLzTRWxePFoYuxa7yXgmj9A==",
"salt": "uW4fEI1+IOzj7ED9mVor+yTSJFd68DGlGOeLgJELYsTU5ikhG/83/+jGd4KKAaQdSrsfzrdOhAMftTSih5Ux6w==",
}
When the repository is opened by restic, the user is prompted for the
repository password. This is then used with ``scrypt``, a key derivation
function (KDF), and the supplied parameters (``N``, ``r``, ``p`` and
``salt``) to derive 64 key bytes. The first 32 bytes are used as the
encryption key (for AES-256) and the last 32 bytes are used as the
message authentication key (for Poly1305-AES). These last 32 bytes are
divided into a 16 byte AES key ``k`` followed by 16 bytes of secret key
``r``. The key ``r`` is then masked for use with Poly1305 (see the paper
for details).
Those message authentication keys (``k`` and ``r``) are used to compute
a MAC over the bytes contained in the JSON field ``data`` (after
removing the Base64 encoding and not including the last 32 byte). If the
password is incorrect or the key file has been tampered with, the
computed MAC will not match the last 16 bytes of the data, and restic
exits with an error. Otherwise, the data is decrypted with the
encryption key derived from ``scrypt``. This yields a JSON document
which contains the master encryption and message authentication keys for
this repository (encoded in Base64). The command
``restic cat masterkey`` can be used as follows to decrypt and
pretty-print the master key:
.. code-block:: console
$ restic -r /tmp/restic-repo cat masterkey
{
"mac": {
"k": "evFWd9wWlndL9jc501268g==",
"r": "E9eEDnSJZgqwTOkDtOp+Dw=="
},
"encrypt": "UQCqa0lKZ94PygPxMRqkePTZnHRYh1k1pX2k2lM2v3Q=",
}
All data in the repository is encrypted and authenticated with these
master keys. For encryption, the AES-256 algorithm in Counter mode is
used. For message authentication, Poly1305-AES is used as described
above.
A repository can have several different passwords, with a key file for
each. This way, the password can be changed without having to re-encrypt
all data.
Snapshots
---------
A snapshot represents a directory with all files and sub-directories at
a given point in time. For each backup that is made, a new snapshot is
created. A snapshot is a JSON document that is stored in an encrypted
file below the directory ``snapshots`` in the repository. The filename
is the storage ID. This string is unique and used within restic to
uniquely identify a snapshot.
The command ``restic cat snapshot`` can be used as follows to decrypt
and pretty-print the contents of a snapshot file:
.. code-block:: console
$ restic -r /tmp/restic-repo cat snapshot 251c2e58
enter password for repository:
{
"time": "2015-01-02T18:10:50.895208559+01:00",
"tree": "2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf",
"dir": "/tmp/testdata",
"hostname": "kasimir",
"username": "fd0",
"uid": 1000,
"gid": 100,
"tags": [
"NL"
]
}
Here it can be seen that this snapshot represents the contents of the
directory ``/tmp/testdata``. The most important field is ``tree``. When
the meta data (e.g. the tags) of a snapshot change, the snapshot needs
to be re-encrypted and saved. This will change the storage ID, so in
order to relate these seemingly different snapshots, a field
``original`` is introduced which contains the ID of the original
snapshot, e.g. after adding the tag ``DE`` to the snapshot above it
becomes:
.. code-block:: console
$ restic -r /tmp/restic-repo cat snapshot 22a5af1b
enter password for repository:
{
"time": "2015-01-02T18:10:50.895208559+01:00",
"tree": "2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf",
"dir": "/tmp/testdata",
"hostname": "kasimir",
"username": "fd0",
"uid": 1000,
"gid": 100,
"tags": [
"NL",
"DE"
],
"original": "251c2e5841355f743f9d4ffd3260bee765acee40a6229857e32b60446991b837"
}
Once introduced, the ``original`` field is not modified when the
snapshot's meta data is changed again.
All content within a restic repository is referenced according to its
SHA-256 hash. Before saving, each file is split into variable sized
Blobs of data. The SHA-256 hashes of all Blobs are saved in an ordered
list which then represents the content of the file.
In order to relate these plaintext hashes to the actual location within
a Pack file , an index is used. If the index is not available, the
header of all data Blobs can be read.
Trees and Data
--------------
A snapshot references a tree by the SHA-256 hash of the JSON string
representation of its contents. Trees and data are saved in pack files
in a subdirectory of the directory ``data``.
The command ``restic cat blob`` can be used to inspect the tree
referenced above (piping the output of the command to ``jq .`` so that
the JSON is indented):
.. code-block:: console
$ restic -r /tmp/restic-repo cat blob 2da81727b6585232894cfbb8f8bdab8d1eccd3d8f7c92bc934d62e62e618ffdf | jq .
enter password for repository:
{
"nodes": [
{
"name": "testdata",
"type": "dir",
"mode": 493,
"mtime": "2014-12-22T14:47:59.912418701+01:00",
"atime": "2014-12-06T17:49:21.748468803+01:00",
"ctime": "2014-12-22T14:47:59.912418701+01:00",
"uid": 1000,
"gid": 100,
"user": "fd0",
"inode": 409704562,
"content": null,
"subtree": "b26e315b0988ddcd1cee64c351d13a100fedbc9fdbb144a67d1b765ab280b4dc"
}
]
}
A tree contains a list of entries (in the field ``nodes``) which contain
meta data like a name and timestamps. When the entry references a
directory, the field ``subtree`` contains the plain text ID of another
tree object.
When the command ``restic cat blob`` is used, the plaintext ID is needed
to print a tree. The tree referenced above can be dumped as follows:
.. code-block:: console
$ restic -r /tmp/restic-repo cat blob b26e315b0988ddcd1cee64c351d13a100fedbc9fdbb144a67d1b765ab280b4dc
enter password for repository:
{
"nodes": [
{
"name": "testfile",
"type": "file",
"mode": 420,
"mtime": "2014-12-06T17:50:23.34513538+01:00",
"atime": "2014-12-06T17:50:23.338468713+01:00",
"ctime": "2014-12-06T17:50:23.34513538+01:00",
"uid": 1000,
"gid": 100,
"user": "fd0",
"inode": 416863351,
"size": 1234,
"links": 1,
"content": [
"50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d"
]
},
[...]
]
}
This tree contains a file entry. This time, the ``subtree`` field is not
present and the ``content`` field contains a list with one plain text
SHA-256 hash.
The command ``restic cat blob`` can also be used to extract and decrypt
data given a plaintext ID, e.g. for the data mentioned above:
.. code-block:: console
$ restic -r /tmp/restic-repo cat blob 50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d | sha256sum
enter password for repository:
50f77b3b4291e8411a027b9f9b9e64658181cc676ce6ba9958b95f268cb1109d -
As can be seen from the output of the program ``sha256sum``, the hash
matches the plaintext hash from the map included in the tree above, so
the correct data has been returned.
Locks
-----
The restic repository structure is designed in a way that allows
parallel access of multiple instance of restic and even parallel writes.
However, there are some functions that work more efficient or even
require exclusive access of the repository. In order to implement these
functions, restic processes are required to create a lock on the
repository before doing anything.
Locks come in two types: Exclusive and non-exclusive locks. At most one
process can have an exclusive lock on the repository, and during that
time there must not be any other locks (exclusive and non-exclusive).
There may be multiple non-exclusive locks in parallel.
A lock is a file in the subdir ``locks`` whose filename is the storage
ID of the contents. It is encrypted and authenticated the same way as
other files in the repository and contains the following JSON structure:
.. code:: json
{
"time": "2015-06-27T12:18:51.759239612+02:00",
"exclusive": false,
"hostname": "kasimir",
"username": "fd0",
"pid": 13607,
"uid": 1000,
"gid": 100
}
The field ``exclusive`` defines the type of lock. When a new lock is to
be created, restic checks all locks in the repository. When a lock is
found, it is tested if the lock is stale, which is the case for locks
with timestamps older than 30 minutes. If the lock was created on the
same machine, even for younger locks it is tested whether the process is
still alive by sending a signal to it. If that fails, restic assumes
that the process is dead and considers the lock to be stale.
When a new lock is to be created and no other conflicting locks are
detected, restic creates a new lock, waits, and checks if other locks
appeared in the repository. Depending on the type of the other locks and
the lock to be created, restic either continues or fails.
Backups and Deduplication
-------------------------
For creating a backup, restic scans the source directory for all files,
sub-directories and other entries. The data from each file is split into
variable length Blobs cut at offsets defined by a sliding window of 64
byte. The implementation uses Rabin Fingerprints for implementing this
Content Defined Chunking (CDC). An irreducible polynomial is selected at
random and saved in the file ``config`` when a repository is
initialized, so that watermark attacks are much harder.
Files smaller than 512 KiB are not split, Blobs are of 512 KiB to 8 MiB
in size. The implementation aims for 1 MiB Blob size on average.
For modified files, only modified Blobs have to be saved in a subsequent
backup. This even works if bytes are inserted or removed at arbitrary
positions within the file.
Threat Model
------------
The design goals for restic include being able to securely store backups
in a location that is not completely trusted, e.g. a shared system where
others can potentially access the files or (in the case of the system
administrator) even modify or delete them.
General assumptions:
- The host system a backup is created on is trusted. This is the most
basic requirement, and essential for creating trustworthy backups.
The restic backup program guarantees the following:
- Accessing the unencrypted content of stored files and metadata should
not be possible without a password for the repository. Everything
except the metadata included for informational purposes in the key
files is encrypted and authenticated.
- Modifications (intentional or unintentional) can be detected
automatically on several layers:
1. For all accesses of data stored in the repository it is checked
whether the cryptographic hash of the contents matches the storage
ID (the file's name). This way, modifications (bad RAM, broken
harddisk) can be detected easily.
2. Before decrypting any data, the MAC on the encrypted data is
checked. If there has been a modification, the MAC check will
fail. This step happens even before the data is decrypted, so data
that has been tampered with is not decrypted at all.
However, the restic backup program is not designed to protect against
attackers deleting files at the storage location. There is nothing that
can be done about this. If this needs to be guaranteed, get a secure
location without any access from third parties. If you assume that
attackers have write access to your files at the storage location,
attackers are able to figure out (e.g. based on the timestamps of the
stored files) which files belong to what snapshot. When only these files
are deleted, the particular snapshot vanished and all snapshots
depending on data that has been added in the snapshot cannot be restored
completely. Restic is not designed to detect this attack.

69
doc/development.rst Normal file
View File

@@ -0,0 +1,69 @@
Development
===========
Contribute
----------
Contributions are welcome! Please **open an issue first** (or add a
comment to an existing issue) if you plan to work on any code or add a
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>`__.
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>`__.
If you'd like to start contributing to restic, but don't know exactly
what do to, have a look at this great article by Dave Cheney:
`Suggestions for contributing to an Open Source
project <http://dave.cheney.net/2016/03/12/suggestions-for-contributing-to-an-open-source-project>`__
A few issues have been tagged with the label ``help wanted``, you can
start looking at those:
https://github.com/restic/restic/labels/help%20wanted
Security
--------
**Important**: If you discover something that you believe to be a
possible critical security problem, please do *not* open a GitHub issue
but send an email directly to alexander@bumpern.de. If possible, please
encrypt your email using the following PGP key
(`0x91A6868BD3F7A907 <https://pgp.mit.edu/pks/lookup?op=get&search=0xCF8F18F2844575973F79D4E191A6868BD3F7A907>`__):
::
pub 4096R/91A6868BD3F7A907 2014-11-01
Key fingerprint = CF8F 18F2 8445 7597 3F79 D4E1 91A6 868B D3F7 A907
uid Alexander Neumann <alexander@bumpern.de>
sub 4096R/D5FC2ACF4043FDF1 2014-11-01
Compatibility
-------------
Backward compatibility for backups is important so that our users are
always able to restore saved data. Therefore restic follows `Semantic
Versioning <http://semver.org>`__ to clearly define which versions are
compatible. The repository and data structures contained therein are
considered the "Public API" in the sense of Semantic Versioning. This
goes for all released versions of restic, this may not be the case for
the master branch.
We guarantee backward compatibility of all repositories within one major
version; as long as we do not increment the major version, data can be
read and restored. We strive to be fully backward compatible to all
prior versions.
Building documentation
----------------------
The restic documentation is built with `Sphinx <http://sphinx-doc.org>`__,
therefore building it locally requires a recent Python version and requirements listed in ``doc/requirements.txt``.
This example will guide you through the process using `virtualenv <https://virtualenv.pypa.io>`__:
::
$ virtualenv venv # create virtual python environment
$ source venv/bin/activate # activate the virtual environment
$ cd doc
$ pip install -r requirements.txt # install dependencies
$ make html # build html documentation
$ # open _build/html/index.html with your favorite browser

28
doc/faq.rst Normal file
View File

@@ -0,0 +1,28 @@
FAQ
===
This is the list of Frequently Asked Questions for restic.
``restic check`` reports packs that aren't referenced in any index, is my repository broken?
--------------------------------------------------------------------------------------------
When ``restic check`` reports that there are pack files in the
repository that are not referenced in any index, that's (in contrast to
what restic reports at the moment) not a source for concern. The output
looks like this:
::
$ restic check
Create exclusive lock for repository
Load indexes
Check all packs
pack 819a9a52e4f51230afa89aefbf90df37fb70996337ae57e6f7a822959206a85e: not referenced in any index
pack de299e69fb075354a3775b6b045d152387201f1cdc229c31d1caa34c3b340141: not referenced in any index
Check snapshots, trees and blobs
Fatal: repository contains errors
The message means that there is more data stored in the repo than
strictly necessary. With high probability this is duplicate data. In
order to clean it up, the command ``restic prune`` can be used. The
cause of this bug is not yet known.

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -1,117 +0,0 @@
Welcome to restic
=================
![restic logo](logo/logo.svg)
restic is a backup program that is fast, efficient and secure. On the left you
can find an overview of the documentation. The project's homepage is
<https://restic.github.io>, the source code repository can be found on GitHub
at the URL <https://github.com/restic/restic>.
Building and viewing the documentation
--------------------------------------
The documentation you're currently viewing may not match the version of restic
you have installed. If you cloned the repository manually, you can find the
right documentation in the directory `doc/`. If you're viewing this online at
<https://restic.readthedocs.io>, there is a small menu at the bottom left of
this page, where you can select the version.
The restic documentation is built with [MkDocs](http://www.mkdocs.org). After
installing it, you can edit and view the documentation locally by running:
$ mkdocs serve
INFO - Building documentation...
INFO - Cleaning site directory
[I 160221 12:33:57 server:271] Serving on http://127.0.0.1:8000
Afterwards visit the URL with a browser.
Design Principles
-----------------
Restic is a program that does backups right and was designed with the following
principles in mind:
* **Easy:** Doing backups should be a frictionless process, otherwise you might be
tempted to skip it. Restic should be easy to configure and use, so that, in
the event of a data loss, you can just restore it. Likewise,
restoring data should not be complicated.
* **Fast**: Backing up your data with restic should only be limited by your
network or hard disk bandwidth so that you can backup your files every day.
Nobody does backups if it takes too much time. Restoring backups should only
transfer data that is needed for the files that are to be restored, so that
this process is also fast.
* **Verifiable**: Much more important than backup is restore, so restic enables
you to easily verify that all data can be restored.
* **Secure**: Restic uses cryptography to guarantee confidentiality and integrity
of your data. The location the backup data is stored is assumed not to be a
trusted environment (e.g. a shared space where others like system
administrators are able to access your backups). Restic is built to secure
your data against such attackers.
* **Efficient**: With the growth of data, additional snapshots should only take
the storage of the actual increment. Even more, duplicate data should be
de-duplicated before it is actually written to the storage back end to save
precious backup space.
Compatibility
-------------
Backward compatibility for backups is important so that our users are always
able to restore saved data. Therefore restic follows [Semantic
Versioning](http://semver.org) to clearly define which versions are compatible.
The repository and data structures contained therein are considered the "Public
API" in the sense of Semantic Versioning. This goes for all released versions
of restic, this may not be the case for the master branch.
We guarantee backward compatibility of all repositories within one major version;
as long as we do not increment the major version, data can be read and restored.
We strive to be fully backward compatible to all prior versions.
Contribute and Documentation
----------------------------
Contributions are welcome! More information can be found in the document
[`CONTRIBUTING.md`](https://github.com/restic/restic/blob/master/CONTRIBUTING.md).
Contact
-------
If you discover a bug, find something surprising or if you would like to
discuss or ask something, please [open a github
issue](https://github.com/restic/restic/issues/new). If you would like to chat
about restic, there is also the IRC channel #restic on irc.freenode.net.
**Important**: If you discover something that you believe to be a possible
critical security problem, please do *not* open a GitHub issue but send an
email directly to alexander@bumpern.de. If possible, please encrypt your email
using the following PGP key
([0x91A6868BD3F7A907](https://pgp.mit.edu/pks/lookup?op=get&search=0xCF8F18F2844575973F79D4E191A6868BD3F7A907)):
```
pub 4096R/91A6868BD3F7A907 2014-11-01
Key fingerprint = CF8F 18F2 8445 7597 3F79 D4E1 91A6 868B D3F7 A907
uid Alexander Neumann <alexander@bumpern.de>
sub 4096R/D5FC2ACF4043FDF1 2014-11-01
```
Talks
-----
The following talks will be or have been given about restic:
* 2016-01-31: Lightning Talk at the Go Devroom at FOSDEM 2016, Brussels, Belgium
* 2016-01-29: [restic - Backups mal richtig](https://media.ccc.de/v/c4.openchaos.2016.01.restic): Public lecture in German at [CCC Cologne e.V.](https://koeln.ccc.de) in Cologne, Germany
* 2015-08-23: [A Solution to the Backup Inconvenience](https://programm.froscon.de/2015/events/1515.html): Lecture at [FROSCON 2015](https://www.froscon.de) in Bonn, Germany
* 2015-02-01: [Lightning Talk at FOSDEM 2015](https://www.youtube.com/watch?v=oM-MfeflUZ8&t=11m40s): A short introduction (with slightly outdated command line)
* 2015-01-27: [Talk about restic at CCC Aachen](https://videoag.fsmpi.rwth-aachen.de/?view=player&lectureid=4442#content) (in German)
License
=======
Restic is licensed under "BSD 2-Clause License". You can find the complete text
in the file `LICENSE`.

15
doc/index.rst Normal file
View File

@@ -0,0 +1,15 @@
Restic Documentation
====================
.. toctree::
:maxdepth: 2
installation
manual
faq
tutorials
development
references
talks
.. include:: ../README.rst

68
doc/installation.rst Normal file
View File

@@ -0,0 +1,68 @@
Installation
============
Packages
--------
Mac OS X
~~~~~~~~~
If you are using Mac OS X, you can install restic using the
`homebrew <http://brew.sh/>`__ packet manager:
.. code-block:: console
$ brew tap restic/restic
$ brew install restic
archlinux
~~~~~~~~~
On archlinux, there is a package called ``restic-git`` which can be
installed from AUR, e.g. with ``pacaur``:
.. code-block:: console
$ pacaur -S restic-git
Pre-compiled Binary
-------------------
You can download the latest pre-compiled binary from the `restic release
page <https://github.com/restic/restic/releases/latest>`__.
From Source
-----------
restic is written in the Go programming language and you need at least
Go version 1.7. Building restic may also work with older versions of Go,
but that's not supported. See the `Getting
started <https://golang.org/doc/install>`__ guide of the Go project for
instructions how to install Go.
In order to build restic from source, execute the following steps:
.. code-block:: console
$ git clone https://github.com/restic/restic
[...]
$ cd restic
$ go run build.go
You can easily cross-compile restic for all supported platforms, just
supply the target OS and platform via the command-line options like this
(for Windows and FreeBSD respectively):
::
$ go run build.go --goos windows --goarch amd64
$ go run build.go --goos freebsd --goarch 386
The resulting binary is statically linked and does not require any
libraries.
At the moment, the only tested compiler for restic is the official Go
compiler. Building restic with gccgo may work, but is not supported.

989
doc/manual.rst Normal file
View File

@@ -0,0 +1,989 @@
Manual
======
Usage help
----------
Usage help is available:
.. code-block:: console
$ ./restic --help
restic is a backup program which allows saving multiple revisions of files and
directories in an encrypted repository stored on different backends.
Usage:
restic [command]
Available Commands:
autocomplete generate shell autocompletion script
backup create a new backup of files and/or directories
cat print internal objects to stdout
check check the repository for errors
find find a file or directory
forget forget removes snapshots from the repository
init initialize a new repository
key manage keys (passwords)
list list items in the repository
ls list files in a snapshot
mount mount the repository
prune remove unneeded data from the repository
rebuild-index build a new index file
restore extract the data from a snapshot
snapshots list all snapshots
tag modifies tags on snapshots
unlock remove locks other processes created
version Print version information
Flags:
--json set output mode to JSON for commands that support it
--no-lock do not lock the repo, this allows some operations on read-only repos
-p, --password-file string read the repository password from a file
-q, --quiet do not output comprehensive progress report
-r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY)
Use "restic [command] --help" for more information about a command.
Similar to programs such as ``git``, restic has a number of
sub-commands. You can see these commands in the listing above. Each
sub-command may have own command-line options, and there is a help
option for each command which lists them, e.g. for the ``backup``
command:
.. code-block:: console
$ ./restic backup --help
The "backup" command creates a new snapshot and saves the files and directories
given as the arguments.
Usage:
restic backup [flags] FILE/DIR [FILE/DIR] ...
Flags:
-e, --exclude pattern exclude a pattern (can be specified multiple times)
--exclude-file string read exclude patterns from a file
--files-from string read the files to backup from file (can be combined with file args)
-f, --force force re-reading the target files/directories. Overrides the "parent" flag
-x, --one-file-system Exclude other file systems
--parent string use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)
--stdin read backup from stdin
--stdin-filename string file name to use when reading from stdin
--tag tag add a tag for the new snapshot (can be specified multiple times)
Global Flags:
--json set output mode to JSON for commands that support it
--no-lock do not lock the repo, this allows some operations on read-only repos
-p, --password-file string read the repository password from a file
-q, --quiet do not output comprehensive progress report
-r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY)
Subcommand that support showing progress information such as ``backup``,
``check`` and ``prune`` will do so unless the quiet flag ``-q`` or
``--quiet`` is set. When running from a non-interactive console progress
reporting will be limited to once every 10 seconds to not fill your
logs.
Additionally on Unix systems if ``restic`` receives a SIGUSR signal the
current progress will written to the standard output so you can check up
on the status at will.
Initialize a repository
-----------------------
First, we need to create a "repository". This is the place where your
backups will be saved at.
Local
~~~~~
In order to create a repository at ``/tmp/backup``, run the following
command and enter the same password twice:
.. code-block:: console
$ restic init --repo /tmp/backup
enter password for new backend:
enter password again:
created restic backend 085b3c76b9 at /tmp/backup
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
Other backends like sftp and s3 are `described in a later
section <#create-an-sftp-repository>`__ of this document.
Remembering your password is important! If you lose it, you won't be
able to access data stored in the repository.
For automated backups, restic accepts the repository location in the
environment variable ``RESTIC_REPOSITORY``. The password can be read
from a file (via the option ``--password-file``) or the environment
variable ``RESTIC_PASSWORD``.
SFTP
~~~~
In order to backup data via SFTP, you must first set up a server with
SSH and let it know your public key. Passwordless login is really
important since restic fails to connect to the repository if the server
prompts for credentials.
Once the server is configured, the setup of the SFTP repository can
simply be achieved by changing the URL scheme in the ``init`` command:
.. code-block:: console
$ restic -r sftp:user@host:/tmp/backup init
enter password for new backend:
enter password again:
created restic backend f1c6108821 at sftp:user@host:/tmp/backup
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
You can also specify a relative (read: no slash (``/``) character at the
beginning) directory, in this case the dir is relative to the remote
user's home directory.
The backend config string does not allow specifying a port. If you need
to contact an sftp server on a different port, you can create an entry
in the ``ssh`` file, usually located in your user's home directory at
``~/.ssh/config`` or in ``/etc/ssh/ssh_config``:
::
Host foo
User bar
Port 2222
Then use the specified host name ``foo`` normally (you don't need to
specify the user name in this case):
::
$ restic -r sftp:foo:/tmp/backup init
You can also add an entry with a special host name which does not exist,
just for use with restic, and use the ``Hostname`` option to set the
real host name:
::
Host restic-backup-host
Hostname foo
User bar
Port 2222
Then use it in the backend specification:
::
$ restic -r sftp:restic-backup-host:/tmp/backup init
Last, if you'd like to use an entirely different program to create the
SFTP connection, you can specify the command to be run with the option
``-o sftp.command="foobar"``.
REST Server
~~~~~~~~~~~
In order to backup data to the remote server via HTTP or HTTPS protocol,
you must first set up a remote `REST
server <https://github.com/restic/rest-server>`__ instance. Once the
server is configured, accessing it is achieved by changing the URL
scheme like this:
.. code-block:: console
$ restic -r rest:http://host:8000/
Depending on your REST server setup, you can use HTTPS protocol,
password protection, or multiple repositories. Or any combination of
those features, as you see fit. TCP/IP port is also configurable. Here
are some more examples:
.. code-block:: console
$ restic -r rest:https://host:8000/
$ restic -r rest:https://user:pass@host:8000/
$ restic -r rest:https://user:pass@host:8000/my_backup_repo/
If you use TLS, make sure your certificates are signed, 'cause restic
client will refuse to communicate otherwise. It's easy to obtain such
certificates today, thanks to free certificate authorities like `Lets
Encrypt <https://letsencrypt.org/>`__.
REST server uses exactly the same directory structure as local backend,
so you should be able to access it both locally and via HTTP, even
simultaneously.
Amazon S3
~~~~~~~~~
Restic can backup data to any Amazon S3 bucket. However, in this case,
changing the URL scheme is not enough since Amazon uses special security
credentials to sign HTTP requests. By consequence, you must first setup
the following environment variables with the credentials you obtained
while creating the bucket.
.. code-block:: console
$ export AWS_ACCESS_KEY_ID=<MY_ACCESS_KEY>
$ export AWS_SECRET_ACCESS_KEY=<MY_SECRET_ACCESS_KEY>
You can then easily initialize a repository that uses your Amazon S3 as
a backend, if the bucket does not exist yet it will be created in the
default location:
.. code-block:: console
$ restic -r s3:s3.amazonaws.com/bucket_name init
enter password for new backend:
enter password again:
created restic backend eefee03bbd at s3:s3.amazonaws.com/bucket_name
Please note that knowledge of your password is required to access the repository.
Losing your password means that your data is irrecoverably lost.
It is not possible at the moment to have restic create a new bucket in a
different location, so you need to create it using a different program.
Afterwards, the S3 server (``s3.amazonaws.com``) will redirect restic to
the correct endpoint.
For an S3-compatible server that is not Amazon (like Minio, see below),
or is only available via HTTP, you can specify the URL to the server
like this: ``s3:http://server:port/bucket_name``.
Minio Server
~~~~~~~~~~~~
`Minio <https://www.minio.io>`__ is an Open Source Object Storage,
written in Go and compatible with AWS S3 API.
- Download and Install `Minio
Server <https://minio.io/downloads/#minio-server>`__.
- You can also refer to https://docs.minio.io for step by step guidance
on installation and getting started on Minio Client and Minio Server.
You must first setup the following environment variables with the
credentials of your running Minio Server.
.. code-block:: console
$ export AWS_ACCESS_KEY_ID=<YOUR-MINIO-ACCESS-KEY-ID>
$ export AWS_SECRET_ACCESS_KEY= <YOUR-MINIO-SECRET-ACCESS-KEY>
Now you can easily initialize restic to use Minio server as backend with
this command.
.. code-block:: console
$ ./restic -r s3:http://localhost:9000/restic init
enter password for new backend:
enter password again:
created restic backend 6ad29560f5 at s3:http://localhost:9000/restic1
Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is irrecoverably lost.
Password prompt on Windows
~~~~~~~~~~~~~~~~~~~~~~~~~~
At the moment, restic only supports the default Windows console
interaction. If you use emulation environments like
`MSYS2 <https://msys2.github.io/>`__ or
`Cygwin <https://www.cygwin.com/>`__, which use terminals like
``Mintty`` or ``rxvt``, you may get a password error:
You can workaround this by using a special tool called ``winpty`` (look
`here <https://sourceforge.net/p/msys2/wiki/Porting/>`__ and
`here <https://github.com/rprichard/winpty>`__ for detail information).
On MSYS2, you can install ``winpty`` as follows:
.. code-block:: console
$ pacman -S winpty
$ winpty restic -r /tmp/backup init
Create a snapshot
-----------------
Now we're ready to backup some data. The contents of a directory at a
specific point in time is called a "snapshot" in restic. Run the
following command and enter the repository password you chose above
again:
.. code-block:: console
$ restic -r /tmp/backup backup ~/work
enter password for repository:
scan [/home/user/work]
scanned 764 directories, 1816 files in 0:00
[0:29] 100.00% 54.732 MiB/s 1.582 GiB / 1.582 GiB 2580 / 2580 items 0 errors ETA 0:00
duration: 0:29, 54.47MiB/s
snapshot 40dc1520 saved
As you can see, restic created a backup of the directory and was pretty
fast! The specific snapshot just created is identified by a sequence of
hexadecimal characters, ``40dc1520`` in this case.
If you run the command again, restic will create another snapshot of
your data, but this time it's even faster. This is de-duplication at
work!
.. code-block:: console
$ restic -r /tmp/backup backup ~/shared/work/web
enter password for repository:
using parent snapshot 40dc1520aa6a07b7b3ae561786770a01951245d2367241e71e9485f18ae8228c
scan [/home/user/work]
scanned 764 directories, 1816 files in 0:00
[0:00] 100.00% 0B/s 1.582 GiB / 1.582 GiB 2580 / 2580 items 0 errors ETA 0:00
duration: 0:00, 6572.38MiB/s
snapshot 79766175 saved
You can even backup individual files in the same repository.
.. code-block:: console
$ restic -r /tmp/backup backup ~/work.txt
scan [~/work.txt]
scanned 0 directories, 1 files in 0:00
[0:00] 100.00% 0B/s 220B / 220B 1 / 1 items 0 errors ETA 0:00
duration: 0:00, 0.03MiB/s
snapshot 31f7bd63 saved
In fact several hosts may use the same repository to backup directories
and files leading to a greater de-duplication.
Please be aware that when you backup different directories (or the
directories to be saved have a variable name component like a
time/date), restic always needs to read all files and only afterwards
can compute which parts of the files need to be saved. When you backup
the same directory again (maybe with new or changed files) restic will
find the old snapshot in the repo and by default only reads those files
that are new or have been modified since the last snapshot. This is
decided based on the modify date of the file in the file system.
You can exclude folders and files by specifying exclude-patterns. Either
specify them with multiple ``--exclude``'s or one ``--exclude-file``
.. code-block:: console
$ cat exclude
# exclude go-files
*.go
# exclude foo/x/y/z/bar foo/x/bar foo/bar
foo/**/bar
$ restic -r /tmp/backup backup ~/work --exclude=*.c --exclude-file=exclude
Patterns use `filepath.Glob <https://golang.org/pkg/path/filepath/#Glob>`__ internally,
see `filepath.Match <https://golang.org/pkg/path/filepath/#Match>`__ for syntax.
Additionally ``**`` excludes arbitrary subdirectories.
Environment-variables in exclude-files are expanded with
`os.ExpandEnv <https://golang.org/pkg/os/#ExpandEnv>`__.
By specifying the option ``--one-file-system`` you can instruct restic
to only backup files from the file systems the initially specified files
or directories reside on. For example, calling restic like this won't
backup ``/sys`` or ``/dev`` on a Linux system:
.. code-block:: console
$ restic -r /tmp/backup backup --one-file-system /
By using the ``--files-from`` option you can read the files you want to
backup from a file. This is especially useful if a lot of files have to
be backed up that are not in the same folder or are maybe pre-filtered
by other software.
For example maybe you want to backup files that have a certain filename
in them:
.. code-block:: console
$ find /tmp/somefiles | grep 'PATTERN' > /tmp/files_to_backup
You can then use restic to backup the filtered files:
.. code-block:: console
$ restic -r /tmp/backup backup --files-from /tmp/files_to_backup
Incidentally you can also combine ``--files-from`` with the normal files
args:
.. code-block:: console
$ restic -r /tmp/backup backup --files-from /tmp/files_to_backup /tmp/some_additional_file
Reading data from stdin
~~~~~~~~~~~~~~~~~~~~~~~
Sometimes it can be nice to directly save the output of a program, e.g.
``mysqldump`` so that the SQL can later be restored. Restic supports
this mode of operation, just supply the option ``--stdin`` to the
``backup`` command like this:
.. code-block:: console
$ mysqldump [...] | restic -r /tmp/backup backup --stdin
This creates a new snapshot of the output of ``mysqldump``. You can then
use e.g. the fuse mounting option (see below) to mount the repository
and read the file.
By default, the file name ``stdin`` is used, a different name can be
specified with ``--stdin-filename``, e.g. like this:
.. code-block:: console
$ mysqldump [...] | restic -r /tmp/backup backup --stdin --stdin-filename production.sql
Tags
~~~~
Snapshots can have one or more tags, short strings which add identifying
information. Just specify the tags for a snapshot with ``--tag``:
.. code-block:: console
$ restic -r /tmp/backup backup --tag projectX ~/shared/work/web
[...]
The tags can later be used to keep (or forget) snapshots.
List all snapshots
------------------
Now, you can list all the snapshots stored in the repository:
.. code-block:: console
$ restic -r /tmp/backup snapshots
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
40dc1520 2015-05-08 21:38:30 kasimir /home/user/work
79766175 2015-05-08 21:40:19 kasimir /home/user/work
bdbd3439 2015-05-08 21:45:17 luigi /home/art
590c8fc8 2015-05-08 21:47:38 kazik /srv
9f0bc19e 2015-05-08 21:46:11 luigi /srv
You can filter the listing by directory path:
.. code-block:: console
$ restic -r /tmp/backup snapshots --path="/srv"
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
590c8fc8 2015-05-08 21:47:38 kazik /srv
9f0bc19e 2015-05-08 21:46:11 luigi /srv
Or filter by host:
.. code-block:: console
$ restic -r /tmp/backup snapshots --host luigi
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
bdbd3439 2015-05-08 21:45:17 luigi /home/art
9f0bc19e 2015-05-08 21:46:11 luigi /srv
Combining filters is also possible.
Restore a snapshot
------------------
Restoring a snapshot is as easy as it sounds, just use the following
command to restore the contents of the latest snapshot to
``/tmp/restore-work``:
.. code-block:: console
$ restic -r /tmp/backup restore 79766175 --target ~/tmp/restore-work
enter password for repository:
restoring <Snapshot of [/home/user/work] at 2015-05-08 21:40:19.884408621 +0200 CEST> to /tmp/restore-work
Use the word ``latest`` to restore the last backup. You can also combine
``latest`` with the ``--host`` and ``--path`` filters to choose the last
backup for a specific host, path or both.
.. code-block:: console
$ restic -r /tmp/backup restore latest --target ~/tmp/restore-work --path "/home/art" --host luigi
enter password for repository:
restoring <Snapshot of [/home/art] at 2015-05-08 21:45:17.884408621 +0200 CEST> to /tmp/restore-work
Manage repository keys
----------------------
The ``key`` command allows you to set multiple access keys or passwords
per repository. In fact, you can use the ``list``, ``add``, ``remove``
and ``passwd`` sub-commands to manage these keys very precisely:
.. code-block:: console
$ restic -r /tmp/backup key list
enter password for repository:
ID User Host Created
----------------------------------------------------------------------
*eb78040b username kasimir 2015-08-12 13:29:57
$ restic -r /tmp/backup key add
enter password for repository:
enter password for new key:
enter password again:
saved new key as <Key of username@kasimir, created on 2015-08-12 13:35:05.316831933 +0200 CEST>
$ restic -r backup key list
enter password for repository:
ID User Host Created
----------------------------------------------------------------------
5c657874 username kasimir 2015-08-12 13:35:05
*eb78040b username kasimir 2015-08-12 13:29:57
Manage tags
-----------
Managing tags on snapshots is done with the ``tag`` command. The
existing set of tags can be replaced completely, tags can be added to
removed. The result is directly visible in the ``snapshots`` command.
Let's say we want to tag snapshot ``590c8fc8`` with the tags ``NL`` and
``CH`` and remove all other tags that may be present, the following
command does that:
.. code-block:: console
$ restic -r /tmp/backup tag --set NL,CH 590c8fc8
Create exclusive lock for repository
Modified tags on 1 snapshots
Note the snapshot ID has changed, so between each change we need to look
up the new ID of the snapshot. But there is an even better way, the
``tag`` command accepts ``--tag`` for a filter, so we can filter
snapshots based on the tag we just added.
So we can add and remove tags incrementally like this:
.. code-block:: console
$ restic -r /tmp/backup tag --tag NL --remove CH
Create exclusive lock for repository
Modified tags on 1 snapshots
$ restic -r /tmp/backup tag --tag NL --add UK
Create exclusive lock for repository
Modified tags on 1 snapshots
$ restic -r /tmp/backup tag --tag NL --remove NL
Create exclusive lock for repository
Modified tags on 1 snapshots
$ restic -r /tmp/backup tag --tag NL --add SOMETHING
No snapshots were modified
Check integrity and consistency
-------------------------------
Imagine your repository is saved on a server that has a faulty hard
drive, or even worse, attackers get privileged access and modify your
backup with the intention to make you restore malicious data:
.. code-block:: console
$ sudo echo "boom" >> backup/index/d795ffa99a8ab8f8e42cec1f814df4e48b8f49129360fb57613df93739faee97
In order to detect these things, it is a good idea to regularly use the
``check`` command to test whether everything is alright, your precious
backup data is consistent and the integrity is unharmed:
.. code-block:: console
$ restic -r /tmp/backup check
Load indexes
ciphertext verification failed
Trying to restore a snapshot which has been modified as shown above will
yield the same error:
.. code-block:: console
$ restic -r /tmp/backup restore 79766175 --target ~/tmp/restore-work
Load indexes
ciphertext verification failed
Mount a repository
------------------
Browsing your backup as a regular file system is also very easy. First,
create a mount point such as ``/mnt/restic`` and then use the following
command to serve the repository with FUSE:
.. code-block:: console
$ mkdir /mnt/restic
$ restic -r /tmp/backup mount /mnt/restic
enter password for repository:
Now serving /tmp/backup at /tmp/restic
Don't forget to umount after quitting!
Mounting repositories via FUSE is not possible on Windows and OpenBSD.
Restic supports storage and preservation of hard links. However, since
hard links exist in the scope of a filesystem by definition, restoring
hard links from a fuse mount should be done by a program that preserves
hard links. A program that does so is rsync, used with the option
--hard-links.
Removing old snapshots
----------------------
All backup space is finite, so restic allows removing old snapshots.
This can be done either manually (by specifying a snapshot ID to remove)
or by using a policy that describes which snapshots to forget. For all
remove operations, two commands need to be called in sequence:
``forget`` to remove a snapshot and ``prune`` to actually remove the
data that was referenced by the snapshot from the repository. This can
be automated with the ``--prune`` option of the ``forget`` command,
which runs ``prune`` automatically if snapshots have been removed.
Remove a single snapshot
~~~~~~~~~~~~~~~~~~~~~~~~
The command ``snapshots`` can be used to list all snapshots in a
repository like this:
.. code-block:: console
$ restic -r /tmp/backup snapshots
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
40dc1520 2015-05-08 21:38:30 kasimir /home/user/work
79766175 2015-05-08 21:40:19 kasimir /home/user/work
bdbd3439 2015-05-08 21:45:17 luigi /home/art
590c8fc8 2015-05-08 21:47:38 kazik /srv
9f0bc19e 2015-05-08 21:46:11 luigi /srv
In order to remove the snapshot of ``/home/art``, use the ``forget``
command and specify the snapshot ID on the command line:
.. code-block:: console
$ restic -r /tmp/backup forget bdbd3439
enter password for repository:
removed snapshot d3f01f63
Afterwards this snapshot is removed:
.. code-block:: console
$ restic -r /tmp/backup snapshots
enter password for repository:
ID Date Host Tags Directory
----------------------------------------------------------------------
40dc1520 2015-05-08 21:38:30 kasimir /home/user/work
79766175 2015-05-08 21:40:19 kasimir /home/user/work
590c8fc8 2015-05-08 21:47:38 kazik /srv
9f0bc19e 2015-05-08 21:46:11 luigi /srv
But the data that was referenced by files in this snapshot is still
stored in the repository. To cleanup unreferenced data, the ``prune``
command must be run:
.. code-block:: console
$ restic -r /tmp/backup prune
enter password for repository:
counting files in repo
building new index for repo
[0:00] 100.00% 22 / 22 files
repository contains 22 packs (8512 blobs) with 100.092 MiB bytes
processed 8512 blobs: 0 duplicate blobs, 0B duplicate
load all snapshots
find data that is still in use for 1 snapshots
[0:00] 100.00% 1 / 1 snapshots
found 8433 of 8512 data blobs still in use
will rewrite 3 packs
creating new index
[0:00] 86.36% 19 / 22 files
saved new index as 544a5084
done
Afterwards the repository is smaller.
You can automate this two-step process by using the ``--prune`` switch
to ``forget``:
.. code-block:: console
$ restic forget --keep-last 1 --prune
snapshots for host mopped, directories /home/user/work:
keep 1 snapshots:
ID Date Host Tags Directory
----------------------------------------------------------------------
4bba301e 2017-02-21 10:49:18 mopped /home/user/work
remove 1 snapshots:
ID Date Host Tags Directory
----------------------------------------------------------------------
8c02b94b 2017-02-21 10:48:33 mopped /home/user/work
1 snapshots have been removed, running prune
counting files in repo
building new index for repo
[0:00] 100.00% 37 / 37 packs
repository contains 37 packs (5521 blobs) with 151.012 MiB bytes
processed 5521 blobs: 0 duplicate blobs, 0B duplicate
load all snapshots
find data that is still in use for 1 snapshots
[0:00] 100.00% 1 / 1 snapshots
found 5323 of 5521 data blobs still in use, removing 198 blobs
will delete 0 packs and rewrite 27 packs, this frees 22.106 MiB
creating new index
[0:00] 100.00% 30 / 30 packs
saved new index as b49f3e68
done
Removing snapshots according to a policy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Removing snapshots manually is tedious and error-prone, therefore restic
allows specifying which snapshots should be removed automatically
according to a policy. You can specify how many hourly, daily, weekly,
monthly and yearly snapshots to keep, any other snapshots are removed.
The most important command-line parameter here is ``--dry-run`` which
instructs restic to not remove anything but print which snapshots would
be removed.
When ``forget`` is run with a policy, restic loads the list of all
snapshots, then groups these by host name and list of directories. The
policy is then applied to each group of snapshots separately. This is a
safety feature.
The ``forget`` command accepts the following parameters:
- ``--keep-last n`` never delete the ``n`` last (most recent) snapshots
- ``--keep-hourly n`` for the last ``n`` hours in which a snapshot was
made, keep only the last snapshot for each hour.
- ``--keep-daily n`` for the last ``n`` days which have one or more
snapshots, only keep the last one for that day.
- ``--keep-weekly n`` for the last ``n`` weeks which have one or more
snapshots, only keep the last one for that week.
- ``--keep-monthly n`` for the last ``n`` months which have one or more
snapshots, only keep the last one for that month.
- ``--keep-yearly n`` for the last ``n`` years which have one or more
snapshots, only keep the last one for that year.
- ``--keep-tag`` keep all snapshots which have all tags specified by
this option (can be specified multiple times).
Additionally, you can restrict removing snapshots to those which have a
particular hostname with the ``--hostname`` parameter, or tags with the
``--tag`` option. When multiple tags are specified, only the snapshots
which have all the tags are considered.
All the ``--keep-*`` options above only count
hours/days/weeks/months/years which have a snapshot, so those without a
snapshot are ignored.
All snapshots are evaluated counted against all matching keep-* counts. A
single snapshot on 2017-09-30 (Sun) will count as a daily, weekly and monthly.
Let's explain this with an example: Suppose you have only made a backup
on each Sunday for 12 weeks. Then ``forget --keep-daily 4`` will keep
the last four snapshots for the last four Sundays, but remove the rest.
Only counting the days which have a backup and ignore the ones without
is a safety feature: it prevents restic from removing many snapshots
when no new ones are created. If it was implemented otherwise, running
``forget --keep-daily 4`` on a Friday would remove all snapshots!
Another example: Suppose you make daily backups for 100 years. Then
``forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --keep-yearly 75``
will keep the most recent 7 daily snapshots, then 4 (remember, 7 dailies
already include a week!) last-day-of-the-weeks and 11 or 12
last-day-of-the-months (11 or 12 depends if the 5 weeklies cross a month).
And finally 75 last-day-of-the-year snapshots. All other snapshots are
removed.
Autocompletion
--------------
Restic can write out a bash compatible autocompletion script:
.. code-block:: console
$ ./restic autocomplete --help
The "autocomplete" command generates a shell 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.:
$ sudo restic autocomplete
Usage:
restic autocomplete [flags]
Flags:
--completionfile string autocompletion file (default "/etc/bash_completion.d/restic.sh")
Global Flags:
--json set output mode to JSON for commands that support it
--no-lock do not lock the repo, this allows some operations on read-only repos
-o, --option key=value set extended option (key=value, can be specified multiple times)
-p, --password-file string read the repository password from a file
-q, --quiet do not output comprehensive progress report
-r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY)
Debugging
---------
The program can be built with debug support like this:
.. code-block:: console
$ go run build.go -tags debug
Afterwards, extensive debug messages are written to the file in
environment variable ``DEBUG_LOG``, e.g.:
.. code-block:: console
$ DEBUG_LOG=/tmp/restic-debug.log restic backup ~/work
If you suspect that there is a bug, you can have a look at the debug
log. Please be aware that the debug log might contain sensitive
information such as file and directory names.
The debug log will always contain all log messages restic generates. You
can also instruct restic to print some or all debug messages to stderr.
These can also be limited to e.g. a list of source files or a list of
patterns for function names. The patterns are globbing patterns (see the
documentation for `path.Glob <https://golang.org/pkg/path/#Glob>`__), multiple
patterns are separated by commas. Patterns are case sensitive.
Printing all log messages to the console can be achieved by setting the
file filter to ``*``:
.. code-block:: console
$ DEBUG_FILES=* restic check
If you want restic to just print all debug log messages from the files
``main.go`` and ``lock.go``, set the environment variable
``DEBUG_FILES`` like this:
.. code-block:: console
$ DEBUG_FILES=main.go,lock.go restic check
The following command line instructs restic to only print debug
statements originating in functions that match the pattern ``*unlock*``
(case sensitive):
.. code-block:: console
$ DEBUG_FUNCS=*unlock* restic check
Under the hood: Browse repository objects
-----------------------------------------
Internally, a repository stores data of several different types
described in the `design
documentation <https://github.com/restic/restic/blob/master/doc/Design.md>`__.
You can ``list`` objects such as blobs, packs, index, snapshots, keys or
locks with the following command:
.. code-block:: console
$ restic -r /tmp/backup list snapshots
d369ccc7d126594950bf74f0a348d5d98d9e99f3215082eb69bf02dc9b3e464c
The ``find`` command searches for a given
`pattern <http://golang.org/pkg/path/filepath/#Match>`__ in the
repository.
.. code-block:: console
$ restic -r backup find test.txt
debug log file restic.log
debug enabled
enter password for repository:
found 1 matching entries in snapshot 196bc5760c909a7681647949e80e5448e276521489558525680acf1bd428af36
-rw-r--r-- 501 20 5 2015-08-26 14:09:57 +0200 CEST path/to/test.txt
The ``cat`` command allows you to display the JSON representation of the
objects or its raw content.
.. code-block:: console
$ restic -r /tmp/backup cat snapshot d369ccc7d126594950bf74f0a348d5d98d9e99f3215082eb69bf02dc9b3e464c
enter password for repository:
{
"time": "2015-08-12T12:52:44.091448856+02:00",
"tree": "05cec17e8d3349f402576d02576a2971fc0d9f9776ce2f441c7010849c4ff5af",
"paths": [
"/home/user/work"
],
"hostname": "kasimir",
"username": "username",
"uid": 501,
"gid": 20
}
Scripting
---------
Restic supports the output of some commands in JSON format, the JSON
data can then be processed by other programs (e.g.
`jq <https://stedolan.github.io/jq/>`__). The following example
lists all snapshots as JSON and uses ``jq`` to pretty-print the result:
.. code-block:: console
$ restic -r /tmp/backup snapshots --json | jq .
[
{
"time": "2017-03-11T09:57:43.26630619+01:00",
"tree": "bf25241679533df554fc0fd0ae6dbb9dcf1859a13f2bc9dd4543c354eff6c464",
"paths": [
"/home/work/doc"
],
"hostname": "kasimir",
"username": "fd0",
"uid": 1000,
"gid": 100,
"id": "bbeed6d28159aa384d1ccc6fa0b540644b1b9599b162d2972acda86b1b80f89e"
},
{
"time": "2017-03-11T09:58:57.541446938+01:00",
"tree": "7f8c95d3420baaac28dc51609796ae0e0ecfb4862b609a9f38ffaf7ae2d758da",
"paths": [
"/home/user/shared"
],
"hostname": "kasimir",
"username": "fd0",
"uid": 1000,
"gid": 100,
"id": "b157d91c16f0ba56801ece3a708dfc53791fe2a97e827090d6ed9a69a6ebdca0"
}
]
Temporary files
---------------
During some operations (e.g. ``backup`` and ``prune``) restic uses
temporary files to store data. These files will, by default, be saved to
the system's temporary directory, on Linux this is usually located in
``/tmp/``. The environment variable ``TMPDIR`` can be used to specify a
different directory, e.g. to use the directory ``/var/tmp/restic-tmp``
instead of the default, set the environment variable like this:
.. code-block:: console
$ export TMPDIR=/var/tmp/restic-tmp
$ restic -r /tmp/backup backup ~/work

9
doc/references.rst Normal file
View File

@@ -0,0 +1,9 @@
==========
References
==========
.. include:: design.rst
------------------------
.. include:: rest_backend.rst

2
doc/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
sphinx
sphinx_rtd_theme

84
doc/rest_backend.rst Normal file
View File

@@ -0,0 +1,84 @@
REST Backend
============
Restic can interact with HTTP Backend that respects the following REST
API. The following values are valid for ``{type}``: ``data``, ``keys``,
``locks``, ``snapshots``, ``index``, ``config``. ``{path}`` is a path to
the repository, so that multiple different repositories can be accessed.
The default path is ``/``.
POST {path}?create=true
-----------------------
This request is used to initially create a new repository. The server
responds with "200 OK" if the repository structure was created
successfully or already exists, otherwise an error is returned.
DELETE {path}
-------------
Deletes the repository on the server side. The server responds with "200
OK" if the repository was successfully removed. If this function is not
implemented the server returns "501 Not Implemented", if this it is
denied by the server it returns "403 Forbidden".
HEAD {path}/config
------------------
Returns "200 OK" if the repository has a configuration, an HTTP error
otherwise.
GET {path}/config
-----------------
Returns the content of the configuration file if the repository has a
configuration, an HTTP error otherwise.
Response format: binary/octet-stream
POST {path}/config
------------------
Returns "200 OK" if the configuration of the request body has been
saved, an HTTP error otherwise.
GET {path}/{type}/
------------------
Returns a JSON array containing the names of all the blobs stored for a
given type.
Response format: JSON
HEAD {path}/{type}/{name}
-------------------------
Returns "200 OK" if the blob with the given name and type is stored in
the repository, "404 not found" otherwise. If the blob exists, the HTTP
header ``Content-Length`` is set to the file size.
GET {path}/{type}/{name}
------------------------
Returns the content of the blob with the given name and type if it is
stored in the repository, "404 not found" otherwise.
If the request specifies a partial read with a Range header field, then
the status code of the response is 206 instead of 200 and the response
only contains the specified range.
Response format: binary/octet-stream
POST {path}/{type}/{name}
-------------------------
Saves the content of the request body as a blob with the given name and
type, an HTTP error otherwise.
Request format: binary/octet-stream
DELETE {path}/{type}/{name}
---------------------------
Returns "200 OK" if the blob with the given name and type has been
deleted from the repository, an HTTP error otherwise.

20
doc/talks.rst Normal file
View File

@@ -0,0 +1,20 @@
Talks
=====
The following talks will be or have been given about restic:
- 2016-01-31: Lightning Talk at the Go Devroom at FOSDEM 2016,
Brussels, Belgium
- 2016-01-29: `restic - Backups mal
richtig <https://media.ccc.de/v/c4.openchaos.2016.01.restic>`__:
Public lecture in German at `CCC Cologne
e.V. <https://koeln.ccc.de>`__ in Cologne, Germany
- 2015-08-23: `A Solution to the Backup
Inconvenience <https://programm.froscon.de/2015/events/1515.html>`__:
Lecture at `FROSCON 2015 <https://www.froscon.de>`__ in Bonn, Germany
- 2015-02-01: `Lightning Talk at FOSDEM
2015 <https://www.youtube.com/watch?v=oM-MfeflUZ8&t=11m40s>`__: A
short introduction (with slightly outdated command line)
- 2015-01-27: `Talk about restic at CCC
Aachen <https://videoag.fsmpi.rwth-aachen.de/?view=player&lectureid=4442#content>`__
(in German)

255
doc/tutorial_aws_s3.rst Normal file
View File

@@ -0,0 +1,255 @@
Setting up restic with Amazon S3
================================
Preface
-------
This tutorial will show you how to use restic with AWS S3. It will show you how
to navigate the AWS web interface, create an S3 bucket, create a user with
access to only this bucket, and finally how to connect restic to this bucket.
Prerequisites
-------------
You should already have a ``restic`` binary available on your system that you can
run. Furthermore, you should also have an account with
`AWS <https://aws.amazon.com/>`__. You will likely need to provide credit card
details for billing purposes, even if you use their
`free-tier <https://aws.amazon.com/free/>`__.
Logging into AWS
----------------
Point your browser to
https://console.aws.amazon.com
and log in using your AWS account. You will be presented with the AWS homepage:
.. image:: images/aws_s3/01_aws_start.png
:alt: AWS Homepage
By using the "Services" button in the upper left corder, a menu of all services
provided by AWS can be opened:
.. image:: images/aws_s3/02_aws_menu.png
:alt: AWS Services Menu
For this tutorial, the Simple Storage Service (S3), as well as Identity and
Access Management (IAM) are relevant.
Creating the bucket
-------------------
First, a bucket to store your backups in must be created. Using the "Services"
menu, navigate to S3. In case you already have some S3 buckets, you will see a
list of them here:
.. image:: images/aws_s3/03_buckets_list_before.png
:alt: List of S3 Buckets
Click the "Create bucket" button and choose a name and region for your new
bucket. For the purpose of this tutorial, the bucket will be named
``restic-demo`` and reside in Frankfurt. Because the bucket name space is
shared among all AWS users, the name ``restic-demo`` may not be available to
you. Be creative and choose a unique bucket name.
.. image:: images/aws_s3/04_bucket_create_start.png
:alt: Create a Bucket
It is not necessary to configure any special properties or permissions of the
bucket just yet. Therefore, just finish the wizard without making any further
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
buckets:
.. image:: images/aws_s3/06_buckets_list_after.png
:alt: List With New Bucket
Creating a user
---------------
Use the "Services" menu of the AWS web interface to navigate to IAM. This will
bring you to the IAM homepage. To create a new user, click on the "Users" menu
entry on the left:
.. image:: images/aws_s3/07_iam_start.png
:alt: IAM Home Page
In case you already have set-up users with IAM before, you will see a list of
them here. Use the "Add user" button at the top to create a new user:
.. image:: images/aws_s3/08_user_list.png
:alt: IAM User List
For this tutorial, the new user will be named ``restic-demo-user``. Feel free to
choose your own name that best fits your needs. This user will only ever access
AWS through the ``restic`` program and not through the web interface. Therefore,
"Programmatic access" is selected for "Access type":
.. image:: images/aws_s3/09_user_name.png
:alt: Choose User Name and Access Type
During the next step, permissions can be assigned to the new user. To use this
user with restic, it only needs access to the ``restic-demo`` bucket. Select
"Attach existing policies directly", which will bring up a list of pre-defined
policies below. Afterwards, click the "Create policy" button to create a custom
policy:
.. image:: images/aws_s3/10_user_pre_policy.png
:alt: Assign a Policy
A new browser window or tab will open with the policy wizard. In Amazon IAM,
policies are defined as JSON documents. For this tutorial, the "Policy
Generator" will be used to generate a policy file using a web interface:
.. image:: images/aws_s3/11_policy_start.png
:alt: Create a New Policy
After invoking the policy generator, you will be presented with a user
interface to generate individual permission statements. For restic to work, two
such statements must be created. The first statement is set up as follows:
.. code::
Effect: Allow
Service: Amazon S3
Actions: DeleteObject, GetObject, PutObject
Resource: arn:aws:s3:::restic-demo/*
This statement allows restic to create, read and delete objects inside the S3
bucket named ``restic-demo``. Adjust the bucket's name to the name of the bucket
you created earlier. Using the "Add Statement" button, this statement can be
saved. Now a second statement is created:
.. code::
Effect: Allow
Service: Amazon S3
Actions: ListBucket
Resource: arn:aws:s3:::restic-demo
Again, substitute ``restic-demo`` with the actual name of your bucket. Note that,
unlike before, there is no ``/*`` after the bucket name. This statement allows
restic to list the objects stored in the ``restic-demo`` bucket. Again, use "Add
Statement" to save this statement. The policy creator interface should now
look as follows:
.. image:: images/aws_s3/12_policy_permissions_done.png
:alt: Policy Creator With Two Statements
Continue to the next step and enter a name and description for this policy. For
this tutorial, the policy will be named ``restic-demo-policy``. In this step you
can also examine the JSON document created by the policy generator. Click
"Create Policy" to finish the process:
.. image:: images/aws_s3/13_policy_review.png
:alt: Policy Review
Go back to the browser window or tab where you were previously creating the new
user. Click the button labeled "Refresh" above the list of policies to make
sure the newly created policy is available to you. Afterwards, use the search
function to search for the ``restic-demo-policy``. Select this policy using the
checkbox on the left. Then, continue to the next step.
.. image:: images/aws_s3/14_user_attach_policy.png
:alt: Attach Policy to User
The next page will present an overview of the user account that is about to be
created. If everything looks good, click "Create user" to complete the process:
.. image:: images/aws_s3/15_user_review.png
:alt: User Creation Review
After the user has been created, its access credentials will be displayed. They
consist of the "Access key ID" (think user name), and the "Secret access key"
(think password). Copy these down to a safe place.
.. image:: images/aws_s3/16_user_created.png
:alt: User Credentials
You have now completed the configuration in AWS. Feel free to close your web
browser now.
Initializing the restic repository
----------------------------------
Open a terminal and make sure you have the ``restic`` binary ready. First, choose
a password to encrypt your backups with. In this tutorial, ``apg`` is used for
this purpose:
.. code-block:: console
$ apg -a 1 -m 32 -n 1 -M NCL
I9n7G7G0ZpDWA3GOcJbIuwQCGvGUBkU5
Note this password somewhere safe along with your AWS credentials. Next, the
configuration of restic will be placed into environment variables. This will
include sensitive information, such as your AWS secret and repository password.
Therefore, make sure the next commands **do not** end up in your shell's
history file. Adjust the contents of the environment variables to fit your
bucket's name and your user's API credentials.
.. code-block:: console
$ unset HISTFILE
$ export RESTIC_REPOSITORY="s3:https://s3.amazonaws.com/restic-demo"
$ export AWS_ACCESS_KEY_ID="AKIAJAJSLTZCAZ4SRI5Q"
$ export AWS_SECRET_ACCESS_KEY="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"
$ export RESTIC_PASSWORD="I9n7G7G0ZpDWA3GOcJbIuwQCGvGUBkU5"
After the environment is set up, restic may be called to initialize the
repository:
.. code-block:: console
$ ./restic init
created restic backend b5c661a86a at s3:https://s3.amazonaws.com/restic-demo
Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is
irrecoverably lost.
restic is now ready to be used with AWS S3. Try to create a backup:
.. code-block:: console
$ dd if=/dev/urandom bs=1M count=10 of=test.bin
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0,0891322 s, 118 MB/s
$ ./restic backup test.bin
scan [/home/philip/restic-demo/test.bin]
scanned 0 directories, 1 files in 0:00
[0:04] 100.00% 2.500 MiB/s 10.000 MiB / 10.000 MiB 1 / 1 items ... ETA 0:00
duration: 0:04, 2.47MiB/s
snapshot 10fdbace saved
$ ./restic snapshots
ID Date Host Tags Directory
----------------------------------------------------------------------
10fdbace 2017-03-26 16:41:50 blackbox /home/philip/restic-demo/test.bin
A snapshot was created and stored in the S3 bucket. This snapshot may now be
restored:
.. code-block:: console
$ mkdir restore
$ ./restic restore 10fdbace --target restore
restoring <Snapshot 10fdbace of [/home/philip/restic-demo/test.bin] at 2017-03-26 16:41:50.201418102 +0200 CEST by philip@blackbox> to restore
$ ls restore/
test.bin
The snapshot was successfully restored. This concludes the tutorial.

5
doc/tutorials.rst Normal file
View File

@@ -0,0 +1,5 @@
==========
Tutorials
==========
.. include:: tutorial_aws_s3.rst

View File

@@ -1,7 +0,0 @@
site_name: Documentation for restic
theme: readthedocs
docs_dir: doc
pages:
- Getting Started: index.md
- User Manual: Manual.md
- restic Design Document: Design.md

View File

@@ -9,12 +9,10 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
)
@@ -26,19 +24,6 @@ var ForbiddenImports = map[string]bool{
}
var runCrossCompile = flag.Bool("cross-compile", true, "run cross compilation tests")
var minioServer = flag.String("minio", "", "path to the minio server binary")
var debug = flag.Bool("debug", false, "output debug messages")
var minioServerEnv = map[string]string{
"MINIO_ACCESS_KEY": "KEBIYDZ87HCIH5D17YCN",
"MINIO_SECRET_KEY": "bVX1KhipSBPopEfmhc7rGz8ooxx27xdJ7Gkh1mVe",
}
var minioEnv = map[string]string{
"RESTIC_TEST_S3_SERVER": "http://127.0.0.1:9000",
"AWS_ACCESS_KEY_ID": "KEBIYDZ87HCIH5D17YCN",
"AWS_SECRET_ACCESS_KEY": "bVX1KhipSBPopEfmhc7rGz8ooxx27xdJ7Gkh1mVe",
}
func init() {
flag.Parse()
@@ -54,22 +39,11 @@ type CIEnvironment interface {
// TravisEnvironment is the environment in which Travis tests run.
type TravisEnvironment struct {
goxOSArch []string
minio string
minioSrv *Background
minioTempdir string
env map[string]string
env map[string]string
}
func (env *TravisEnvironment) getMinio() error {
if *minioServer != "" {
msg("using minio server at %q\n", *minioServer)
env.minio = *minioServer
return nil
}
tempfile, err := ioutil.TempFile("", "minio-server-")
tempfile, err := os.Create(filepath.Join(os.Getenv("GOPATH"), "bin", "minio"))
if err != nil {
return fmt.Errorf("create tempfile for minio download failed: %v\n", err)
}
@@ -104,41 +78,6 @@ func (env *TravisEnvironment) getMinio() error {
}
msg("downloaded minio server to %v\n", tempfile.Name())
env.minio = tempfile.Name()
return nil
}
func (env *TravisEnvironment) runMinio() error {
if env.minio == "" {
return nil
}
// start minio server
msg("starting minio server at %s", env.minio)
dir, err := ioutil.TempDir("", "minio-root")
if err != nil {
return fmt.Errorf("running minio server failed: %v", err)
}
env.minioSrv, err = StartBackgroundCommand(minioServerEnv, env.minio,
"server",
"--address", "127.0.0.1:9000",
dir)
if err != nil {
return fmt.Errorf("error running minio server: %v", err)
}
// go func() {
// time.Sleep(300 * time.Millisecond)
// env.minioSrv.Cmd.Process.Kill()
// }()
for k, v := range minioEnv {
env.env[k] = v
}
env.minioTempdir = dir
return nil
}
@@ -148,10 +87,14 @@ func (env *TravisEnvironment) Prepare() error {
msg("preparing environment for Travis CI\n")
for _, pkg := range []string{
pkgs := []string{
"golang.org/x/tools/cmd/cover",
"github.com/pierrre/gotestcover",
} {
"github.com/NebulousLabs/glyphcheck",
"github.com/restic/rest-server",
}
for _, pkg := range pkgs {
err := run("go", "get", pkg)
if err != nil {
return err
@@ -161,11 +104,8 @@ func (env *TravisEnvironment) Prepare() error {
if err := env.getMinio(); err != nil {
return err
}
if err := env.runMinio(); err != nil {
return err
}
if *runCrossCompile && !(runtime.Version() < "go1.7") {
if *runCrossCompile {
// only test cross compilation on linux with Travis
if err := run("go", "get", "github.com/mitchellh/gox"); err != nil {
return err
@@ -176,26 +116,14 @@ func (env *TravisEnvironment) Prepare() error {
"windows/386", "windows/amd64",
"darwin/386", "darwin/amd64",
"freebsd/386", "freebsd/amd64",
"opendbsd/386", "opendbsd/amd64",
}
if !strings.HasPrefix(runtime.Version(), "go1.3") {
env.goxOSArch = append(env.goxOSArch,
"linux/arm", "freebsd/arm")
"openbsd/386", "openbsd/amd64",
"linux/arm", "freebsd/arm",
}
} else {
env.goxOSArch = []string{runtime.GOOS + "/" + runtime.GOARCH}
}
msg("gox: OS/ARCH %v\n", env.goxOSArch)
if runtime.Version() < "go1.5" {
err := run("gox", "-build-toolchain",
"-osarch", strings.Join(env.goxOSArch, " "))
if err != nil {
return err
}
}
}
return nil
@@ -204,107 +132,15 @@ func (env *TravisEnvironment) Prepare() error {
// Teardown stops backend services and cleans the environment again.
func (env *TravisEnvironment) Teardown() error {
msg("run travis teardown\n")
if env.minioSrv != nil {
msg("stopping minio server\n")
if env.minioSrv.Cmd.ProcessState == nil {
err := env.minioSrv.Cmd.Process.Kill()
if err != nil {
fmt.Fprintf(os.Stderr, "error killing minio server process: %v", err)
}
} else {
result := <-env.minioSrv.Result
if result.Error != nil {
msg("minio server returned error: %v\n", result.Error)
msg("stdout: %s\n", result.Stdout)
msg("stderr: %s\n", result.Stderr)
}
}
err := os.RemoveAll(env.minioTempdir)
if err != nil {
msg("error removing minio tempdir %v: %v\n", env.minioTempdir, err)
}
}
return nil
}
func goVersionAtLeast151() bool {
v := runtime.Version()
if match, _ := regexp.MatchString(`^go1\.[0-4]`, v); match {
return false
}
if v == "go1.5" {
return false
}
return true
}
// Background is a program running in the background.
type Background struct {
Cmd *exec.Cmd
Result chan Result
}
// Result is the result of a program that ran in the background.
type Result struct {
Stdout, Stderr string
Error error
}
// StartBackgroundCommand runs a program in the background.
func StartBackgroundCommand(env map[string]string, cmd string, args ...string) (*Background, error) {
msg("running background command %v %v\n", cmd, args)
b := Background{
Result: make(chan Result, 1),
}
stdout := bytes.NewBuffer(nil)
stderr := bytes.NewBuffer(nil)
c := exec.Command(cmd, args...)
c.Stdout = stdout
c.Stderr = stderr
if *debug {
c.Stdout = io.MultiWriter(c.Stdout, os.Stdout)
c.Stderr = io.MultiWriter(c.Stderr, os.Stderr)
}
c.Env = updateEnv(os.Environ(), env)
b.Cmd = c
err := c.Start()
if err != nil {
msg("error starting background job %v: %v\n", cmd, err)
return nil, err
}
go func() {
err := b.Cmd.Wait()
msg("background job %v returned: %v\n", cmd, err)
msg("stdout: %s\n", stdout.Bytes())
msg("stderr: %s\n", stderr.Bytes())
b.Result <- Result{
Stdout: string(stdout.Bytes()),
Stderr: string(stderr.Bytes()),
Error: err,
}
}()
return &b, nil
}
// RunTests starts the tests for Travis.
func (env *TravisEnvironment) RunTests() error {
// do not run fuse tests on darwin
if runtime.GOOS == "darwin" {
msg("skip fuse integration tests on %v\n", runtime.GOOS)
os.Setenv("RESTIC_TEST_FUSE", "0")
_ = os.Setenv("RESTIC_TEST_FUSE", "0")
}
cwd, err := os.Getwd()
@@ -314,7 +150,23 @@ func (env *TravisEnvironment) RunTests() error {
env.env["GOPATH"] = cwd + ":" + filepath.Join(cwd, "vendor")
if *runCrossCompile && !(runtime.Version() < "go1.7") {
// ensure that the following tests cannot be silently skipped on Travis
ensureTests := []string{
"restic/backend/rest.TestBackendREST",
"restic/backend/sftp.TestBackendSFTP",
"restic/backend/s3.TestBackendMinio",
}
// if the test s3 repository is available, make sure that the test is not skipped
if os.Getenv("RESTIC_TEST_S3_REPOSITORY") != "" {
ensureTests = append(ensureTests, "restic/backend/s3.TestBackendS3")
} else {
msg("S3 repository not available\n")
}
env.env["RESTIC_TEST_DISALLOW_SKIP"] = strings.Join(ensureTests, ",")
if *runCrossCompile {
// compile for all target architectures with tags
for _, tags := range []string{"release", "debug"} {
err := runWithEnv(env.env, "gox", "-verbose",
@@ -343,6 +195,10 @@ func (env *TravisEnvironment) RunTests() error {
return err
}
if err = runGlyphcheck(); err != nil {
return err
}
deps, err := findImports()
if err != nil {
return err
@@ -387,16 +243,16 @@ func (env *AppveyorEnvironment) Teardown() error {
// findGoFiles returns a list of go source code file names below dir.
func findGoFiles(dir string) (list []string, err error) {
err = filepath.Walk(dir, func(name string, fi os.FileInfo, err error) error {
if filepath.Base(name) == "vendor" {
relpath, err := filepath.Rel(dir, name)
if err != nil {
return err
}
if relpath == "vendor" || relpath == "pkg" {
return filepath.SkipDir
}
if filepath.Ext(name) == ".go" {
relpath, err := filepath.Rel(dir, name)
if err != nil {
return err
}
if filepath.Ext(relpath) == ".go" {
list = append(list, relpath)
}
@@ -498,6 +354,18 @@ func runGofmt() error {
return nil
}
func runGlyphcheck() error {
cmd := exec.Command("glyphcheck", "./...")
cmd.Stderr = os.Stderr
buf, err := cmd.Output()
if err != nil {
return fmt.Errorf("error running glyphcheck: %v\noutput: %s\n", err, buf)
}
return nil
}
func run(command string, args ...string) error {
msg("run %v %v\n", command, strings.Join(args, " "))
return runWithEnv(nil, command, args...)

View File

@@ -1,24 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

View File

@@ -1,24 +0,0 @@
Copyright (c) 2015, Bertil Chapuis
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,29 +0,0 @@
# Restic Server
Restic Server is a sample server that implement restic's rest backend api.
It has been developed for demonstration purpose and is not intented to be used in production.
## Getting started
By default the server persists backup data in `/tmp/restic`.
Build and start the server with a custom persistence directory:
```
go build
./restic-server -path /user/home/backup
```
The server use an `.htpasswd` file to specify users. You can create such a file at the root of the persistence directory by executing the following command. In order to append new user to the file, just omit the `-c` argument.
```
htpasswd -s -c .htpasswd username
```
By default the server uses http. This is not very secure since with Basic Authentication, username and passwords will be present in every request. In order to enable TLS support just add the `-tls` argument and add a private and public key at the root of your persistence directory.
Signed certificate are required by the restic backend but if you just want to test the feature you can generate unsigned keys with the following commands:
```
openssl genrsa -out private_key 2048
openssl req -new -x509 -key private_key -out public_key -days 365
```

View File

@@ -1,194 +0,0 @@
// +build go1.4
package main
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"restic/fs"
)
// Context contains repository meta-data.
type Context struct {
path string
}
// AuthHandler wraps h with a http.HandlerFunc that performs basic
// authentication against the user/passwords pairs stored in f and returns the
// http.HandlerFunc.
func AuthHandler(f *HtpasswdFile, h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok {
http.Error(w, "401 unauthorized", 401)
return
}
if !f.Validate(username, password) {
http.Error(w, "401 unauthorized", 401)
return
}
h.ServeHTTP(w, r)
}
}
// CheckConfig returns a http.HandlerFunc that checks whether
// a configuration exists.
func CheckConfig(c *Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
config := filepath.Join(c.path, "config")
st, err := os.Stat(config)
if err != nil {
http.Error(w, "404 not found", 404)
return
}
w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}
}
// GetConfig returns a http.HandlerFunc that allows for a
// config to be retrieved.
func GetConfig(c *Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
config := filepath.Join(c.path, "config")
bytes, err := ioutil.ReadFile(config)
if err != nil {
http.Error(w, "404 not found", 404)
return
}
w.Write(bytes)
}
}
// SaveConfig returns a http.HandlerFunc that allows for a
// config to be saved.
func SaveConfig(c *Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
config := filepath.Join(c.path, "config")
bytes, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "400 bad request", 400)
return
}
errw := ioutil.WriteFile(config, bytes, 0600)
if errw != nil {
http.Error(w, "500 internal server error", 500)
return
}
w.Write([]byte("200 ok"))
}
}
// ListBlobs returns a http.HandlerFunc that lists
// all blobs of a given type in an arbitrary order.
func ListBlobs(c *Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := strings.Split(r.RequestURI, "/")
dir := vars[1]
path := filepath.Join(c.path, dir)
files, err := ioutil.ReadDir(path)
if err != nil {
http.Error(w, "404 not found", 404)
return
}
names := make([]string, len(files))
for i, f := range files {
names[i] = f.Name()
}
data, err := json.Marshal(names)
if err != nil {
http.Error(w, "500 internal server error", 500)
return
}
w.Write(data)
}
}
// CheckBlob reutrns a http.HandlerFunc that tests whether a blob exists
// and returns 200, if it does, or 404 otherwise.
func CheckBlob(c *Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := strings.Split(r.RequestURI, "/")
dir := vars[1]
name := vars[2]
path := filepath.Join(c.path, dir, name)
st, err := os.Stat(path)
if err != nil {
http.Error(w, "404 not found", 404)
return
}
w.Header().Add("Content-Length", fmt.Sprint(st.Size()))
}
}
// GetBlob returns a http.HandlerFunc that retrieves a blob
// from the repository.
func GetBlob(c *Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := strings.Split(r.RequestURI, "/")
dir := vars[1]
name := vars[2]
path := filepath.Join(c.path, dir, name)
file, err := fs.Open(path)
if err != nil {
http.Error(w, "404 not found", 404)
return
}
defer file.Close()
http.ServeContent(w, r, "", time.Unix(0, 0), file)
}
}
// SaveBlob returns a http.HandlerFunc that saves a blob to the repository.
func SaveBlob(c *Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := strings.Split(r.RequestURI, "/")
dir := vars[1]
name := vars[2]
path := filepath.Join(c.path, dir, name)
tmp := path + "_tmp"
tf, err := fs.OpenFile(tmp, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
http.Error(w, "500 internal server error", 500)
return
}
if _, err := io.Copy(tf, r.Body); err != nil {
http.Error(w, "400 bad request", 400)
tf.Close()
os.Remove(tmp)
return
}
if err := tf.Close(); err != nil {
http.Error(w, "500 internal server error", 500)
}
if err := os.Rename(tmp, path); err != nil {
http.Error(w, "500 internal server error", 500)
return
}
w.Write([]byte("200 ok"))
}
}
// DeleteBlob returns a http.HandlerFunc that deletes a blob from the
// repository.
func DeleteBlob(c *Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := strings.Split(r.RequestURI, "/")
dir := vars[1]
name := vars[2]
path := filepath.Join(c.path, dir, name)
err := os.Remove(path)
if err != nil {
http.Error(w, "500 internal server error", 500)
return
}
w.Write([]byte("200 ok"))
}
}

View File

@@ -1,97 +0,0 @@
// +build go1.4
package main
/*
Copied from: github.com/bitly/oauth2_proxy
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import (
"crypto/sha1"
"encoding/base64"
"encoding/csv"
"io"
"log"
"restic/fs"
)
// lookup passwords in a htpasswd file
// The entries must have been created with -s for SHA encryption
// HtpasswdFile is a map for usernames to passwords.
type HtpasswdFile struct {
Users map[string]string
}
// NewHtpasswdFromFile reads the users and passwords from a htpasswd
// file and returns them. If an error is encountered, it is returned, together
// with a nil-Pointer for the HtpasswdFile.
func NewHtpasswdFromFile(path string) (*HtpasswdFile, error) {
r, err := fs.Open(path)
if err != nil {
return nil, err
}
defer r.Close()
return NewHtpasswd(r)
}
// NewHtpasswd reads the users and passwords from a htpasswd
// datastream in file and returns them. If an error is encountered,
// it is returned, together with a nil-Pointer for the HtpasswdFile.
func NewHtpasswd(file io.Reader) (*HtpasswdFile, error) {
cr := csv.NewReader(file)
cr.Comma = ':'
cr.Comment = '#'
cr.TrimLeadingSpace = true
records, err := cr.ReadAll()
if err != nil {
return nil, err
}
h := &HtpasswdFile{Users: make(map[string]string)}
for _, record := range records {
h.Users[record[0]] = record[1]
}
return h, nil
}
// Validate returns true if password matches the stored password
// for user. If no password for user is stored, or the password
// is wrong, false is returned.
func (h *HtpasswdFile) Validate(user string, password string) bool {
realPassword, exists := h.Users[user]
if !exists {
return false
}
if realPassword[:5] == "{SHA}" {
d := sha1.New()
d.Write([]byte(password))
if realPassword[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
return true
}
} else {
log.Printf("Invalid htpasswd entry for %s. Must be a SHA entry.", user)
}
return false
}

View File

@@ -1,137 +0,0 @@
// +build go1.4
package main
import (
"log"
"net/http"
"strings"
)
// Route is a handler for a path that was already split.
type Route struct {
path []string
handler http.Handler
}
// Router maps HTTP methods to a slice of Route handlers.
type Router struct {
routes map[string][]Route
}
// NewRouter creates a new Router and returns a pointer to it.
func NewRouter() *Router {
return &Router{make(map[string][]Route)}
}
// Options registers handler for path with method "OPTIONS".
func (router *Router) Options(path string, handler http.Handler) {
router.Handle("OPTIONS", path, handler)
}
// OptionsFunc registers handler for path with method "OPTIONS".
func (router *Router) OptionsFunc(path string, handler http.HandlerFunc) {
router.Handle("OPTIONS", path, handler)
}
// Get registers handler for path with method "GET".
func (router *Router) Get(path string, handler http.Handler) {
router.Handle("GET", path, handler)
}
// GetFunc registers handler for path with method "GET".
func (router *Router) GetFunc(path string, handler http.HandlerFunc) {
router.Handle("GET", path, handler)
}
// Head registers handler for path with method "HEAD".
func (router *Router) Head(path string, handler http.Handler) {
router.Handle("HEAD", path, handler)
}
// HeadFunc registers handler for path with method "HEAD".
func (router *Router) HeadFunc(path string, handler http.HandlerFunc) {
router.Handle("HEAD", path, handler)
}
// Post registers handler for path with method "POST".
func (router *Router) Post(path string, handler http.Handler) {
router.Handle("POST", path, handler)
}
// PostFunc registers handler for path with method "POST".
func (router *Router) PostFunc(path string, handler http.HandlerFunc) {
router.Handle("POST", path, handler)
}
// Put registers handler for path with method "PUT".
func (router *Router) Put(path string, handler http.Handler) {
router.Handle("PUT", path, handler)
}
// PutFunc registers handler for path with method "PUT".
func (router *Router) PutFunc(path string, handler http.HandlerFunc) {
router.Handle("PUT", path, handler)
}
// Delete registers handler for path with method "DELETE".
func (router *Router) Delete(path string, handler http.Handler) {
router.Handle("DELETE", path, handler)
}
// DeleteFunc registers handler for path with method "DELETE".
func (router *Router) DeleteFunc(path string, handler http.HandlerFunc) {
router.Handle("DELETE", path, handler)
}
// Trace registers handler for path with method "TRACE".
func (router *Router) Trace(path string, handler http.Handler) {
router.Handle("TRACE", path, handler)
}
// TraceFunc registers handler for path with method "TRACE".
func (router *Router) TraceFunc(path string, handler http.HandlerFunc) {
router.Handle("TRACE", path, handler)
}
// Connect registers handler for path with method "Connect".
func (router *Router) Connect(path string, handler http.Handler) {
router.Handle("Connect", path, handler)
}
// ConnectFunc registers handler for path with method "Connect".
func (router *Router) ConnectFunc(path string, handler http.HandlerFunc) {
router.Handle("Connect", path, handler)
}
// Handle registers a http.Handler for method and uri
func (router *Router) Handle(method string, uri string, handler http.Handler) {
routes := router.routes[method]
path := strings.Split(uri, "/")
routes = append(routes, Route{path, handler})
router.routes[method] = routes
}
func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
method := r.Method
uri := r.RequestURI
path := strings.Split(uri, "/")
log.Printf("%s %s", method, uri)
ROUTE:
for _, route := range router.routes[method] {
if len(route.path) != len(path) {
continue
}
for i := 0; i < len(route.path); i++ {
if !strings.HasPrefix(route.path[i], ":") && route.path[i] != path[i] {
continue ROUTE
}
}
route.handler.ServeHTTP(w, r)
return
}
http.Error(w, "404 not found", 404)
}

View File

@@ -1,74 +0,0 @@
// +build go1.4
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestRouter(t *testing.T) {
router := NewRouter()
getConfig := []byte("GET /config")
router.GetFunc("/config", func(w http.ResponseWriter, r *http.Request) {
w.Write(getConfig)
})
postConfig := []byte("POST /config")
router.PostFunc("/config", func(w http.ResponseWriter, r *http.Request) {
w.Write(postConfig)
})
getBlobs := []byte("GET /blobs/")
router.GetFunc("/blobs/", func(w http.ResponseWriter, r *http.Request) {
w.Write(getBlobs)
})
getBlob := []byte("GET /blobs/:sha")
router.GetFunc("/blobs/:sha", func(w http.ResponseWriter, r *http.Request) {
w.Write(getBlob)
})
server := httptest.NewServer(router)
defer server.Close()
getConfigResp, _ := http.Get(server.URL + "/config")
getConfigBody, _ := ioutil.ReadAll(getConfigResp.Body)
if getConfigResp.StatusCode != 200 {
t.Fatalf("Wanted HTTP Status 200, got %d", getConfigResp.StatusCode)
}
if string(getConfig) != string(getConfigBody) {
t.Fatalf("Config wrong:\nWanted '%s'\nGot: '%s'", string(getConfig), string(getConfigBody))
}
postConfigResp, _ := http.Post(server.URL+"/config", "binary/octet-stream", strings.NewReader("post test"))
postConfigBody, _ := ioutil.ReadAll(postConfigResp.Body)
if postConfigResp.StatusCode != 200 {
t.Fatalf("Wanted HTTP Status 200, got %d", postConfigResp.StatusCode)
}
if string(postConfig) != string(postConfigBody) {
t.Fatalf("Config wrong:\nWanted '%s'\nGot: '%s'", string(postConfig), string(postConfigBody))
}
getBlobsResp, _ := http.Get(server.URL + "/blobs/")
getBlobsBody, _ := ioutil.ReadAll(getBlobsResp.Body)
if getBlobsResp.StatusCode != 200 {
t.Fatalf("Wanted HTTP Status 200, got %d", getBlobsResp.StatusCode)
}
if string(getBlobs) != string(getBlobsBody) {
t.Fatalf("Config wrong:\nWanted '%s'\nGot: '%s'", string(getBlobs), string(getBlobsBody))
}
getBlobResp, _ := http.Get(server.URL + "/blobs/test")
getBlobBody, _ := ioutil.ReadAll(getBlobResp.Body)
if getBlobResp.StatusCode != 200 {
t.Fatalf("Wanted HTTP Status 200, got %d", getBlobResp.StatusCode)
}
if string(getBlob) != string(getBlobBody) {
t.Fatalf("Config wrong:\nWanted '%s'\nGot: '%s'", string(getBlob), string(getBlobBody))
}
}

View File

@@ -1,73 +0,0 @@
// +build go1.4
package main
import (
"flag"
"log"
"net/http"
"os"
"path/filepath"
)
const (
defaultHTTPPort = ":8000"
defaultHTTPSPort = ":8443"
)
func main() {
// Parse command-line args
var path = flag.String("path", "/tmp/restic", "specifies the path of the data directory")
var tls = flag.Bool("tls", false, "turns on tls support")
flag.Parse()
// Create the missing directories
dirs := []string{
"data",
"snapshots",
"index",
"locks",
"keys",
"tmp",
}
for _, d := range dirs {
os.MkdirAll(filepath.Join(*path, d), 0700)
}
// Define the routes
context := &Context{*path}
router := NewRouter()
router.HeadFunc("/config", CheckConfig(context))
router.GetFunc("/config", GetConfig(context))
router.PostFunc("/config", SaveConfig(context))
router.GetFunc("/:dir/", ListBlobs(context))
router.HeadFunc("/:dir/:name", CheckBlob(context))
router.GetFunc("/:type/:name", GetBlob(context))
router.PostFunc("/:type/:name", SaveBlob(context))
router.DeleteFunc("/:type/:name", DeleteBlob(context))
// Check for a password file
var handler http.Handler
htpasswdFile, err := NewHtpasswdFromFile(filepath.Join(*path, ".htpasswd"))
if err != nil {
log.Println("Authentication disabled")
handler = router
} else {
log.Println("Authentication enabled")
handler = AuthHandler(htpasswdFile, router)
}
// start the server
if !*tls {
log.Printf("start server on port %s\n", defaultHTTPPort)
http.ListenAndServe(defaultHTTPPort, handler)
} else {
privateKey := filepath.Join(*path, "private_key")
publicKey := filepath.Join(*path, "public_key")
log.Println("TLS enabled")
log.Printf("private key: %s", privateKey)
log.Printf("public key: %s", publicKey)
log.Printf("start server on port %s\n", defaultHTTPSPort)
http.ListenAndServeTLS(defaultHTTPSPort, publicKey, privateKey, handler)
}
}

View File

@@ -0,0 +1,9 @@
// +build !linux
package main
// IsProcessBackground should return true if it is running in the background or false if not
func IsProcessBackground() bool {
//TODO: Check if the process are running in the background in other OS than linux
return false
}

View File

@@ -0,0 +1,21 @@
package main
import (
"syscall"
"unsafe"
"restic/debug"
)
// IsProcessBackground returns true if it is running in the background or false if not
func IsProcessBackground() bool {
var pid int
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), syscall.TIOCGPGRP, uintptr(unsafe.Pointer(&pid)))
if err != 0 {
debug.Log("Can't check if we are in the background. Using default behaviour. Error: %s\n", err.Error())
return false
}
return pid != syscall.Getpgrp()
}

View File

@@ -62,8 +62,13 @@ func CleanupHandler(c <-chan os.Signal) {
for s := range c {
debug.Log("signal %v received, cleaning up", s)
fmt.Printf("%sInterrupt received, cleaning up\n", ClearLine())
RunCleanupHandlers()
fmt.Println("exiting")
os.Exit(0)
Exit(0)
}
}
// Exit runs the cleanup handlers and then terminates the process with the
// given exit code.
func Exit(code int) {
RunCleanupHandlers()
os.Exit(code)
}

View File

@@ -0,0 +1,36 @@
package main
import (
"github.com/spf13/cobra"
)
var autocompleteTarget string
var cmdAutocomplete = &cobra.Command{
Use: "autocomplete",
Short: "generate shell autocompletion script",
Long: `The "autocomplete" command generates a shell 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.:
$ sudo restic autocomplete`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := cmdRoot.GenBashCompletionFile(autocompleteTarget); err != nil {
return err
}
return nil
},
}
func init() {
cmdRoot.AddCommand(cmdAutocomplete)
cmdAutocomplete.Flags().StringVarP(&autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/restic.sh", "autocompletion file")
// For bash-completion
cmdAutocomplete.Flags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{})
}

View File

@@ -3,14 +3,13 @@ package main
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"restic"
"strings"
"time"
"golang.org/x/crypto/ssh/terminal"
"github.com/spf13/cobra"
"restic/archiver"
@@ -28,6 +27,10 @@ The "backup" command creates a new snapshot and saves the files and directories
given as the arguments.
`,
RunE: func(cmd *cobra.Command, args []string) error {
if backupOptions.Stdin && backupOptions.FilesFrom == "-" {
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
}
if backupOptions.Stdin {
return readBackupFromStdin(backupOptions, globalOptions, args)
}
@@ -41,11 +44,13 @@ type BackupOptions struct {
Parent string
Force bool
Excludes []string
ExcludeFile string
ExcludeFiles []string
ExcludeOtherFS bool
Stdin bool
StdinFilename string
Tags []string
Hostname string
FilesFrom string
}
var backupOptions BackupOptions
@@ -53,15 +58,23 @@ var backupOptions BackupOptions
func init() {
cmdRoot.AddCommand(cmdBackup)
hostname, err := os.Hostname()
if err != nil {
debug.Log("os.Hostname() returned err: %v", err)
hostname = ""
}
f := cmdBackup.Flags()
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)")
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories. Overrides the "parent" flag`)
f.StringSliceVarP(&backupOptions.Excludes, "exclude", "e", []string{}, "exclude a `pattern` (can be specified multiple times)")
f.StringVar(&backupOptions.ExcludeFile, "exclude-file", "", "read exclude patterns from a file")
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "Exclude other file systems")
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`)
f.StringSliceVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
f.StringSliceVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "", "file name to use when reading from stdin")
f.StringSliceVar(&backupOptions.Tags, "tag", []string{}, "add a `tag` for the new snapshot (can be specified multiple times)")
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin")
f.StringSliceVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
f.StringVar(&backupOptions.Hostname, "hostname", hostname, "set the `hostname` for the snapshot manually")
f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)")
}
func newScanProgress(gopts GlobalOptions) *restic.Progress {
@@ -71,8 +84,13 @@ func newScanProgress(gopts GlobalOptions) *restic.Progress {
p := restic.NewProgress()
p.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
if IsProcessBackground() {
return
}
PrintProgress("[%s] %d directories, %d files, %s", formatDuration(d), s.Dirs, s.Files, formatBytes(s.Bytes))
}
p.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
PrintProgress("scanned %d directories, %d files in %s\n", s.Dirs, s.Files, formatDuration(d))
}
@@ -91,6 +109,10 @@ func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress
itemsTodo := todo.Files + todo.Dirs
archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
if IsProcessBackground() {
return
}
sec := uint64(d / time.Second)
if todo.Bytes > 0 && sec > 0 && ticker {
bps = s.Bytes / sec
@@ -112,8 +134,7 @@ func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress
s.Errors)
status2 := fmt.Sprintf("ETA %s ", formatSeconds(eta))
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
if err == nil {
if w := stdoutTerminalWidth(); w > 0 {
maxlen := w - len(status2) - 1
if maxlen < 4 {
@@ -144,6 +165,10 @@ func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress {
var bps uint64
archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
if IsProcessBackground() {
return
}
sec := uint64(d / time.Second)
if s.Bytes > 0 && sec > 0 && ticker {
bps = s.Bytes / sec
@@ -153,8 +178,7 @@ func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress {
formatBytes(s.Bytes),
formatBytes(bps))
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
if err == nil {
if w := stdoutTerminalWidth(); w > 0 {
maxlen := w - len(status1)
if maxlen < 4 {
@@ -196,8 +220,8 @@ func filterExisting(items []string) (result []string, err error) {
// gatherDevices returns the set of unique device ids of the files and/or
// directory paths listed in "items".
func gatherDevices(items []string) (deviceMap map[uint64]struct{}, err error) {
deviceMap = make(map[uint64]struct{})
func gatherDevices(items []string) (deviceMap map[string]uint64, err error) {
deviceMap = make(map[string]uint64)
for _, item := range items {
fi, err := fs.Lstat(item)
if err != nil {
@@ -207,7 +231,7 @@ func gatherDevices(items []string) (deviceMap map[uint64]struct{}, err error) {
if err != nil {
return nil, err
}
deviceMap[id] = struct{}{}
deviceMap[item] = id
}
if len(deviceMap) == 0 {
return nil, errors.New("zero allowed devices")
@@ -217,7 +241,15 @@ func gatherDevices(items []string) (deviceMap map[uint64]struct{}, err error) {
func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 {
return errors.Fatalf("when reading from stdin, no additional files can be specified")
return errors.Fatal("when reading from stdin, no additional files can be specified")
}
if opts.StdinFilename == "" {
return errors.Fatal("filename for backup from stdin must not be empty")
}
if gopts.password == "" && gopts.PasswordFile == "" {
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
}
repo, err := OpenRepository(gopts)
@@ -236,18 +268,74 @@ func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string)
return err
}
_, id, err := archiver.ArchiveReader(repo, newArchiveStdinProgress(gopts), os.Stdin, opts.StdinFilename, opts.Tags)
r := &archiver.Reader{
Repository: repo,
Tags: opts.Tags,
Hostname: opts.Hostname,
}
_, id, err := r.Archive(opts.StdinFilename, os.Stdin, newArchiveStdinProgress(gopts))
if err != nil {
return err
}
fmt.Printf("archived as %v\n", id.Str())
Verbosef("archived as %v\n", id.Str())
return nil
}
// readFromFile will read all lines from the given filename and write them to a
// string array, if filename is empty readFromFile returns and empty string
// array. If filename is a dash (-), readFromFile will read the lines from
// the standard input.
func readLinesFromFile(filename string) ([]string, error) {
if filename == "" {
return nil, nil
}
var r io.Reader = os.Stdin
if filename != "-" {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
r = f
}
var lines []string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
lines = append(lines, line)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return lines, nil
}
func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
if opts.FilesFrom == "-" && gopts.password == "" && gopts.PasswordFile == "" {
return errors.Fatal("no password; either use `--password-file` option or put the password into the RESTIC_PASSWORD environment variable")
}
fromfile, err := readLinesFromFile(opts.FilesFrom)
if err != nil {
return err
}
// merge files from files-from into normal args so we can reuse the normal
// args checks and have the ability to use both files-from and args at the
// same time
args = append(args, fromfile...)
if len(args) == 0 {
return errors.Fatalf("wrong number of parameters")
return errors.Fatal("wrong number of parameters")
}
target := make([]string, 0, len(args))
@@ -258,13 +346,13 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
target = append(target, d)
}
target, err := filterExisting(target)
target, err = filterExisting(target)
if err != nil {
return err
}
// allowed devices
var allowedDevs map[uint64]struct{}
var allowedDevs map[string]uint64
if opts.ExcludeOtherFS {
allowedDevs, err = gatherDevices(target)
if err != nil {
@@ -303,7 +391,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, "")
id, err := restic.FindLatestSnapshot(repo, target, opts.Tags, opts.Hostname)
if err == nil {
parentSnapshotID = &id
} else if err != restic.ErrNoSnapshotFound {
@@ -318,17 +406,28 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
Verbosef("scan %v\n", target)
// add patterns from file
if opts.ExcludeFile != "" {
file, err := fs.Open(opts.ExcludeFile)
if err != nil {
Warnf("error reading exclude patterns: %v", err)
return nil
}
if len(opts.ExcludeFiles) > 0 {
for _, filename := range opts.ExcludeFiles {
file, err := fs.Open(filename)
if err != nil {
Warnf("error reading exclude patterns: %v", err)
return nil
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// ignore empty lines
if line == "" {
continue
}
// strip comments
if strings.HasPrefix(line, "#") {
continue
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "#") {
line = os.ExpandEnv(line)
opts.Excludes = append(opts.Excludes, line)
}
@@ -346,7 +445,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
return false
}
if !opts.ExcludeOtherFS {
if !opts.ExcludeOtherFS || fi == nil {
return true
}
@@ -356,13 +455,24 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
// errored out earlier. If it still does that's a reason to panic.
panic(err)
}
_, found := allowedDevs[id]
if !found {
debug.Log("path %q on disallowed device %d", item, id)
return false
for dir := item; dir != ""; dir = filepath.Dir(dir) {
debug.Log("item %v, test dir %v", item, dir)
allowedID, ok := allowedDevs[dir]
if !ok {
continue
}
if allowedID != id {
debug.Log("path %q on disallowed device %d", item, id)
return false
}
return true
}
return true
panic(fmt.Sprintf("item %v, device id %v not found, allowedDevs: %v", item, id, allowedDevs))
}
stat, err := archiver.Scan(target, selectFilter, newScanProgress(gopts))
@@ -374,13 +484,12 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error {
arch.Excludes = opts.Excludes
arch.SelectFilter = selectFilter
arch.Error = func(dir string, fi os.FileInfo, err error) error {
arch.Warn = func(dir string, fi os.FileInfo, err error) {
// TODO: make ignoring errors configurable
Warnf("%s\rerror for %s: %v\n", ClearLine(), dir, err)
return nil
Warnf("%s\rwarning for %s: %v\n", ClearLine(), dir, err)
}
_, id, err := arch.Snapshot(newArchiveProgress(gopts, stat), target, opts.Tags, parentSnapshotID)
_, id, err := arch.Snapshot(newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID)
if err != nil {
return err
}

View File

@@ -9,13 +9,12 @@ import (
"restic"
"restic/backend"
"restic/debug"
"restic/errors"
"restic/repository"
)
var cmdCat = &cobra.Command{
Use: "cat [flags] [pack|blob|tree|snapshot|key|masterkey|config|lock] ID",
Use: "cat [flags] [pack|blob|snapshot|index|key|masterkey|config|lock] ID",
Short: "print internal objects to stdout",
Long: `
The "cat" command is used to print internal objects to stdout.
@@ -31,7 +30,7 @@ func init() {
func runCat(gopts GlobalOptions, args []string) error {
if len(args) < 1 || (args[0] != "masterkey" && args[0] != "config" && len(args) != 2) {
return errors.Fatalf("type or ID not specified")
return errors.Fatal("type or ID not specified")
}
repo, err := OpenRepository(gopts)
@@ -99,7 +98,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, nil)
buf, err := backend.LoadAll(repo.Backend(), h)
if err != nil {
return err
}
@@ -150,7 +149,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, nil)
buf, err := backend.LoadAll(repo.Backend(), h)
if err != nil {
return err
}
@@ -172,7 +171,7 @@ func runCat(gopts GlobalOptions, args []string) error {
blob := list[0]
buf := make([]byte, blob.Length)
n, err := repo.LoadBlob(restic.DataBlob, id, buf)
n, err := repo.LoadBlob(t, id, buf)
if err != nil {
return err
}
@@ -184,23 +183,6 @@ func runCat(gopts GlobalOptions, args []string) error {
return errors.Fatal("blob not found")
case "tree":
debug.Log("cat tree %v", id.Str())
tree, err := repo.LoadTree(id)
if err != nil {
debug.Log("unable to load tree %v: %v", id.Str(), err)
return err
}
buf, err := json.MarshalIndent(&tree, "", " ")
if err != nil {
debug.Log("error json.MarshalIndent(): %v", err)
return err
}
_, err = os.Stdout.Write(append(buf, '\n'))
return nil
default:
return errors.Fatal("invalid type")
}

View File

@@ -7,8 +7,6 @@ import (
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"restic"
"restic/checker"
"restic/errors"
@@ -26,7 +24,7 @@ finds. It can also be used to read all data and therefore simulate a restore.
},
}
// CheckOptions bundle all options for the 'check' command.
// CheckOptions bundles all options for the 'check' command.
type CheckOptions struct {
ReadData bool
CheckUnused bool
@@ -38,8 +36,8 @@ func init() {
cmdRoot.AddCommand(cmdCheck)
f := cmdCheck.Flags()
f.BoolVar(&checkOptions.ReadData, "read-data", false, "Read all data blobs")
f.BoolVar(&checkOptions.CheckUnused, "check-unused", false, "Find unused blobs")
f.BoolVar(&checkOptions.ReadData, "read-data", false, "read all data blobs")
f.BoolVar(&checkOptions.CheckUnused, "check-unused", false, "find unused blobs")
}
func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
@@ -55,8 +53,7 @@ func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
formatPercent(s.Blobs, todo.Blobs),
s.Blobs, todo.Blobs)
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
if err == nil {
if w := stdoutTerminalWidth(); w > 0 {
if len(status) > w {
max := w - len(status) - 4
status = status[:max] + "... "

View File

@@ -22,7 +22,7 @@ var cmdDump = &cobra.Command{
Use: "dump [indexes|snapshots|trees|all|packs]",
Short: "dump data structures",
Long: `
The "dump" command dumps data structures from a repository as JSON objects. It
The "dump" command dumps data structures from the repository as JSON objects. It
is used for debugging purposes only.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runDump(globalOptions, args)
@@ -168,7 +168,7 @@ func dumpIndexes(repo restic.Repository) error {
func runDump(gopts GlobalOptions, args []string) error {
if len(args) != 1 {
return errors.Fatalf("type not specified")
return errors.Fatal("type not specified")
}
repo, err := OpenRepository(gopts)

View File

@@ -1,7 +1,10 @@
package main
import (
"context"
"encoding/json"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
@@ -23,11 +26,16 @@ repo. `,
},
}
// FindOptions bundle all options for the find command.
// FindOptions bundles all options for the find command.
type FindOptions struct {
Oldest string
Newest string
Snapshot string
Oldest string
Newest string
Snapshots []string
CaseInsensitive bool
ListLong bool
Host string
Paths []string
Tags []string
}
var findOptions FindOptions
@@ -36,19 +44,21 @@ func init() {
cmdRoot.AddCommand(cmdFind)
f := cmdFind.Flags()
f.StringVarP(&findOptions.Oldest, "oldest", "o", "", "Oldest modification date/time")
f.StringVarP(&findOptions.Newest, "newest", "n", "", "Newest modification date/time")
f.StringVarP(&findOptions.Snapshot, "snapshot", "s", "", "Snapshot ID to search in")
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time")
f.StringSliceVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
f.StringVarP(&findOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
f.StringSliceVar(&findOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given")
f.StringSliceVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
}
type findPattern struct {
oldest, newest time.Time
pattern string
}
type findResult struct {
node *restic.Node
path string
ignoreCase bool
}
var timeFormats = []string{
@@ -75,20 +85,112 @@ func parseTime(str string) (time.Time, error) {
return time.Time{}, errors.Fatalf("unable to parse time: %q", str)
}
func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path string) ([]findResult, error) {
type statefulOutput struct {
ListLong bool
JSON bool
inuse bool
newsn *restic.Snapshot
oldsn *restic.Snapshot
hits int
}
func (s *statefulOutput) PrintJSON(prefix string, node *restic.Node) {
type findNode restic.Node
b, err := json.Marshal(struct {
// Add these attributes
Path string `json:"path,omitempty"`
Permissions string `json:"permissions,omitempty"`
*findNode
// Make the following attributes disappear
Name byte `json:"name,omitempty"`
Inode byte `json:"inode,omitempty"`
ExtendedAttributes byte `json:"extended_attributes,omitempty"`
Device byte `json:"device,omitempty"`
Content byte `json:"content,omitempty"`
Subtree byte `json:"subtree,omitempty"`
}{
Path: filepath.Join(prefix, node.Name),
Permissions: node.Mode.String(),
findNode: (*findNode)(node),
})
if err != nil {
Warnf("Marshall failed: %v\n", err)
return
}
if !s.inuse {
Printf("[")
s.inuse = true
}
if s.newsn != s.oldsn {
if s.oldsn != nil {
Printf("],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID())
}
Printf(`{"matches":[`)
s.oldsn = s.newsn
s.hits = 0
}
if s.hits > 0 {
Printf(",")
}
Printf(string(b))
s.hits++
}
func (s *statefulOutput) PrintNormal(prefix string, node *restic.Node) {
if s.newsn != s.oldsn {
if s.oldsn != nil {
Verbosef("\n")
}
s.oldsn = s.newsn
Verbosef("Found matching entries in snapshot %s\n", s.oldsn.ID())
}
Printf(formatNode(prefix, node, s.ListLong) + "\n")
}
func (s *statefulOutput) Print(prefix string, node *restic.Node) {
if s.JSON {
s.PrintJSON(prefix, node)
} else {
s.PrintNormal(prefix, node)
}
}
func (s *statefulOutput) Finish() {
if s.JSON {
// do some finishing up
if s.oldsn != nil {
Printf("],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID())
}
if s.inuse {
Printf("]\n")
} else {
Printf("[]\n")
}
return
}
}
func findInTree(repo *repository.Repository, pat *findPattern, id restic.ID, prefix string, state *statefulOutput) error {
debug.Log("checking tree %v\n", id)
tree, err := repo.LoadTree(id)
if err != nil {
return nil, err
return err
}
results := []findResult{}
for _, node := range tree.Nodes {
debug.Log(" testing entry %q\n", node.Name)
m, err := filepath.Match(pat.pattern, node.Name)
name := node.Name
if pat.ignoreCase {
name = strings.ToLower(name)
}
m, err := filepath.Match(pat.pattern, name)
if err != nil {
return nil, err
return err
}
if m {
@@ -103,69 +205,51 @@ func findInTree(repo *repository.Repository, pat findPattern, id restic.ID, path
continue
}
results = append(results, findResult{node: node, path: path})
state.Print(prefix, node)
} else {
debug.Log(" pattern does not match\n")
}
if node.Type == "dir" {
subdirResults, err := findInTree(repo, pat, *node.Subtree, filepath.Join(path, node.Name))
if err != nil {
return nil, err
if err := findInTree(repo, pat, *node.Subtree, filepath.Join(prefix, node.Name), state); err != nil {
return err
}
results = append(results, subdirResults...)
}
}
return results, nil
}
func findInSnapshot(repo *repository.Repository, pat findPattern, id restic.ID) error {
debug.Log("searching in snapshot %s\n for entries within [%s %s]", id.Str(), pat.oldest, pat.newest)
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
return err
}
results, err := findInTree(repo, pat, *sn.Tree, "")
if err != nil {
return err
}
if len(results) == 0 {
return nil
}
Verbosef("found %d matching entries in snapshot %s\n", len(results), id)
for _, res := range results {
res.node.Name = filepath.Join(res.path, res.node.Name)
Printf(" %s\n", res.node)
}
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)
state.newsn = sn
if err := findInTree(repo, &pat, *sn.Tree, string(filepath.Separator), state); err != nil {
return err
}
return nil
}
func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
if len(args) != 1 {
return errors.Fatalf("wrong number of arguments")
return errors.Fatal("wrong number of arguments")
}
var (
err error
pat findPattern
)
var err error
pat := findPattern{pattern: args[0]}
if opts.CaseInsensitive {
pat.pattern = strings.ToLower(pat.pattern)
pat.ignoreCase = true
}
if opts.Oldest != "" {
pat.oldest, err = parseTime(opts.Oldest)
if err != nil {
if pat.oldest, err = parseTime(opts.Oldest); err != nil {
return err
}
}
if opts.Newest != "" {
pat.newest, err = parseTime(opts.Newest)
if err != nil {
if pat.newest, err = parseTime(opts.Newest); err != nil {
return err
}
}
@@ -183,31 +267,19 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
}
}
err = repo.LoadIndex()
if err != nil {
if err = repo.LoadIndex(); err != nil {
return err
}
pat.pattern = args[0]
if opts.Snapshot != "" {
snapshotID, err := restic.FindSnapshot(repo, opts.Snapshot)
if err != nil {
return errors.Fatalf("invalid id %q: %v", args[1], err)
}
return findInSnapshot(repo, pat, snapshotID)
}
done := make(chan struct{})
defer close(done)
for snapshotID := range repo.List(restic.SnapshotFile, done) {
err := findInSnapshot(repo, pat, snapshotID)
if err != nil {
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
state := statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON}
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
if err = findInSnapshot(repo, sn, pat, &state); err != nil {
return err
}
}
state.Finish()
return nil
}

View File

@@ -1,9 +1,10 @@
package main
import (
"fmt"
"io"
"context"
"encoding/json"
"restic"
"sort"
"strings"
"github.com/spf13/cobra"
@@ -24,19 +25,21 @@ data after 'forget' was run successfully, see the 'prune' command. `,
// ForgetOptions collects all options for the forget command.
type ForgetOptions struct {
Last int
Hourly int
Daily int
Weekly int
Monthly int
Yearly int
Last int
Hourly int
Daily int
Weekly int
Monthly int
Yearly int
KeepTags []string
Hostname string
Tags []string
Host string
Tags []string
Paths []string
DryRun bool
GroupByTags bool
DryRun bool
Prune bool
}
var forgetOptions ForgetOptions
@@ -52,51 +55,19 @@ func init() {
f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots")
f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
f.StringSliceVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "always keep snapshots with this `tag` (can be specified multiple times)")
f.StringVar(&forgetOptions.Hostname, "hostname", "", "only forget snapshots for the given hostname")
f.StringSliceVar(&forgetOptions.Tags, "tag", []string{}, "only forget snapshots with the `tag` (can be specified multiple times)")
f.StringSliceVar(&forgetOptions.KeepTags, "keep-tag", []string{}, "keep snapshots with this `tag` (can be specified multiple times)")
f.BoolVarP(&forgetOptions.GroupByTags, "group-by-tags", "G", false, "Group by host,paths,tags instead of just host,paths")
// Sadly the commonly used shortcut `H` is already used.
f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
// Deprecated since 2017-03-07.
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname` (deprecated)")
f.StringSliceVar(&forgetOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)")
f.StringSliceVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
}
f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
func printSnapshots(w io.Writer, snapshots restic.Snapshots) {
tab := NewTable()
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %-10s %s", "ID", "Date", "Host", "Tags", "Directory")
tab.RowFormat = "%-8s %-19s %-10s %-10s %s"
for _, sn := range snapshots {
if len(sn.Paths) == 0 {
continue
}
firstTag := ""
if len(sn.Tags) > 0 {
firstTag = sn.Tags[0]
}
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, sn.Paths[0]})
rows := len(sn.Paths)
if len(sn.Tags) > rows {
rows = len(sn.Tags)
}
for i := 1; i < rows; i++ {
path := ""
if len(sn.Paths) > i {
path = sn.Paths[i]
}
tag := ""
if len(sn.Tags) > i {
tag = sn.Tags[i]
}
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, path})
}
}
tab.Write(w)
f.SortFlags = false
}
func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
@@ -111,29 +82,45 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
return err
}
err = repo.LoadIndex()
if err != nil {
return err
// group by hostname and dirs
type key struct {
Hostname string
Paths []string
Tags []string
}
snapshotGroups := make(map[string]restic.Snapshots)
// first, process all snapshot IDs given as arguments
for _, s := range args {
id, err := restic.FindSnapshot(repo, s)
if err != nil {
return err
}
if !opts.DryRun {
err = repo.Backend().Remove(restic.SnapshotFile, id.String())
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
if len(args) > 0 {
// 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 {
return err
}
Verbosef("removed snapshot %v\n", sn.ID().Str())
} else {
Verbosef("would have removed snapshot %v\n", sn.ID().Str())
}
} else {
var tags []string
if opts.GroupByTags {
tags = sn.Tags
sort.StringSlice(tags).Sort()
}
sort.StringSlice(sn.Paths).Sort()
k, err := json.Marshal(key{Hostname: sn.Hostname, Tags: tags, Paths: sn.Paths})
if err != nil {
return err
}
Verbosef("removed snapshot %v\n", id.Str())
} else {
Verbosef("would removed snapshot %v\n", id.Str())
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
}
}
if len(args) > 0 {
return nil
}
policy := restic.ExpirePolicy{
Last: opts.Last,
@@ -146,53 +133,41 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
}
if policy.Empty() {
Verbosef("no policy was specified, no snapshots will be removed\n")
return nil
}
// then, load all remaining snapshots
snapshots, err := restic.LoadAllSnapshots(repo)
if err != nil {
return err
}
// group by hostname and dirs
type key struct {
Hostname string
Dirs string
}
snapshotGroups := make(map[key]restic.Snapshots)
for _, sn := range snapshots {
if opts.Hostname != "" && sn.Hostname != opts.Hostname {
continue
removeSnapshots := 0
for k, snapshotGroup := range snapshotGroups {
var key key
if json.Unmarshal([]byte(k), &key) != nil {
return err
}
if !sn.HasTags(opts.Tags) {
continue
if opts.GroupByTags {
Verbosef("snapshots for host %v, tags [%v], paths: [%v]:\n\n", key.Hostname, strings.Join(key.Tags, ", "), strings.Join(key.Paths, ", "))
} else {
Verbosef("snapshots for host %v, paths: [%v]:\n\n", key.Hostname, strings.Join(key.Paths, ", "))
}
k := key{Hostname: sn.Hostname, Dirs: strings.Join(sn.Paths, ":")}
list := snapshotGroups[k]
list = append(list, sn)
snapshotGroups[k] = list
}
for key, snapshotGroup := range snapshotGroups {
Printf("snapshots for host %v, directories %v:\n\n", key.Hostname, key.Dirs)
keep, remove := restic.ApplyPolicy(snapshotGroup, policy)
Printf("keep %d snapshots:\n", len(keep))
printSnapshots(globalOptions.stdout, keep)
Printf("\n")
if len(keep) != 0 && !gopts.Quiet {
Printf("keep %d snapshots:\n", len(keep))
PrintSnapshots(globalOptions.stdout, keep)
Printf("\n")
}
Printf("remove %d snapshots:\n", len(remove))
printSnapshots(globalOptions.stdout, remove)
Printf("\n")
if len(remove) != 0 && !gopts.Quiet {
Printf("remove %d snapshots:\n", len(remove))
PrintSnapshots(globalOptions.stdout, remove)
Printf("\n")
}
removeSnapshots += len(remove)
if !opts.DryRun {
for _, sn := range remove {
err = repo.Backend().Remove(restic.SnapshotFile, sn.ID().String())
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
err = repo.Backend().Remove(h)
if err != nil {
return err
}
@@ -200,5 +175,12 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
}
}
if removeSnapshots > 0 && opts.Prune {
Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
if !opts.DryRun {
return pruneRepository(gopts, repo)
}
}
return nil
}

View File

@@ -27,7 +27,7 @@ func runInit(gopts GlobalOptions, args []string) error {
return errors.Fatal("Please specify repository location (-r)")
}
be, err := create(gopts.Repo)
be, err := create(gopts.Repo, gopts.extended)
if err != nil {
return errors.Fatalf("create backend at %s failed: %v\n", gopts.Repo, err)
}

View File

@@ -1,20 +1,20 @@
package main
import (
"context"
"fmt"
"restic"
"github.com/spf13/cobra"
"restic/errors"
"restic/repository"
"github.com/spf13/cobra"
)
var cmdKey = &cobra.Command{
Use: "key [list|add|rm|passwd] [ID]",
Short: "manage keys (passwords)",
Long: `
The "key" command manages keys (passwords) for accessing a repository.
The "key" command manages keys (passwords) for accessing the repository.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runKey(globalOptions, args)
@@ -25,15 +25,12 @@ func init() {
cmdRoot.AddCommand(cmdKey)
}
func listKeys(s *repository.Repository) error {
func listKeys(ctx context.Context, s *repository.Repository) error {
tab := NewTable()
tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created")
tab.RowFormat = "%s%-10s %-10s %-10s %s"
done := make(chan struct{})
defer close(done)
for id := range s.List(restic.KeyFile, done) {
for id := range s.List(restic.KeyFile, ctx.Done()) {
k, err := repository.LoadKey(s, id.String())
if err != nil {
Warnf("LoadKey() failed: %v\n", err)
@@ -87,7 +84,8 @@ func deleteKey(repo *repository.Repository, name string) error {
return errors.Fatal("refusing to remove key currently used to access repository")
}
err := repo.Backend().Remove(restic.KeyFile, name)
h := restic.Handle{Type: restic.KeyFile, Name: name}
err := repo.Backend().Remove(h)
if err != nil {
return err
}
@@ -107,7 +105,8 @@ func changePassword(gopts GlobalOptions, repo *repository.Repository) error {
return errors.Fatalf("creating new key failed: %v\n", err)
}
err = repo.Backend().Remove(restic.KeyFile, repo.KeyName())
h := restic.Handle{Type: restic.KeyFile, Name: repo.KeyName()}
err = repo.Backend().Remove(h)
if err != nil {
return err
}
@@ -118,10 +117,13 @@ func changePassword(gopts GlobalOptions, repo *repository.Repository) error {
}
func runKey(gopts GlobalOptions, args []string) error {
if len(args) < 1 || (args[0] == "rm" && len(args) != 2) {
return errors.Fatalf("wrong number of arguments")
if len(args) < 1 || (args[0] == "rm" && len(args) != 2) || (args[0] != "rm" && len(args) != 1) {
return errors.Fatal("wrong number of arguments")
}
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
repo, err := OpenRepository(gopts)
if err != nil {
return err
@@ -135,7 +137,7 @@ func runKey(gopts GlobalOptions, args []string) error {
return err
}
return listKeys(repo)
return listKeys(ctx, repo)
case "add":
lock, err := lockRepo(repo)
defer unlockRepo(lock)

View File

@@ -1,17 +1,19 @@
package main
import (
"fmt"
"restic"
"restic/errors"
"restic/index"
"github.com/spf13/cobra"
)
var cmdList = &cobra.Command{
Use: "list [blobs|packs|index|snapshots|keys|locks]",
Short: "list items in the repository",
Short: "list objects in the repository",
Long: `
The "list" command allows listing objects in the repository based on type.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runList(globalOptions, args)
@@ -24,7 +26,7 @@ func init() {
func runList(opts GlobalOptions, args []string) error {
if len(args) != 1 {
return errors.Fatalf("type not specified")
return errors.Fatal("type not specified")
}
repo, err := OpenRepository(opts)
@@ -52,6 +54,19 @@ func runList(opts GlobalOptions, args []string) error {
t = restic.KeyFile
case "locks":
t = restic.LockFile
case "blobs":
idx, err := index.Load(repo, nil)
if err != nil {
return err
}
for _, pack := range idx.Packs {
for _, entry := range pack.Entries {
fmt.Printf("%v %v\n", entry.Type, entry.ID)
}
}
return nil
default:
return errors.Fatal("invalid type")
}

View File

@@ -1,8 +1,7 @@
package main
import (
"fmt"
"os"
"context"
"path/filepath"
"github.com/spf13/cobra"
@@ -13,56 +12,50 @@ import (
)
var cmdLs = &cobra.Command{
Use: "ls [flags] snapshot-ID",
Use: "ls [flags] [snapshot-ID ...]",
Short: "list files in a snapshot",
Long: `
The "ls" command allows listing files and directories in a snapshot.
The special snapshot-ID "latest" can be used to list files and directories of the latest snapshot in the repository.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runLs(globalOptions, args)
return runLs(lsOptions, globalOptions, args)
},
}
var listLong bool
// LsOptions collects all options for the ls command.
type LsOptions struct {
ListLong bool
Host string
Tags []string
Paths []string
}
var lsOptions LsOptions
func init() {
cmdRoot.AddCommand(cmdLs)
cmdLs.Flags().BoolVarP(&listLong, "long", "l", false, "use a long listing format showing size and mode")
flags := cmdLs.Flags()
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
flags.StringSliceVar(&lsOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot ID is given")
flags.StringSliceVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
}
func printNode(prefix string, n *restic.Node) string {
if !listLong {
return filepath.Join(prefix, n.Name)
}
switch n.Type {
case "file":
return fmt.Sprintf("%s %5d %5d %6d %s %s",
n.Mode, n.UID, n.GID, n.Size, n.ModTime, filepath.Join(prefix, n.Name))
case "dir":
return fmt.Sprintf("%s %5d %5d %6d %s %s",
n.Mode|os.ModeDir, n.UID, n.GID, n.Size, n.ModTime, filepath.Join(prefix, n.Name))
case "symlink":
return fmt.Sprintf("%s %5d %5d %6d %s %s -> %s",
n.Mode|os.ModeSymlink, n.UID, n.GID, n.Size, n.ModTime, filepath.Join(prefix, n.Name), n.LinkTarget)
default:
return fmt.Sprintf("<Node(%s) %s>", n.Type, n.Name)
}
}
func printTree(prefix string, repo *repository.Repository, id restic.ID) error {
tree, err := repo.LoadTree(id)
func printTree(repo *repository.Repository, id *restic.ID, prefix string) error {
tree, err := repo.LoadTree(*id)
if err != nil {
return err
}
for _, entry := range tree.Nodes {
Printf(printNode(prefix, entry) + "\n")
Printf(formatNode(prefix, entry, lsOptions.ListLong) + "\n")
if entry.Type == "dir" && entry.Subtree != nil {
err = printTree(filepath.Join(prefix, entry.Name), repo, *entry.Subtree)
if err != nil {
if err = printTree(repo, entry.Subtree, filepath.Join(prefix, entry.Name)); err != nil {
return err
}
}
@@ -71,9 +64,9 @@ func printTree(prefix string, repo *repository.Repository, id restic.ID) error {
return nil
}
func runLs(gopts GlobalOptions, args []string) error {
if len(args) < 1 || len(args) > 2 {
return errors.Fatalf("no snapshot ID given")
func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
if len(args) == 0 && opts.Host == "" && len(opts.Tags) == 0 && len(opts.Paths) == 0 {
return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
}
repo, err := OpenRepository(gopts)
@@ -81,22 +74,18 @@ func runLs(gopts GlobalOptions, args []string) error {
return err
}
err = repo.LoadIndex()
if err != nil {
if err = repo.LoadIndex(); err != nil {
return err
}
id, err := restic.FindSnapshot(repo, args[0])
if err != nil {
return err
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time)
if err = printTree(repo, sn.Tree, string(filepath.Separator)); err != nil {
return err
}
}
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
return err
}
Verbosef("snapshot of %v at %s:\n", sn.Paths, sn.Time)
return printTree("", repo, *sn.Tree)
return nil
}

View File

@@ -32,7 +32,12 @@ read-only mount.
// MountOptions collects all options for the mount command.
type MountOptions struct {
OwnerRoot bool
OwnerRoot bool
AllowRoot bool
AllowOther bool
Host string
Tags []string
Paths []string
}
var mountOptions MountOptions
@@ -40,7 +45,14 @@ var mountOptions MountOptions
func init() {
cmdRoot.AddCommand(cmdMount)
cmdMount.Flags().BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
mountFlags := cmdMount.Flags()
mountFlags.BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
mountFlags.BoolVar(&mountOptions.AllowRoot, "allow-root", false, "allow root user to access the data in the mounted directory")
mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory")
mountFlags.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`)
mountFlags.StringSliceVar(&mountOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`")
mountFlags.StringSliceVar(&mountOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`")
}
func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
@@ -64,11 +76,21 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
return err
}
}
c, err := systemFuse.Mount(
mountpoint,
mountOptions := []systemFuse.MountOption{
systemFuse.ReadOnly(),
systemFuse.FSName("restic"),
)
}
if opts.AllowRoot {
mountOptions = append(mountOptions, systemFuse.AllowRoot())
}
if opts.AllowOther {
mountOptions = append(mountOptions, systemFuse.AllowOther())
}
c, err := systemFuse.Mount(mountpoint, mountOptions...)
if err != nil {
return err
}
@@ -77,7 +99,7 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
Printf("Don't forget to umount after quitting!\n")
root := fs.Tree{}
root.Add("snapshots", fuse.NewSnapshotsDir(repo, opts.OwnerRoot))
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)
@@ -95,7 +117,7 @@ func umount(mountpoint string) error {
func runMount(opts MountOptions, gopts GlobalOptions, args []string) error {
if len(args) == 0 {
return errors.Fatalf("wrong number of parameters")
return errors.Fatal("wrong number of parameters")
}
mountpoint := args[0]

View File

@@ -0,0 +1,27 @@
package main
import (
"fmt"
"restic/options"
"github.com/spf13/cobra"
)
var optionsCmd = &cobra.Command{
Use: "options",
Short: "print list of extended options",
Long: `
The "options" command prints a list of extended options.
`,
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("All Extended Options:\n")
for _, opt := range options.List() {
fmt.Printf(" %-15s %s\n", opt.Namespace+"."+opt.Name, opt.Text)
}
},
}
func init() {
cmdRoot.AddCommand(optionsCmd)
}

View File

@@ -1,8 +1,8 @@
package main
import (
"context"
"fmt"
"os"
"restic"
"restic/debug"
"restic/errors"
@@ -11,8 +11,6 @@ import (
"time"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
var cmdPrune = &cobra.Command{
@@ -45,8 +43,7 @@ func newProgressMax(show bool, max uint64, description string) *restic.Progress
formatPercent(s.Blobs, max),
s.Blobs, max, description)
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
if err == nil {
if w := stdoutTerminalWidth(); w > 0 {
if len(status) > w {
max := w - len(status) - 4
status = status[:max] + "... "
@@ -75,13 +72,17 @@ func runPrune(gopts GlobalOptions) error {
return err
}
err = repo.LoadIndex()
return pruneRepository(gopts, repo)
}
func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
err := repo.LoadIndex()
if err != nil {
return err
}
done := make(chan struct{})
defer close(done)
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
var stats struct {
blobs int
@@ -91,7 +92,7 @@ func runPrune(gopts GlobalOptions) error {
}
Verbosef("counting files in repo\n")
for _ = range repo.List(restic.DataFile, done) {
for range repo.List(restic.DataFile, ctx.Done()) {
stats.packs++
}
@@ -103,11 +104,13 @@ func runPrune(gopts GlobalOptions) error {
return err
}
blobs := 0
for _, pack := range idx.Packs {
stats.bytes += pack.Size
blobs += len(pack.Entries)
}
Verbosef("repository contains %v packs (%v blobs) with %v bytes\n",
len(idx.Packs), len(idx.Blobs), formatBytes(uint64(stats.bytes)))
len(idx.Packs), blobs, formatBytes(uint64(stats.bytes)))
blobCount := make(map[restic.BlobHandle]int)
duplicateBlobs := 0
@@ -164,18 +167,21 @@ func runPrune(gopts GlobalOptions) error {
// find packs that need a rewrite
rewritePacks := restic.NewIDSet()
for h, blob := range idx.Blobs {
if !usedBlobs.Has(h) {
rewritePacks.Merge(blob.Packs)
continue
}
for _, pack := range idx.Packs {
for _, blob := range pack.Entries {
h := restic.BlobHandle{ID: blob.ID, Type: blob.Type}
if !usedBlobs.Has(h) {
rewritePacks.Insert(pack.ID)
continue
}
if blobCount[h] > 1 {
rewritePacks.Merge(blob.Packs)
if blobCount[h] > 1 {
rewritePacks.Insert(pack.ID)
}
}
}
removeBytes := 0
removeBytes := duplicateBytes
// find packs that are unneeded
removePacks := restic.NewIDSet()
@@ -208,46 +214,34 @@ func runPrune(gopts GlobalOptions) error {
Verbosef("will delete %d packs and rewrite %d packs, this frees %s\n",
len(removePacks), len(rewritePacks), formatBytes(uint64(removeBytes)))
err = repository.Repack(repo, rewritePacks, usedBlobs)
if err != nil {
return err
}
for packID := range removePacks {
err = repo.Backend().Remove(restic.DataFile, packID.String())
if len(rewritePacks) != 0 {
bar = newProgressMax(!gopts.Quiet, uint64(len(rewritePacks)), "packs rewritten")
bar.Start()
err = repository.Repack(repo, rewritePacks, usedBlobs, bar)
if err != nil {
Warnf("unable to remove file %v from the repository\n", packID.Str())
return err
}
bar.Done()
}
Verbosef("creating new index\n")
stats.packs = 0
for _ = range repo.List(restic.DataFile, done) {
stats.packs++
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)
if err != nil {
Warnf("unable to remove file %v from the repository\n", packID.Str())
}
bar.Report(restic.Stat{Blobs: 1})
}
bar.Done()
}
bar = newProgressMax(!gopts.Quiet, uint64(stats.packs), "packs")
idx, err = index.New(repo, bar)
if err != nil {
if err = rebuildIndex(ctx, repo); err != nil {
return err
}
var supersedes restic.IDs
for idxID := range repo.List(restic.IndexFile, done) {
err := repo.Backend().Remove(restic.IndexFile, idxID.String())
if err != nil {
fmt.Fprintf(os.Stderr, "unable to remove index %v: %v\n", idxID.Str(), err)
}
supersedes = append(supersedes, idxID)
}
id, err := idx.Save(repo, supersedes)
if err != nil {
return err
}
Verbosef("saved new index as %v\n", id.Str())
Verbosef("done\n")
return nil
}

View File

@@ -1,7 +1,9 @@
package main
import (
"restic/repository"
"context"
"restic"
"restic/index"
"github.com/spf13/cobra"
)
@@ -10,8 +12,8 @@ var cmdRebuildIndex = &cobra.Command{
Use: "rebuild-index [flags]",
Short: "build a new index file",
Long: `
The "rebuild-index" command creates a new index by combining the index files
into a new one.
The "rebuild-index" command creates a new index based on the pack files in the
repository.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runRebuildIndex(globalOptions)
@@ -34,5 +36,49 @@ func runRebuildIndex(gopts GlobalOptions) error {
return err
}
return repository.RebuildIndex(repo)
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
return rebuildIndex(ctx, repo)
}
func rebuildIndex(ctx context.Context, repo restic.Repository) error {
Verbosef("counting files in repo\n")
var packs uint64
for range repo.List(restic.DataFile, ctx.Done()) {
packs++
}
bar := newProgressMax(!globalOptions.Quiet, packs, "packs")
idx, err := index.New(repo, bar)
if err != nil {
return err
}
Verbosef("finding old index files\n")
var supersedes restic.IDs
for id := range repo.List(restic.IndexFile, ctx.Done()) {
supersedes = append(supersedes, id)
}
id, err := idx.Save(repo, supersedes)
if err != nil {
return err
}
Verbosef("saved new index as %v\n", id.Str())
Verbosef("remove %d old index files\n", len(supersedes))
for _, id := range supersedes {
if err := repo.Backend().Remove(restic.Handle{
Type: restic.IndexFile,
Name: id.String(),
}); err != nil {
Warnf("error removing old index %v: %v\n", id.Str(), err)
}
}
return nil
}

View File

@@ -31,6 +31,7 @@ type RestoreOptions struct {
Target string
Host string
Paths []string
Tags []string
}
var restoreOptions RestoreOptions
@@ -44,12 +45,13 @@ func init() {
flags.StringVarP(&restoreOptions.Target, "target", "t", "", "directory to extract data to")
flags.StringVarP(&restoreOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`)
flags.StringSliceVar(&restoreOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` for snapshot ID \"latest\"")
flags.StringSliceVar(&restoreOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
}
func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
if len(args) != 1 {
return errors.Fatalf("no snapshot ID specified")
return errors.Fatal("no snapshot ID specified")
}
if opts.Target == "" {
@@ -85,7 +87,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.Host)
id, err = restic.FindLatestSnapshot(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)
}
@@ -101,8 +103,10 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
Exitf(2, "creating restorer failed: %v\n", err)
}
totalErrors := 0
res.Error = func(dir string, node *restic.Node, err error) error {
Warnf("error for %s: %+v\n", dir, err)
Warnf("ignoring error for %s: %s\n", dir, err)
totalErrors++
return nil
}
@@ -132,5 +136,9 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
return res.RestoreTo(opts.Target)
err = res.RestoreTo(opts.Target)
if totalErrors > 0 {
Printf("There were %d errors\n", totalErrors)
}
return err
}

View File

@@ -1,9 +1,10 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"restic/errors"
"io"
"sort"
"github.com/spf13/cobra"
@@ -12,19 +13,20 @@ import (
)
var cmdSnapshots = &cobra.Command{
Use: "snapshots",
Use: "snapshots [snapshotID ...]",
Short: "list all snapshots",
Long: `
The "snapshots" command lists all snapshots stored in a repository.
The "snapshots" command lists all snapshots stored in the repository.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runSnapshots(snapshotOptions, globalOptions, args)
},
}
// SnapshotOptions bundle all options for the snapshots command.
// SnapshotOptions bundles all options for the snapshots command.
type SnapshotOptions struct {
Host string
Tags []string
Paths []string
}
@@ -34,15 +36,12 @@ func init() {
cmdRoot.AddCommand(cmdSnapshots)
f := cmdSnapshots.Flags()
f.StringVar(&snapshotOptions.Host, "host", "", "only print snapshots for this host")
f.StringSliceVar(&snapshotOptions.Paths, "path", []string{}, "only print snapshots for this `path` (can be specified multiple times)")
f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`")
f.StringSliceVar(&snapshotOptions.Tags, "tag", nil, "only consider snapshots which include this `tag` (can be specified multiple times)")
f.StringSliceVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)")
}
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
if len(args) != 0 {
return errors.Fatalf("wrong number of arguments")
}
repo, err := OpenRepository(gopts)
if err != nil {
return err
@@ -56,37 +55,47 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
}
}
tab := NewTable()
tab.Header = fmt.Sprintf("%-8s %-19s %-10s %-10s %s", "ID", "Date", "Host", "Tags", "Directory")
tab.RowFormat = "%-8s %-19s %-10s %-10s %s"
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
done := make(chan struct{})
defer close(done)
var list restic.Snapshots
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
list = append(list, sn)
}
sort.Sort(sort.Reverse(list))
list := []*restic.Snapshot{}
for id := range repo.List(restic.SnapshotFile, done) {
sn, err := restic.LoadSnapshot(repo, id)
if gopts.JSON {
err := printSnapshotsJSON(gopts.stdout, list)
if err != nil {
fmt.Fprintf(os.Stderr, "error loading snapshot %s: %v\n", id, err)
continue
Warnf("error printing snapshot: %v\n", err)
}
return nil
}
PrintSnapshots(gopts.stdout, list)
if restic.SamePaths(sn.Paths, opts.Paths) && (opts.Host == "" || opts.Host == sn.Hostname) {
pos := sort.Search(len(list), func(i int) bool {
return list[i].Time.After(sn.Time)
})
return nil
}
if pos < len(list) {
list = append(list, nil)
copy(list[pos+1:], list[pos:])
list[pos] = sn
} else {
list = append(list, sn)
// PrintSnapshots prints a text table of the snapshots in list to stdout.
func PrintSnapshots(stdout io.Writer, list restic.Snapshots) {
// Determine the max widths for host and tag.
maxHost, maxTag := 10, 6
for _, sn := range list {
if len(sn.Hostname) > maxHost {
maxHost = len(sn.Hostname)
}
for _, tag := range sn.Tags {
if len(tag) > maxTag {
maxTag = len(tag)
}
}
}
tab := NewTable()
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s %-3s %s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags", "", "Directory")
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%%ds %%-3s %%s", -maxHost, -maxTag)
for _, sn := range list {
if len(sn.Paths) == 0 {
continue
@@ -97,9 +106,18 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
firstTag = sn.Tags[0]
}
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, sn.Paths[0]})
rows := len(sn.Paths)
if rows < len(sn.Tags) {
rows = len(sn.Tags)
}
treeElement := " "
if rows != 1 {
treeElement = "┌──"
}
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, treeElement, sn.Paths[0]})
if len(sn.Tags) > rows {
rows = len(sn.Tags)
}
@@ -115,11 +133,38 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
tag = sn.Tags[i]
}
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, path})
treeElement := "│"
if i == (rows - 1) {
treeElement = "└──"
}
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, treeElement, path})
}
}
tab.Write(os.Stdout)
return nil
tab.Write(stdout)
}
// Snapshot helps to print Snaphots as JSON with their ID included.
type Snapshot struct {
*restic.Snapshot
ID *restic.ID `json:"id"`
}
// printSnapshotsJSON writes the JSON representation of list to stdout.
func printSnapshotsJSON(stdout io.Writer, list restic.Snapshots) error {
var snapshots []Snapshot
for _, sn := range list {
k := Snapshot{
Snapshot: sn,
ID: sn.ID(),
}
snapshots = append(snapshots, k)
}
return json.NewEncoder(stdout).Encode(snapshots)
}

142
src/cmds/restic/cmd_tag.go Normal file
View File

@@ -0,0 +1,142 @@
package main
import (
"context"
"github.com/spf13/cobra"
"restic"
"restic/debug"
"restic/errors"
"restic/repository"
)
var cmdTag = &cobra.Command{
Use: "tag [flags] [snapshot-ID ...]",
Short: "modifies tags on snapshots",
Long: `
The "tag" command allows you to modify tags on exiting snapshots.
You can either set/replace the entire set of tags on a snapshot, or
add tags to/remove tags from the existing set.
When no snapshot-ID is given, all snapshots matching the host, tag and path filter criteria are modified.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runTag(tagOptions, globalOptions, args)
},
}
// TagOptions bundles all options for the 'tag' command.
type TagOptions struct {
Host string
Paths []string
Tags []string
SetTags []string
AddTags []string
RemoveTags []string
}
var tagOptions TagOptions
func init() {
cmdRoot.AddCommand(cmdTag)
tagFlags := cmdTag.Flags()
tagFlags.StringSliceVar(&tagOptions.SetTags, "set", nil, "`tag` which will replace the existing tags (can be given multiple times)")
tagFlags.StringSliceVar(&tagOptions.AddTags, "add", nil, "`tag` which will be added to the existing tags (can be given multiple times)")
tagFlags.StringSliceVar(&tagOptions.RemoveTags, "remove", nil, "`tag` which will be removed from the existing tags (can be given multiple times)")
tagFlags.StringVarP(&tagOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
tagFlags.StringSliceVar(&tagOptions.Tags, "tag", nil, "only consider snapshots which include this `tag`, when no snapshot-ID is given")
tagFlags.StringSliceVar(&tagOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
}
func changeTags(repo *repository.Repository, sn *restic.Snapshot, setTags, addTags, removeTags []string) (bool, error) {
var changed bool
if len(setTags) != 0 {
// Setting the tag to an empty string really means no tags.
if len(setTags) == 1 && setTags[0] == "" {
setTags = nil
}
sn.Tags = setTags
changed = true
} else {
changed = sn.AddTags(addTags)
if sn.RemoveTags(removeTags) {
changed = true
}
}
if changed {
// Retain the original snapshot id over all tag changes.
if sn.Original == nil {
sn.Original = sn.ID()
}
// Save the new snapshot.
id, err := repo.SaveJSONUnpacked(restic.SnapshotFile, sn)
if err != nil {
return false, err
}
debug.Log("new snapshot saved as %v", id.Str())
if err = repo.Flush(); err != nil {
return false, err
}
// Remove the old snapshot.
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
if err = repo.Backend().Remove(h); err != nil {
return false, err
}
debug.Log("old snapshot %v removed", sn.ID())
}
return changed, nil
}
func runTag(opts TagOptions, gopts GlobalOptions, args []string) error {
if len(opts.SetTags) == 0 && len(opts.AddTags) == 0 && len(opts.RemoveTags) == 0 {
return errors.Fatal("nothing to do!")
}
if len(opts.SetTags) != 0 && (len(opts.AddTags) != 0 || len(opts.RemoveTags) != 0) {
return errors.Fatal("--set and --add/--remove cannot be given at the same time")
}
repo, err := OpenRepository(gopts)
if err != nil {
return err
}
if !gopts.NoLock {
Verbosef("Create exclusive lock for repository\n")
lock, err := lockRepoExclusive(repo)
defer unlockRepo(lock)
if err != nil {
return err
}
}
changeCnt := 0
ctx, cancel := context.WithCancel(gopts.ctx)
defer cancel()
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
changed, err := changeTags(repo, sn, opts.SetTags, opts.AddTags, opts.RemoveTags)
if err != nil {
Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err)
continue
}
if changed {
changeCnt++
}
}
if changeCnt == 0 {
Verbosef("No snapshots were modified\n")
} else {
Verbosef("Modified tags on %v snapshots\n", changeCnt)
}
return nil
}

View File

@@ -27,7 +27,7 @@ var unlockOptions UnlockOptions
func init() {
cmdRoot.AddCommand(unlockCmd)
unlockCmd.Flags().BoolVar(&unlockOptions.RemoveAll, "remove-all", false, "Remove all locks, even non-stale ones")
unlockCmd.Flags().BoolVar(&unlockOptions.RemoveAll, "remove-all", false, "remove all locks, even non-stale ones")
}
func runUnlock(opts UnlockOptions, gopts GlobalOptions) error {

View File

@@ -9,14 +9,14 @@ import (
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version information",
Short: "print version information",
Long: `
The "version" command prints detailed information about the build environment
and the version of this software.
`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("restic %s\ncompiled at %s with %v on %v/%v\n",
version, compiledAt, runtime.Version(), runtime.GOOS, runtime.GOARCH)
fmt.Printf("restic %s\ncompiled with %v on %v/%v\n",
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
},
}

78
src/cmds/restic/find.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"context"
"restic"
"restic/repository"
)
// FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots.
func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, host string, tags []string, paths []string, snapshotIDs []string) <-chan *restic.Snapshot {
out := make(chan *restic.Snapshot)
go func() {
defer close(out)
if len(snapshotIDs) != 0 {
var (
id restic.ID
usedFilter bool
err error
)
ids := make(restic.IDs, 0, len(snapshotIDs))
// Process all snapshot IDs given as arguments.
for _, s := range snapshotIDs {
if s == "latest" {
id, err = restic.FindLatestSnapshot(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
continue
}
} else {
id, err = restic.FindSnapshot(repo, s)
if err != nil {
Warnf("Ignoring %q, it is not a snapshot id\n", s)
continue
}
}
ids = append(ids, id)
}
// Give the user some indication their filters are not used.
if !usedFilter && (host != "" || len(tags) != 0 || len(paths) != 0) {
Warnf("Ignoring filters as there are explicit snapshot ids given\n")
}
for _, id := range ids.Uniq() {
sn, err := restic.LoadSnapshot(repo, id)
if err != nil {
Warnf("Ignoring %q, could not load snapshot: %v\n", id, err)
continue
}
select {
case <-ctx.Done():
return
case out <- sn:
}
}
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
}
select {
case <-ctx.Done():
return
case out <- sn:
}
}
}()
return out
}

View File

@@ -0,0 +1,28 @@
package main
import (
"io/ioutil"
"testing"
)
// 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)
err := cmd.ParseFlags([]string{"--help"})
if err.Error() == "pflag: help requested" {
err = nil
}
if err != nil {
t.Fatal(err)
}
})
}
}

View File

@@ -2,7 +2,11 @@ package main
import (
"fmt"
"os"
"path/filepath"
"time"
"restic"
)
func formatBytes(c uint64) string {
@@ -58,3 +62,23 @@ func formatDuration(d time.Duration) string {
sec := uint64(d / time.Second)
return formatSeconds(sec)
}
func formatNode(prefix string, n *restic.Node, long bool) string {
if !long {
return filepath.Join(prefix, n.Name)
}
switch n.Type {
case "file":
return fmt.Sprintf("%s %5d %5d %6d %s %s",
n.Mode, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name))
case "dir":
return fmt.Sprintf("%s %5d %5d %6d %s %s",
n.Mode|os.ModeDir, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name))
case "symlink":
return fmt.Sprintf("%s %5d %5d %6d %s %s -> %s",
n.Mode|os.ModeSymlink, n.UID, n.GID, n.Size, n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name), n.LinkTarget)
default:
return fmt.Sprintf("<Node(%s) %s>", n.Type, n.Name)
}
}

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"io"
"io/ioutil"
@@ -10,14 +11,13 @@ import (
"strings"
"syscall"
"github.com/spf13/cobra"
"restic/backend/local"
"restic/backend/location"
"restic/backend/rest"
"restic/backend/s3"
"restic/backend/sftp"
"restic/debug"
"restic/location"
"restic/options"
"restic/repository"
"restic/errors"
@@ -26,19 +26,6 @@ import (
)
var version = "compiled manually"
var compiledAt = "unknown time"
func parseEnvironment(cmd *cobra.Command, args []string) {
repo := os.Getenv("RESTIC_REPOSITORY")
if repo != "" {
globalOptions.Repo = repo
}
pw := os.Getenv("RESTIC_PASSWORD")
if pw != "" {
globalOptions.password = pw
}
}
// GlobalOptions hold all global options for restic.
type GlobalOptions struct {
@@ -46,10 +33,16 @@ type GlobalOptions struct {
PasswordFile string
Quiet bool
NoLock bool
JSON bool
ctx context.Context
password string
stdout io.Writer
stderr io.Writer
Options []string
extended options.Options
}
var globalOptions = GlobalOptions{
@@ -58,11 +51,26 @@ var globalOptions = GlobalOptions{
}
func init() {
pw := os.Getenv("RESTIC_PASSWORD")
if pw != "" {
globalOptions.password = pw
}
var cancel context.CancelFunc
globalOptions.ctx, cancel = context.WithCancel(context.Background())
AddCleanupHandler(func() error {
cancel()
return nil
})
f := cmdRoot.PersistentFlags()
f.StringVarP(&globalOptions.Repo, "repo", "r", "", "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", "", "read the repository password from a file")
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not outputcomprehensive progress report")
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
f.StringSliceVarP(&globalOptions.Options, "option", "o", []string{}, "set extended option (`key=value`, can be specified multiple times)")
restoreTerminal()
}
@@ -90,6 +98,14 @@ func stdoutIsTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}
func stdoutTerminalWidth() int {
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
if err != nil {
return 0
}
return w
}
// restoreTerminal installs a cleanup handler that restores the previous
// terminal state on exit.
func restoreTerminal() {
@@ -118,8 +134,7 @@ func restoreTerminal() {
// current windows cmd shell.
func ClearLine() string {
if runtime.GOOS == "windows" {
w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
if err == nil {
if w := stdoutTerminalWidth(); w > 0 {
return strings.Repeat(" ", w-1) + "\r"
}
return ""
@@ -132,7 +147,7 @@ func Printf(format string, args ...interface{}) {
_, err := fmt.Fprintf(globalOptions.stdout, format, args...)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
os.Exit(100)
Exit(100)
}
}
@@ -175,18 +190,19 @@ func Warnf(format string, args ...interface{}) {
_, err := fmt.Fprintf(globalOptions.stderr, format, args...)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err)
os.Exit(100)
Exit(100)
}
}
// Exitf uses Warnf to write the message and then calls os.Exit(exitcode).
// Exitf uses Warnf to write the message and then terminates the process with
// the given exit code.
func Exitf(exitcode int, format string, args ...interface{}) {
if format[len(format)-1] != '\n' {
format += "\n"
}
Warnf(format, args...)
os.Exit(exitcode)
Exit(exitcode)
}
// readPassword reads the password from the given reader directly.
@@ -278,7 +294,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
return nil, errors.Fatal("Please specify repository location (-r)")
}
be, err := open(opts.Repo)
be, err := open(opts.Repo, opts.extended)
if err != nil {
return nil, err
}
@@ -300,8 +316,61 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
return s, nil
}
func parseConfig(loc location.Location, opts options.Options) (interface{}, error) {
// only apply options for a particular backend here
opts = opts.Extract(loc.Scheme)
switch loc.Scheme {
case "local":
cfg := loc.Config.(local.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening local repository at %#v", cfg)
return cfg, nil
case "sftp":
cfg := loc.Config.(sftp.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening sftp repository at %#v", cfg)
return cfg, nil
case "s3":
cfg := loc.Config.(s3.Config)
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv("AWS_ACCESS_KEY_ID")
}
if cfg.Secret == "" {
cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening s3 repository at %#v", cfg)
return cfg, nil
case "rest":
cfg := loc.Config.(rest.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening rest repository at %#v", cfg)
return cfg, nil
}
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
}
// Open the backend specified by a location config.
func open(s string) (restic.Backend, error) {
func open(s string, opts options.Options) (restic.Backend, error) {
debug.Log("parsing location %v", s)
loc, err := location.Parse(s)
if err != nil {
@@ -310,27 +379,21 @@ func open(s string) (restic.Backend, error) {
var be restic.Backend
cfg, err := parseConfig(loc, opts)
if err != nil {
return nil, err
}
switch loc.Scheme {
case "local":
debug.Log("opening local repository at %#v", loc.Config)
be, err = local.Open(loc.Config.(string))
be, err = local.Open(cfg.(local.Config))
case "sftp":
debug.Log("opening sftp repository at %#v", loc.Config)
be, err = sftp.OpenWithConfig(loc.Config.(sftp.Config))
be, err = sftp.Open(cfg.(sftp.Config))
case "s3":
cfg := loc.Config.(s3.Config)
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv("AWS_ACCESS_KEY_ID")
}
if cfg.Secret == "" {
cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
debug.Log("opening s3 repository at %#v", cfg)
be, err = s3.Open(cfg)
be, err = s3.Open(cfg.(s3.Config))
case "rest":
be, err = rest.Open(loc.Config.(rest.Config))
be, err = rest.Open(cfg.(rest.Config))
default:
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
}
@@ -339,38 +402,41 @@ func open(s string) (restic.Backend, error) {
return nil, errors.Fatalf("unable to open repo at %v: %v", s, err)
}
// check if config is there
fi, err := be.Stat(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)
}
if fi.Size == 0 {
return nil, errors.New("config file has zero size, invalid repository?")
}
return be, nil
}
// Create the backend specified by URI.
func create(s string) (restic.Backend, error) {
func create(s string, opts options.Options) (restic.Backend, error) {
debug.Log("parsing location %v", s)
loc, err := location.Parse(s)
if err != nil {
return nil, err
}
cfg, err := parseConfig(loc, opts)
if err != nil {
return nil, err
}
switch loc.Scheme {
case "local":
debug.Log("create local repository at %#v", loc.Config)
return local.Create(loc.Config.(string))
return local.Create(cfg.(local.Config))
case "sftp":
debug.Log("create sftp repository at %#v", loc.Config)
return sftp.CreateWithConfig(loc.Config.(sftp.Config))
return sftp.Create(cfg.(sftp.Config))
case "s3":
cfg := loc.Config.(s3.Config)
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv("AWS_ACCESS_KEY_ID")
}
if cfg.Secret == "" {
cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
debug.Log("create s3 repository at %#v", loc.Config)
return s3.Open(cfg)
return s3.Open(cfg.(s3.Config))
case "rest":
return rest.Open(loc.Config.(rest.Config))
return rest.Create(cfg.(rest.Config))
}
debug.Log("invalid repository scheme: %v", s)

View File

@@ -0,0 +1,73 @@
// +build debug
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"restic/errors"
"restic/repository"
"github.com/pkg/profile"
)
var (
listenMemoryProfile string
memProfilePath string
cpuProfilePath string
insecure bool
prof interface {
Stop()
}
)
func init() {
f := cmdRoot.PersistentFlags()
f.StringVar(&listenMemoryProfile, "listen-profile", "", "listen on this `address:port` for memory profiling")
f.StringVar(&memProfilePath, "mem-profile", "", "write memory profile to `dir`")
f.StringVar(&cpuProfilePath, "cpu-profile", "", "write cpu profile to `dir`")
f.BoolVar(&insecure, "insecure-kdf", false, "use insecure KDF settings")
}
type fakeTestingTB struct{}
func (fakeTestingTB) Logf(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, msg, args...)
}
func runDebug() error {
if listenMemoryProfile != "" {
fmt.Fprintf(os.Stderr, "running memory profile HTTP server on %v\n", listenMemoryProfile)
go func() {
err := http.ListenAndServe(listenMemoryProfile, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "memory profile listen failed: %v\n", err)
}
}()
}
if memProfilePath != "" && cpuProfilePath != "" {
return errors.Fatal("only one profile (memory or CPU) may be activated at the same time")
}
if memProfilePath != "" {
prof = profile.Start(profile.Quiet, profile.MemProfile, profile.ProfilePath(memProfilePath))
} else if cpuProfilePath != "" {
prof = profile.Start(profile.Quiet, profile.CPUProfile, profile.ProfilePath(cpuProfilePath))
}
if insecure {
repository.TestUseLowSecurityKDFParameters(fakeTestingTB{})
}
return nil
}
func shutdownDebug() {
if prof != nil {
prof.Stop()
}
}

View File

@@ -0,0 +1,9 @@
// +build !debug
package main
// runDebug is a noop without the debug tag.
func runDebug() error { return nil }
// shutdownDebug is a noop without the debug tag.
func shutdownDebug() {}

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"io/ioutil"
"os"
@@ -8,6 +9,7 @@ import (
"runtime"
"testing"
"restic/options"
"restic/repository"
. "restic/test"
)
@@ -15,6 +17,7 @@ import (
type dirEntry struct {
path string
fi os.FileInfo
link uint64
}
func walkDir(dir string) <-chan *dirEntry {
@@ -36,6 +39,7 @@ func walkDir(dir string) <-chan *dirEntry {
ch <- &dirEntry{
path: name,
fi: info,
link: nlink(info),
}
return nil
@@ -192,9 +196,11 @@ func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions))
gopts := GlobalOptions{
Repo: env.repo,
Quiet: true,
ctx: context.Background(),
password: TestPassword,
stdout: os.Stdout,
stderr: os.Stderr,
extended: make(options.Options),
}
// always overwrite global options

View File

@@ -4,7 +4,9 @@ package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"syscall"
)
@@ -37,5 +39,37 @@ func (e *dirEntry) equals(other *dirEntry) bool {
return false
}
if stat.Nlink != stat2.Nlink {
fmt.Fprintf(os.Stderr, "%v: Number of links do not match (%v != %v)\n", e.path, stat.Nlink, stat2.Nlink)
return false
}
return true
}
func nlink(info os.FileInfo) uint64 {
stat, _ := info.Sys().(*syscall.Stat_t)
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)
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil
}
for _, f := range files {
if err := syscall.Stat(filepath.Join(dir, f.Name()), &stat); err != nil {
return nil
}
linkTests[uint64(stat.Ino)] = append(linkTests[uint64(stat.Ino)], f.Name())
}
return linkTests
}

View File

@@ -4,6 +4,7 @@ package main
import (
"fmt"
"io/ioutil"
"os"
)
@@ -25,3 +26,24 @@ func (e *dirEntry) equals(other *dirEntry) bool {
return true
}
func nlink(info os.FileInfo) uint64 {
return 1
}
func inode(info os.FileInfo) uint64 {
return uint64(0)
}
func createFileSetPerHardlink(dir string) map[uint64][]string {
linkTests := make(map[uint64][]string)
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil
}
for i, f := range files {
linkTests[uint64(i)] = append(linkTests[uint64(i)], f.Name())
i++
}
return linkTests
}

View File

@@ -4,9 +4,11 @@ import (
"bufio"
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"io/ioutil"
mrand "math/rand"
"os"
"path/filepath"
"regexp"
@@ -140,23 +142,62 @@ func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string {
globalOptions.Quiet = quiet
}()
OK(t, runLs(gopts, []string{snapshotID}))
opts := LsOptions{}
OK(t, runLs(opts, gopts, []string{snapshotID}))
return strings.Split(string(buf.Bytes()), "\n")
}
func testRunFind(t testing.TB, gopts GlobalOptions, pattern string) []string {
func testRunFind(t testing.TB, wantJSON bool, gopts GlobalOptions, pattern string) []byte {
buf := bytes.NewBuffer(nil)
globalOptions.stdout = buf
globalOptions.JSON = wantJSON
defer func() {
globalOptions.stdout = os.Stdout
globalOptions.JSON = false
}()
opts := FindOptions{}
OK(t, runFind(opts, gopts, []string{pattern}))
return strings.Split(string(buf.Bytes()), "\n")
return buf.Bytes()
}
func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snapmap map[restic.ID]Snapshot) {
buf := bytes.NewBuffer(nil)
globalOptions.stdout = buf
globalOptions.JSON = true
defer func() {
globalOptions.stdout = os.Stdout
globalOptions.JSON = gopts.JSON
}()
opts := SnapshotOptions{}
OK(t, runSnapshots(opts, globalOptions, []string{}))
snapshots := []Snapshot{}
OK(t, json.Unmarshal(buf.Bytes(), &snapshots))
snapmap = make(map[restic.ID]Snapshot, len(snapshots))
for _, sn := range snapshots {
snapmap[*sn.ID] = sn
if newest == nil || sn.Time.After(newest.Time) {
newest = &sn
}
}
return
}
func testRunForget(t testing.TB, gopts GlobalOptions, args ...string) {
opts := ForgetOptions{}
OK(t, runForget(opts, gopts, args))
}
func testRunPrune(t testing.TB, gopts GlobalOptions) {
OK(t, runPrune(gopts))
}
func TestBackup(t *testing.T) {
@@ -343,6 +384,52 @@ func TestBackupMissingFile2(t *testing.T) {
})
}
func TestBackupChangedFile(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
datafile := filepath.Join("testdata", "backup-data.tar.gz")
fd, err := os.Open(datafile)
if os.IsNotExist(errors.Cause(err)) {
t.Skipf("unable to find data file %q, skipping", datafile)
return
}
OK(t, err)
OK(t, fd.Close())
SetupTarTestFixture(t, env.testdata, datafile)
testRunInit(t, gopts)
globalOptions.stderr = ioutil.Discard
defer func() {
globalOptions.stderr = os.Stderr
}()
modFile := filepath.Join(env.testdata, "0", "0", "6", "18")
ranHook := false
debug.Hook("archiver.SaveFile", func(context interface{}) {
pathname := context.(string)
if pathname != modFile {
return
}
t.Logf("in hook, modifying test file %v", modFile)
ranHook = true
OK(t, ioutil.WriteFile(modFile, []byte("modified"), 0600))
})
opts := BackupOptions{}
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
Assert(t, ranHook, "hook did not run")
debug.RemoveHook("archiver.SaveFile")
})
}
func TestBackupDirectoryError(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
datafile := filepath.Join("testdata", "backup-data.tar.gz")
@@ -460,23 +547,23 @@ func TestBackupExclude(t *testing.T) {
testRunBackup(t, []string{datadir}, opts, gopts)
snapshots, snapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, gopts))
files := testRunLs(t, gopts, snapshotID)
Assert(t, includes(files, filepath.Join("testdata", "foo.tar.gz")),
Assert(t, includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")),
"expected file %q in first snapshot, but it's not included", "foo.tar.gz")
opts.Excludes = []string{"*.tar.gz"}
testRunBackup(t, []string{datadir}, opts, gopts)
snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, gopts))
files = testRunLs(t, gopts, snapshotID)
Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")),
Assert(t, !includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")),
"expected file %q not in first snapshot, but it's included", "foo.tar.gz")
opts.Excludes = []string{"*.tar.gz", "private/secret"}
testRunBackup(t, []string{datadir}, opts, gopts)
snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, gopts))
_, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, gopts))
files = testRunLs(t, gopts, snapshotID)
Assert(t, !includes(files, filepath.Join("testdata", "foo.tar.gz")),
Assert(t, !includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")),
"expected file %q not in first snapshot, but it's included", "foo.tar.gz")
Assert(t, !includes(files, filepath.Join("testdata", "private", "secret", "passwords.txt")),
Assert(t, !includes(files, filepath.Join(string(filepath.Separator), "testdata", "private", "secret", "passwords.txt")),
"expected file %q not in first snapshot, but it's included", "passwords.txt")
})
}
@@ -546,6 +633,105 @@ func TestIncrementalBackup(t *testing.T) {
})
}
func TestBackupTags(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
datafile := filepath.Join("testdata", "backup-data.tar.gz")
testRunInit(t, gopts)
SetupTarTestFixture(t, env.testdata, datafile)
opts := BackupOptions{}
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
newest, _ := testRunSnapshots(t, gopts)
Assert(t, newest != nil, "expected a new backup, got nil")
Assert(t, len(newest.Tags) == 0,
"expected no tags, got %v", newest.Tags)
opts.Tags = []string{"NL"}
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
newest, _ = testRunSnapshots(t, gopts)
Assert(t, newest != nil, "expected a new backup, got nil")
Assert(t, len(newest.Tags) == 1 && newest.Tags[0] == "NL",
"expected one NL tag, got %v", newest.Tags)
})
}
func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) {
OK(t, runTag(opts, gopts, []string{}))
}
func TestTag(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
datafile := filepath.Join("testdata", "backup-data.tar.gz")
testRunInit(t, gopts)
SetupTarTestFixture(t, env.testdata, datafile)
testRunBackup(t, []string{env.testdata}, BackupOptions{}, gopts)
testRunCheck(t, gopts)
newest, _ := testRunSnapshots(t, gopts)
Assert(t, newest != nil, "expected a new backup, got nil")
Assert(t, len(newest.Tags) == 0,
"expected no tags, got %v", newest.Tags)
Assert(t, newest.Original == nil,
"expected original ID to be nil, got %v", newest.Original)
originalID := *newest.ID
testRunTag(t, TagOptions{SetTags: []string{"NL"}}, gopts)
testRunCheck(t, gopts)
newest, _ = testRunSnapshots(t, gopts)
Assert(t, newest != nil, "expected a new backup, got nil")
Assert(t, len(newest.Tags) == 1 && newest.Tags[0] == "NL",
"set failed, expected one NL tag, got %v", newest.Tags)
Assert(t, newest.Original != nil, "expected original snapshot id, got nil")
Assert(t, *newest.Original == originalID,
"expected original ID to be set to the first snapshot id")
testRunTag(t, TagOptions{AddTags: []string{"CH"}}, gopts)
testRunCheck(t, gopts)
newest, _ = testRunSnapshots(t, gopts)
Assert(t, newest != nil, "expected a new backup, got nil")
Assert(t, len(newest.Tags) == 2 && newest.Tags[0] == "NL" && newest.Tags[1] == "CH",
"add failed, expected CH,NL tags, got %v", newest.Tags)
Assert(t, newest.Original != nil, "expected original snapshot id, got nil")
Assert(t, *newest.Original == originalID,
"expected original ID to be set to the first snapshot id")
testRunTag(t, TagOptions{RemoveTags: []string{"NL"}}, gopts)
testRunCheck(t, gopts)
newest, _ = testRunSnapshots(t, gopts)
Assert(t, newest != nil, "expected a new backup, got nil")
Assert(t, len(newest.Tags) == 1 && newest.Tags[0] == "CH",
"remove failed, expected one CH tag, got %v", newest.Tags)
Assert(t, newest.Original != nil, "expected original snapshot id, got nil")
Assert(t, *newest.Original == originalID,
"expected original ID to be set to the first snapshot id")
testRunTag(t, TagOptions{AddTags: []string{"US", "RU"}}, gopts)
testRunTag(t, TagOptions{RemoveTags: []string{"CH", "US", "RU"}}, gopts)
testRunCheck(t, gopts)
newest, _ = testRunSnapshots(t, gopts)
Assert(t, newest != nil, "expected a new backup, got nil")
Assert(t, len(newest.Tags) == 0,
"expected no tags, got %v", newest.Tags)
Assert(t, newest.Original != nil, "expected original snapshot id, got nil")
Assert(t, *newest.Original == originalID,
"expected original ID to be set to the first snapshot id")
// Check special case of removing all tags.
testRunTag(t, TagOptions{SetTags: []string{""}}, gopts)
testRunCheck(t, gopts)
newest, _ = testRunSnapshots(t, gopts)
Assert(t, newest != nil, "expected a new backup, got nil")
Assert(t, len(newest.Tags) == 0,
"expected no tags, got %v", newest.Tags)
Assert(t, newest.Original != nil, "expected original snapshot id, got nil")
Assert(t, *newest.Original == originalID,
"expected original ID to be set to the first snapshot id")
})
}
func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
buf := bytes.NewBuffer(nil)
@@ -684,6 +870,30 @@ func TestRestoreFilter(t *testing.T) {
})
}
func TestRestore(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
testRunInit(t, gopts)
for i := 0; i < 10; i++ {
p := filepath.Join(env.testdata, fmt.Sprintf("foo/bar/testfile%v", i))
OK(t, os.MkdirAll(filepath.Dir(p), 0755))
OK(t, appendRandomData(p, uint(mrand.Intn(5<<21))))
}
opts := BackupOptions{}
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
// Restore latest without any filters
restoredir := filepath.Join(env.base, "restore")
testRunRestoreLatest(t, gopts, restoredir, nil, "")
Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata))),
"directories are not equal")
})
}
func TestRestoreLatest(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
@@ -756,7 +966,7 @@ func TestRestoreWithPermissionFailure(t *testing.T) {
testRunRestore(t, gopts, filepath.Join(env.base, "restore"), snapshots[0])
// make sure that all files have been restored, regardeless of any
// make sure that all files have been restored, regardless of any
// permission errors
files := testRunLs(t, gopts, snapshots[0].String())
for _, filename := range files {
@@ -829,14 +1039,61 @@ func TestFind(t *testing.T) {
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
results := testRunFind(t, gopts, "unexistingfile")
Assert(t, len(results) != 0, "unexisting file found in repo (%v)", datafile)
results := testRunFind(t, false, gopts, "unexistingfile")
Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile)
results = testRunFind(t, gopts, "testfile")
Assert(t, len(results) != 1, "file not found in repo (%v)", datafile)
results = testRunFind(t, false, gopts, "testfile")
lines := strings.Split(string(results), "\n")
Assert(t, len(lines) == 2, "expected one file found in repo (%v)", datafile)
results = testRunFind(t, gopts, "test")
Assert(t, len(results) < 2, "less than two file found in repo (%v)", datafile)
results = testRunFind(t, false, gopts, "testfile*")
lines = strings.Split(string(results), "\n")
Assert(t, len(lines) == 4, "expected three files found in repo (%v)", datafile)
})
}
type testMatch struct {
Path string `json:"path,omitempty"`
Permissions string `json:"permissions,omitempty"`
Size uint64 `json:"size,omitempty"`
Date time.Time `json:"date,omitempty"`
UID uint32 `json:"uid,omitempty"`
GID uint32 `json:"gid,omitempty"`
}
type testMatches struct {
Hits int `json:"hits,omitempty"`
SnapshotID string `json:"snapshot,omitempty"`
Matches []testMatch `json:"matches,omitempty"`
}
func TestFindJSON(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
datafile := filepath.Join("testdata", "backup-data.tar.gz")
testRunInit(t, gopts)
SetupTarTestFixture(t, env.testdata, datafile)
opts := BackupOptions{}
testRunBackup(t, []string{env.testdata}, opts, gopts)
testRunCheck(t, gopts)
results := testRunFind(t, true, gopts, "unexistingfile")
matches := []testMatches{}
OK(t, json.Unmarshal(results, &matches))
Assert(t, len(matches) == 0, "expected no match in repo (%v)", datafile)
results = testRunFind(t, true, gopts, "testfile")
OK(t, json.Unmarshal(results, &matches))
Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
Assert(t, len(matches[0].Matches) == 1, "expected a single file to match (%v)", datafile)
Assert(t, matches[0].Hits == 1, "expected hits to show 1 match (%v)", datafile)
results = testRunFind(t, true, gopts, "testfile*")
OK(t, json.Unmarshal(results, &matches))
Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", datafile)
Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
})
}
@@ -855,7 +1112,7 @@ func TestRebuildIndex(t *testing.T) {
}
if !strings.Contains(out, "restic rebuild-index") {
t.Fatalf("did not find hint for rebuild-index comman")
t.Fatalf("did not find hint for rebuild-index command")
}
testRunRebuildIndex(t, gopts)
@@ -901,3 +1158,130 @@ func TestCheckRestoreNoLock(t *testing.T) {
testRunRestore(t, gopts, filepath.Join(env.base, "restore"), snapshotIDs[0])
})
}
func TestPrune(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
datafile := filepath.Join("testdata", "backup-data.tar.gz")
fd, err := os.Open(datafile)
if os.IsNotExist(errors.Cause(err)) {
t.Skipf("unable to find data file %q, skipping", datafile)
return
}
OK(t, err)
OK(t, fd.Close())
testRunInit(t, gopts)
SetupTarTestFixture(t, env.testdata, datafile)
opts := BackupOptions{}
testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0", "1")}, opts, gopts)
testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0", "2")}, opts, gopts)
testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0", "3")}, opts, gopts)
snapshotIDs := testRunList(t, "snapshots", gopts)
Assert(t, len(snapshotIDs) == 3,
"expected one snapshot, got %v", snapshotIDs)
testRunForget(t, gopts, snapshotIDs[0].String())
testRunPrune(t, gopts)
testRunCheck(t, gopts)
})
}
func TestHardLink(t *testing.T) {
// this test assumes a test set with a single directory containing hard linked files
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
datafile := filepath.Join("testdata", "test.hl.tar.gz")
fd, err := os.Open(datafile)
if os.IsNotExist(errors.Cause(err)) {
t.Skipf("unable to find data file %q, skipping", datafile)
return
}
OK(t, err)
OK(t, fd.Close())
testRunInit(t, gopts)
SetupTarTestFixture(t, env.testdata, datafile)
linkTests := createFileSetPerHardlink(env.testdata)
opts := BackupOptions{}
// first backup
testRunBackup(t, []string{env.testdata}, opts, gopts)
snapshotIDs := testRunList(t, "snapshots", gopts)
Assert(t, len(snapshotIDs) == 1,
"expected one snapshot, got %v", snapshotIDs)
testRunCheck(t, gopts)
// restore all backups and compare
for i, snapshotID := range snapshotIDs {
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
testRunRestore(t, gopts, restoredir, snapshotIDs[0])
Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")),
"directories are not equal")
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
Assert(t, linksEqual(linkTests, linkResults),
"links are not equal")
}
testRunCheck(t, gopts)
})
}
func linksEqual(source, dest map[uint64][]string) bool {
for _, vs := range source {
found := false
for kd, vd := range dest {
if linkEqual(vs, vd) {
delete(dest, kd)
found = true
break
}
}
if !found {
return false
}
}
if len(dest) != 0 {
return false
}
return true
}
func linkEqual(source, dest []string) bool {
// equal if sliced are equal without considering order
if source == nil && dest == nil {
return true
}
if source == nil || dest == nil {
return false
}
if len(source) != len(dest) {
return false
}
for i := range source {
found := false
for j := range dest {
if source[i] == dest[j] {
found = true
break
}
}
if !found {
return false
}
}
return true
}

View File

@@ -0,0 +1,39 @@
package main
import (
"path/filepath"
. "restic/test"
"testing"
)
func TestRestoreLocalLayout(t *testing.T) {
withTestEnvironment(t, func(env *testEnvironment, gopts GlobalOptions) {
var tests = []struct {
filename string
layout string
}{
{"repo-layout-default.tar.gz", ""},
{"repo-layout-s3legacy.tar.gz", ""},
{"repo-layout-default.tar.gz", "default"},
{"repo-layout-s3legacy.tar.gz", "s3legacy"},
}
for _, test := range tests {
datafile := filepath.Join("..", "..", "restic", "backend", "testdata", test.filename)
SetupTarTestFixture(t, env.base, datafile)
gopts.extended["local.layout"] = test.layout
// check the repo
testRunCheck(t, gopts)
// restore latest snapshot
target := filepath.Join(env.base, "restore")
testRunRestoreLatest(t, gopts, target, nil, "")
RemoveAll(t, filepath.Join(env.base, "repo"))
RemoveAll(t, target)
}
})
}

View File

@@ -36,6 +36,7 @@ func lockRepository(repo *repository.Repository, exclusive bool) (*restic.Lock,
if err != nil {
return nil, err
}
debug.Log("create lock %p (exclusive %v)", lock, exclusive)
globalLocks.Lock()
if globalLocks.cancelRefresh == nil {
@@ -88,7 +89,7 @@ func unlockRepo(lock *restic.Lock) error {
globalLocks.Lock()
defer globalLocks.Unlock()
debug.Log("unlocking repository")
debug.Log("unlocking repository with lock %p", lock)
if err := lock.Unlock(); err != nil {
debug.Log("error while unlocking: %v", err)
return err

View File

@@ -1,11 +1,14 @@
package main
import (
"bufio"
"bytes"
"fmt"
"log"
"os"
"restic"
"restic/debug"
"runtime"
"restic/options"
"github.com/spf13/cobra"
@@ -20,20 +23,36 @@ var cmdRoot = &cobra.Command{
restic is a backup program which allows saving multiple revisions of files and
directories in an encrypted repository stored on different backends.
`,
SilenceErrors: true,
SilenceUsage: true,
PersistentPreRun: parseEnvironment,
SilenceErrors: true,
SilenceUsage: true,
PersistentPreRunE: func(*cobra.Command, []string) error {
// parse extended options
opts, err := options.Parse(globalOptions.Options)
if err != nil {
return err
}
globalOptions.extended = opts
// run the debug functions for all subcommands (if build tag "debug" is
// enabled)
if err := runDebug(); err != nil {
return err
}
return nil
},
PersistentPostRun: func(*cobra.Command, []string) {
shutdownDebug()
},
}
var logBuffer = bytes.NewBuffer(nil)
func init() {
// set GOMAXPROCS to number of CPUs
if runtime.Version() < "go1.5" {
gomaxprocs := os.Getenv("GOMAXPROCS")
debug.Log("read GOMAXPROCS from env variable, value: %s", gomaxprocs)
if gomaxprocs == "" {
runtime.GOMAXPROCS(runtime.NumCPU())
}
}
// install custom global logger into a buffer, if an error occurs
// we can show the logs
log.SetOutput(logBuffer)
}
func main() {
@@ -47,11 +66,20 @@ func main() {
fmt.Fprintf(os.Stderr, "%v\n", err)
case err != nil:
fmt.Fprintf(os.Stderr, "%+v\n", err)
if logBuffer.Len() > 0 {
fmt.Fprintf(os.Stderr, "also, the following messages were logged by a library:\n")
sc := bufio.NewScanner(logBuffer)
for sc.Scan() {
fmt.Fprintln(os.Stderr, sc.Text())
}
}
}
RunCleanupHandlers()
var exitCode int
if err != nil {
os.Exit(1)
exitCode = 1
}
Exit(exitCode)
}

BIN
src/cmds/restic/testdata/test.hl.tar.gz vendored Normal file

Binary file not shown.

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