mirror of
https://github.com/restic/restic.git
synced 2026-02-22 16:56:24 +00:00
Compare commits
565 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c02923fbfc | ||
|
|
7c5ce83044 | ||
|
|
37e2e9a844 | ||
|
|
26e5db1849 | ||
|
|
a2766ffe0c | ||
|
|
0f5e38609f | ||
|
|
f178cbf93d | ||
|
|
c8096ca8d2 | ||
|
|
27d29b9853 | ||
|
|
8a171731ba | ||
|
|
abde9e2fba | ||
|
|
6a4a328bbc | ||
|
|
8253fadc96 | ||
|
|
134abbd82b | ||
|
|
cd8226130a | ||
|
|
1ebf0e8de8 | ||
|
|
0fdb9a6129 | ||
|
|
47b326b7b5 | ||
|
|
e2cf6eb434 | ||
|
|
f79698dcdd | ||
|
|
35a5307db3 | ||
|
|
014cec06f1 | ||
|
|
431ab5aa6a | ||
|
|
262b0cd9d4 | ||
|
|
e83ec17e95 | ||
|
|
e2005e02bb | ||
|
|
41c8c946ba | ||
|
|
fe08686558 | ||
|
|
0ed2401711 | ||
|
|
06bd606d85 | ||
|
|
c347431907 | ||
|
|
f63d7048f9 | ||
|
|
f39f7c76dd | ||
|
|
0268d0e7d6 | ||
|
|
8515d093e0 | ||
|
|
fe3f326d8d | ||
|
|
8170db40c7 | ||
|
|
99ac0da4bc | ||
|
|
7e2c93420f | ||
|
|
6d46824fb0 | ||
|
|
bb435b39d9 | ||
|
|
2a67d7a6c2 | ||
|
|
ba43c8bab5 | ||
|
|
931e6ed2ac | ||
|
|
a5f0e9ab65 | ||
|
|
6fc133ad6a | ||
|
|
e1b80859f2 | ||
|
|
d069ee31b2 | ||
|
|
981752ade0 | ||
|
|
d01d07fc0a | ||
|
|
526aaca6f5 | ||
|
|
2f8147af59 | ||
|
|
f3016a9096 | ||
|
|
f854a41ba9 | ||
|
|
ca3cadef5e | ||
|
|
3304b0fcf0 | ||
|
|
d8938e259a | ||
|
|
53a554c89d | ||
|
|
e71db01230 | ||
|
|
178e946fc7 | ||
|
|
f3bff12939 | ||
|
|
7a99418dc5 | ||
|
|
c71ba466ea | ||
|
|
8ce5d35543 | ||
|
|
134f834c60 | ||
|
|
8a37c07295 | ||
|
|
bd0ada7842 | ||
|
|
eea96f652d | ||
|
|
38c3061df7 | ||
|
|
f5fa602482 | ||
|
|
e44ac55f63 | ||
|
|
f1cfb73a8b | ||
|
|
5b96885c6d | ||
|
|
c5da90a5b7 | ||
|
|
bcdebfb84e | ||
|
|
359b273649 | ||
|
|
2e2c8dc620 | ||
|
|
8d37b723ca | ||
|
|
315b7f282f | ||
|
|
a3f8e9dfa7 | ||
|
|
982810f7cc | ||
|
|
90b96d19cd | ||
|
|
6a52bb6f54 | ||
|
|
cacaa4393f | ||
|
|
d63ab4e9a4 | ||
|
|
ca6daec8dd | ||
|
|
c87f2420a6 | ||
|
|
f5bbbc52f4 | ||
|
|
9e3dde8ec7 | ||
|
|
9dba182e51 | ||
|
|
944fc857eb | ||
|
|
7507a658ac | ||
|
|
9fa4f5eb6b | ||
|
|
ce4d71d626 | ||
|
|
8e2ef3f38b | ||
|
|
8dc952775e | ||
|
|
99b6163e27 | ||
|
|
beaf55f1fc | ||
|
|
980bb9059f | ||
|
|
0e7281eb71 | ||
|
|
0b6133d7b5 | ||
|
|
b57ca64275 | ||
|
|
faadbd734b | ||
|
|
88b0a93409 | ||
|
|
4a995105a9 | ||
|
|
7fe496f983 | ||
|
|
e56370eb5b | ||
|
|
b8af7f63a0 | ||
|
|
3eea555155 | ||
|
|
897c923cc9 | ||
|
|
67193e3deb | ||
|
|
0e722efb09 | ||
|
|
3736f33ebf | ||
|
|
d1d9c3f9d7 | ||
|
|
cd5cbe0910 | ||
|
|
814e992c0b | ||
|
|
660fe78735 | ||
|
|
87d084e18c | ||
|
|
9ce2a73fc5 | ||
|
|
f2314b26ba | ||
|
|
74dcf41f25 | ||
|
|
b6ba30186f | ||
|
|
32637a0328 | ||
|
|
0addd90e14 | ||
|
|
1b5ee5b10a | ||
|
|
042adeb5d0 | ||
|
|
7e4ce0dacc | ||
|
|
8ceb22fe8a | ||
|
|
c5553ec855 | ||
|
|
bb3ed54291 | ||
|
|
513ba3b6f7 | ||
|
|
17d688afef | ||
|
|
d81eee26b3 | ||
|
|
cc5ada63a4 | ||
|
|
88fb60e0b5 | ||
|
|
02200acad0 | ||
|
|
1a2d190bdb | ||
|
|
2db4ff168a | ||
|
|
a77c8cc5d2 | ||
|
|
b8866c1fe4 | ||
|
|
f0f17db847 | ||
|
|
a5c003acb0 | ||
|
|
7b44fd0f9d | ||
|
|
cebee0b8fa | ||
|
|
d886bc6c48 | ||
|
|
d81adcfaa5 | ||
|
|
6da9bfbbce | ||
|
|
69a6e622d0 | ||
|
|
1dcfd64028 | ||
|
|
5d1c1f721e | ||
|
|
fbc8bbf305 | ||
|
|
cdef55bb88 | ||
|
|
26df48b2aa | ||
|
|
a7baea0522 | ||
|
|
55e6003749 | ||
|
|
846acd5d4c | ||
|
|
43f8145858 | ||
|
|
79759928f6 | ||
|
|
eb59d28154 | ||
|
|
79f63a2e74 | ||
|
|
6a62254048 | ||
|
|
657a1d75af | ||
|
|
2694def56a | ||
|
|
7843341da3 | ||
|
|
d46314648e | ||
|
|
e45011af57 | ||
|
|
fb09884893 | ||
|
|
d1eecafa63 | ||
|
|
b47d991f56 | ||
|
|
553ea812a7 | ||
|
|
216e374310 | ||
|
|
034b0b8040 | ||
|
|
5ab9e12b46 | ||
|
|
abe6e0d22d | ||
|
|
f5b550191c | ||
|
|
3dcacb3730 | ||
|
|
033589a66b | ||
|
|
e46a647c45 | ||
|
|
f26492fc2d | ||
|
|
3473c3f7b6 | ||
|
|
1b5242b4f9 | ||
|
|
6d897def1b | ||
|
|
2f10e25738 | ||
|
|
555bd257bd | ||
|
|
3afd974dea | ||
|
|
f4120c9d45 | ||
|
|
61cb1cc6f8 | ||
|
|
49bc1d0b3b | ||
|
|
ba23d24dd1 | ||
|
|
556a63de19 | ||
|
|
fae3c4d437 | ||
|
|
89c2ed2a1c | ||
|
|
23f1cb06d6 | ||
|
|
ac92e2dd2d | ||
|
|
bf58425351 | ||
|
|
a3dc0ab398 | ||
|
|
224ebdb8b9 | ||
|
|
cf80d295f3 | ||
|
|
2133869127 | ||
|
|
97330ac621 | ||
|
|
1ee1559506 | ||
|
|
eccc336319 | ||
|
|
7fe657ec71 | ||
|
|
77c07bfd19 | ||
|
|
4de938d97a | ||
|
|
dad1c87afe | ||
|
|
801dbb6d03 | ||
|
|
fa0be82da8 | ||
|
|
7e8bc8d362 | ||
|
|
0bb2a8e0d0 | ||
|
|
2e72b57f2f | ||
|
|
bff1039e3a | ||
|
|
5a999cb77f | ||
|
|
3a2539e0ac | ||
|
|
e262f35d0a | ||
|
|
176bfa6529 | ||
|
|
240c4cf2fd | ||
|
|
db5ec5d876 | ||
|
|
e1dfaf5d87 | ||
|
|
5436154f0d | ||
|
|
809e218d20 | ||
|
|
1eaad6cebb | ||
|
|
56fccecd06 | ||
|
|
3890a947ca | ||
|
|
e299272378 | ||
|
|
70248bd05a | ||
|
|
7a5fde8f5a | ||
|
|
62ba9f1950 | ||
|
|
610b676444 | ||
|
|
58699e3c90 | ||
|
|
9be24a1c9f | ||
|
|
5ace41471e | ||
|
|
3b2106ed30 | ||
|
|
5f4f997126 | ||
|
|
49d397a419 | ||
|
|
ea1ab96749 | ||
|
|
24c62e719a | ||
|
|
9c6b7f688e | ||
|
|
d41dce5ecb | ||
|
|
52a3eafede | ||
|
|
55dfc85159 | ||
|
|
a7a478a19e | ||
|
|
2080afd9de | ||
|
|
9aa136b982 | ||
|
|
3a191f37cb | ||
|
|
429106340f | ||
|
|
530c73b457 | ||
|
|
fb9729fdb9 | ||
|
|
45a09c76ff | ||
|
|
efd65a1b65 | ||
|
|
ae60188eb9 | ||
|
|
3b904525d9 | ||
|
|
1e31f5202f | ||
|
|
f12d41138a | ||
|
|
98369f6a5d | ||
|
|
8f9bf1995b | ||
|
|
e7de3b5f9d | ||
|
|
3541d06d07 | ||
|
|
db0e3cd772 | ||
|
|
d3fee08f9a | ||
|
|
727fb6eabe | ||
|
|
d610c60991 | ||
|
|
3f6e11d26e | ||
|
|
a4577769ae | ||
|
|
7f927d4774 | ||
|
|
e091673f8f | ||
|
|
9842eff887 | ||
|
|
c40b3d3983 | ||
|
|
ac5eefdee4 | ||
|
|
bf508643a5 | ||
|
|
02fc16e97d | ||
|
|
1a83a739dc | ||
|
|
81473f4538 | ||
|
|
e1a847e4d1 | ||
|
|
0f426c3795 | ||
|
|
6df3d169b8 | ||
|
|
5479daa6d4 | ||
|
|
397fec0152 | ||
|
|
d7e644272f | ||
|
|
e91749bbb0 | ||
|
|
bcd1e45ba7 | ||
|
|
4c6b626db6 | ||
|
|
835ba16c27 | ||
|
|
3b6a580b32 | ||
|
|
01c486d486 | ||
|
|
6342a08a16 | ||
|
|
94c8ee11f8 | ||
|
|
9b38980ed9 | ||
|
|
649c536250 | ||
|
|
dd49e2b12d | ||
|
|
f61dab1774 | ||
|
|
40edf00182 | ||
|
|
c35518a865 | ||
|
|
7a0b4428e3 | ||
|
|
c784a15aaa | ||
|
|
ce180de9b8 | ||
|
|
fca9a523e9 | ||
|
|
8a3889be11 | ||
|
|
2a1633621b | ||
|
|
e2deeceb1b | ||
|
|
d4e994de7b | ||
|
|
a60e751217 | ||
|
|
81c5d8a968 | ||
|
|
5b1e4df177 | ||
|
|
4d80744cbb | ||
|
|
e243d4b7ee | ||
|
|
dce35fcb00 | ||
|
|
e45a21b0b6 | ||
|
|
fda563d606 | ||
|
|
f3b49987f8 | ||
|
|
c8c01a5cae | ||
|
|
f7ece90129 | ||
|
|
0f25ef9498 | ||
|
|
5bf2228596 | ||
|
|
227b01395f | ||
|
|
9f52fe1a10 | ||
|
|
affc6c3390 | ||
|
|
951a34dcbf | ||
|
|
36eaa22ed0 | ||
|
|
62df316356 | ||
|
|
00797fdd85 | ||
|
|
9eb39cef05 | ||
|
|
ee6150f67c | ||
|
|
9fa909ccd6 | ||
|
|
b1af544b1d | ||
|
|
7d5b17ac72 | ||
|
|
7a221f2473 | ||
|
|
bdbe956c5c | ||
|
|
8e5b1e6f2f | ||
|
|
257a454515 | ||
|
|
b6aeea425b | ||
|
|
c8e05d1f2a | ||
|
|
a8aa4eb06c | ||
|
|
c1a02cc081 | ||
|
|
e66adc42da | ||
|
|
89938bc21c | ||
|
|
0b2947dedb | ||
|
|
47ddd34266 | ||
|
|
2fdca5d310 | ||
|
|
e5d4e33509 | ||
|
|
e117f613af | ||
|
|
0dfdf02885 | ||
|
|
4a0129fc2b | ||
|
|
a9c705009c | ||
|
|
d937ad8cf6 | ||
|
|
1a08a8219f | ||
|
|
9924c311c9 | ||
|
|
e846e14965 | ||
|
|
36e70228f2 | ||
|
|
a677f1139a | ||
|
|
6f8eba9c28 | ||
|
|
c22c582546 | ||
|
|
ea75509d6e | ||
|
|
ed30bd7b76 | ||
|
|
7090c5ceeb | ||
|
|
bee09c1a0f | ||
|
|
8f9ef4402b | ||
|
|
f26c0cb70f | ||
|
|
81d7ecba2b | ||
|
|
087c3fe1dc | ||
|
|
43ff971dfd | ||
|
|
5c75a98053 | ||
|
|
7ce47402fb | ||
|
|
1e48141648 | ||
|
|
dbda892542 | ||
|
|
b46774be21 | ||
|
|
1073bfba37 | ||
|
|
5dfb4d1195 | ||
|
|
0a2219c5f7 | ||
|
|
ff3149831e | ||
|
|
c935d0558c | ||
|
|
83eb075e3a | ||
|
|
204c2bf09c | ||
|
|
2444522243 | ||
|
|
92eb1cbffd | ||
|
|
8c40ae5a03 | ||
|
|
fa2ee78a5c | ||
|
|
e4a5cdc5bc | ||
|
|
2d73a273af | ||
|
|
761af08889 | ||
|
|
0ee1650f82 | ||
|
|
0e647417f3 | ||
|
|
d1bf5a4882 | ||
|
|
b8414b240c | ||
|
|
3fbdd12b04 | ||
|
|
a3f6bf3e5a | ||
|
|
3a5805db50 | ||
|
|
de8c64e767 | ||
|
|
73d6b15095 | ||
|
|
5d396b9302 | ||
|
|
61d2519111 | ||
|
|
e61c94a846 | ||
|
|
7ed0f61f3f | ||
|
|
85055d1c68 | ||
|
|
e4c469c149 | ||
|
|
9940e8d9f1 | ||
|
|
3dccca1f27 | ||
|
|
22e96a37f8 | ||
|
|
48b1ab5aaf | ||
|
|
0230fa188f | ||
|
|
4118ce876e | ||
|
|
9537bc561d | ||
|
|
ae43c47ca8 | ||
|
|
2fa4060991 | ||
|
|
f9a934759f | ||
|
|
3686b1ffe5 | ||
|
|
ea017a49c3 | ||
|
|
3559f9c776 | ||
|
|
637f57ca71 | ||
|
|
4e60156b45 | ||
|
|
af9946b098 | ||
|
|
b7d4b0f821 | ||
|
|
62ed776a8c | ||
|
|
f880ff21aa | ||
|
|
4a36993c19 | ||
|
|
d87b2f189d | ||
|
|
f9a097a8c0 | ||
|
|
d43358b6dd | ||
|
|
8058f196e1 | ||
|
|
e13e6f34d2 | ||
|
|
c2ff7150aa | ||
|
|
a899621930 | ||
|
|
a0966e1d1d | ||
|
|
e2464382ed | ||
|
|
095bc79dc3 | ||
|
|
1fd3c2488e | ||
|
|
2ee8485886 | ||
|
|
b67c178672 | ||
|
|
7ac4f0a525 | ||
|
|
c4613c51d1 | ||
|
|
77bf17076b | ||
|
|
8dd6beba15 | ||
|
|
a345386967 | ||
|
|
bdd43bd430 | ||
|
|
1716501598 | ||
|
|
d9a5b9178e | ||
|
|
8ca6a9a240 | ||
|
|
ba75a3884c | ||
|
|
d91d89eef6 | ||
|
|
a726c91116 | ||
|
|
d00fe95f10 | ||
|
|
072b7a014e | ||
|
|
618ce115d7 | ||
|
|
d973aa82fe | ||
|
|
3a85b6b7c6 | ||
|
|
2c22ff175c | ||
|
|
6bc43a4198 | ||
|
|
e348b3deeb | ||
|
|
6724b9a583 | ||
|
|
41c35b2218 | ||
|
|
4477d76f03 | ||
|
|
14f5f6235a | ||
|
|
739350fd8e | ||
|
|
14ed97102b | ||
|
|
db389058fa | ||
|
|
b557d04007 | ||
|
|
52c5da997b | ||
|
|
57d198f99a | ||
|
|
a3ab17b470 | ||
|
|
9bf3141893 | ||
|
|
d35eb6a0c3 | ||
|
|
37aad2e3aa | ||
|
|
efc5d0699a | ||
|
|
893bc9f777 | ||
|
|
61b8729ef9 | ||
|
|
b89d3cc4d0 | ||
|
|
e8cc11ea34 | ||
|
|
2e804511ca | ||
|
|
b6790c491b | ||
|
|
c95e2b009e | ||
|
|
d5615a67c8 | ||
|
|
d9b9bbd4a8 | ||
|
|
d780b1eede | ||
|
|
608adf15a3 | ||
|
|
1717391f6c | ||
|
|
2e6e9ff6f8 | ||
|
|
23c903074c | ||
|
|
94030a12cf | ||
|
|
f63d7de9da | ||
|
|
13ee6792df | ||
|
|
6302444f34 | ||
|
|
61c5e4b54a | ||
|
|
d6118871be | ||
|
|
94b27e8933 | ||
|
|
05500dc5f8 | ||
|
|
c5a72971fe | ||
|
|
5bc6486e3b | ||
|
|
59e18bce0a | ||
|
|
898c5b6df5 | ||
|
|
9cd422791a | ||
|
|
91edebf1fe | ||
|
|
df8a5792f1 | ||
|
|
cda7b417cd | ||
|
|
d2ac35af26 | ||
|
|
6caeff2408 | ||
|
|
83d1a46526 | ||
|
|
d1bd160b0a | ||
|
|
bc88a8bb03 | ||
|
|
04cfb984ae | ||
|
|
02a245941a | ||
|
|
7fb1352aa1 | ||
|
|
4c555bad2e | ||
|
|
75c789bab4 | ||
|
|
626d020e62 | ||
|
|
3830117735 | ||
|
|
042cee8e36 | ||
|
|
03cc5b47e9 | ||
|
|
46fa45942e | ||
|
|
0cb4104aa7 | ||
|
|
f2bbc5fbc4 | ||
|
|
16340ce811 | ||
|
|
2cf8153f4a | ||
|
|
2f00287e45 | ||
|
|
0de17f64e9 | ||
|
|
c30838878f | ||
|
|
bd31281f1e | ||
|
|
7fc54ed98e | ||
|
|
0abdcedcab | ||
|
|
6c05353086 | ||
|
|
e7575bf380 | ||
|
|
89ace85903 | ||
|
|
68a91d66b7 | ||
|
|
724b5bf4fe | ||
|
|
d6da9211bc | ||
|
|
f45abac27f | ||
|
|
00b9a1d87d | ||
|
|
20b835b5a4 | ||
|
|
7bb1a474df | ||
|
|
750ee35dbf | ||
|
|
fda5e1f543 | ||
|
|
78d090aea5 | ||
|
|
7362569cf5 | ||
|
|
f5b1c7e5f1 | ||
|
|
c554cdac4c | ||
|
|
41b624ea1b | ||
|
|
7cdcaadcf5 | ||
|
|
4ad33d3c3b | ||
|
|
2778ac21de | ||
|
|
cb3cd57926 | ||
|
|
ba6815d413 | ||
|
|
52004cdde8 | ||
|
|
1d2045cb61 | ||
|
|
357e2e404a | ||
|
|
38e5640cda | ||
|
|
c4c731bd9a | ||
|
|
04d27acd60 | ||
|
|
80f0303b21 | ||
|
|
d651d9b427 | ||
|
|
3b2648bd5e | ||
|
|
73cc11f000 | ||
|
|
637de0149c | ||
|
|
855575e5a7 | ||
|
|
ed2999a163 | ||
|
|
a18c16e19e | ||
|
|
9032ab2eec | ||
|
|
03f66b8d74 | ||
|
|
8c30ae7c65 | ||
|
|
453c9c9199 | ||
|
|
993e370f92 | ||
|
|
2bcd3a3acc | ||
|
|
c54c632ca1 | ||
|
|
28a4a35625 | ||
|
|
e7577d7bb4 | ||
|
|
d702227af0 | ||
|
|
b7251dbea5 |
40
.github/ISSUE_TEMPLATE.md
vendored
40
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,25 +1,55 @@
|
||||
<!--
|
||||
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!
|
||||
NOTE: Not filling out the issue template needs a good reason, otherwise it may
|
||||
take a lot longer to find the problem! Please take the time to help us
|
||||
debugging the problem by collecting information, even if it seems irrelevant to
|
||||
you. Thanks!
|
||||
|
||||
If you have a question, the forum at https://discourse.restic.net is a better place.
|
||||
Please do not create issues for usage or documentation questions! We're using
|
||||
the GitHub issue tracker mainly for tracking bugs and feature requests.
|
||||
-->
|
||||
|
||||
## Output of `restic version`
|
||||
|
||||
|
||||
## How did you start restic exactly? (Include the complete command line)
|
||||
## How did you run restic exactly?
|
||||
|
||||
<!--
|
||||
This section should include at least:
|
||||
|
||||
* The complete command line and any environment variables you used to
|
||||
configure restic's backend access. Make sure to replace sensitive values!
|
||||
|
||||
* The output of the commands, what restic prints gives may give us much
|
||||
information to diagnose the problem!
|
||||
-->
|
||||
|
||||
|
||||
## What backend/server/service did you use?
|
||||
## What backend/server/service did you use to store the repository?
|
||||
|
||||
|
||||
## Expected behavior
|
||||
|
||||
<!--
|
||||
Describe what you'd like restic to do differently.
|
||||
-->
|
||||
|
||||
## Actual behavior
|
||||
|
||||
<!--
|
||||
In this section, please try to concentrate on observations, so only describe
|
||||
what you observed directly.
|
||||
-->
|
||||
|
||||
## Steps to reproduce the behavior
|
||||
|
||||
<!--
|
||||
The more time you spend describing an easy way to reproduce the behavior (if
|
||||
this is possible), the easier it is for the project developers to fix it!
|
||||
-->
|
||||
|
||||
|
||||
## Do you have any idea what may have caused this?
|
||||
|
||||
|
||||
## Do you have an idea how to solve the issue?
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
/pkg
|
||||
/bin
|
||||
/restic
|
||||
/.vagrant
|
||||
/vendor/pkg
|
||||
/doc/_build
|
||||
|
||||
15
.travis.yml
15
.travis.yml
@@ -2,9 +2,8 @@ language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.7.6
|
||||
- 1.8.3
|
||||
- tip
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
|
||||
os:
|
||||
- linux
|
||||
@@ -17,19 +16,15 @@ env:
|
||||
matrix:
|
||||
exclude:
|
||||
- os: osx
|
||||
go: 1.7.6
|
||||
- os: osx
|
||||
go: tip
|
||||
go: 1.8.x
|
||||
- os: linux
|
||||
go: 1.8.3
|
||||
go: 1.9.x
|
||||
include:
|
||||
- os: linux
|
||||
go: 1.8.3
|
||||
go: 1.9.x
|
||||
sudo: true
|
||||
env:
|
||||
RESTIC_TEST_FUSE=1
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
branches:
|
||||
only:
|
||||
|
||||
292
CHANGELOG.md
292
CHANGELOG.md
@@ -1,7 +1,297 @@
|
||||
This file describes changes relevant to all users that are made in each
|
||||
released version of restic from the perspective of the user.
|
||||
|
||||
Important Changes in 0.X.Y
|
||||
Important Changes in 0.8.0
|
||||
==========================
|
||||
|
||||
* A vulnerability was found in the restic restorer, which allowed attackers in
|
||||
special circumstances to restore files to a location outside of the target
|
||||
directory. Due to the circumstances we estimate this to be a low-risk
|
||||
vulnerability, but urge all users to upgrade to the latest version of restic.
|
||||
|
||||
Exploiting the vulnerability requires a Linux/Unix system which saves
|
||||
backups via restic and a Windows systems which restores files from the repo.
|
||||
In addition, the attackers need to be able to create create files with
|
||||
arbitrary names which are then saved to the restic repo. For example, by
|
||||
creating a file named "..\test.txt" (which is a perfectly legal filename on
|
||||
Linux) and restoring a snapshot containing this file on Windows, it would be
|
||||
written to the parent of the target directory.
|
||||
|
||||
We'd like to thank Tyler Spivey for reporting this responsibly!
|
||||
|
||||
https://github.com/restic/restic/pull/1445
|
||||
|
||||
* The s3 backend used the subdir `restic` within a bucket if no explicit path
|
||||
after the bucket name was specified. Since this version, restic does not use
|
||||
this default path any more. If you created a repo on s3 in a bucket without
|
||||
specifying a path within the bucket, you need to add `/restic` at the end of
|
||||
the repository specification to access your repo: `s3:s3.amazonaws.com/bucket/restic`
|
||||
https://github.com/restic/restic/issues/1292
|
||||
https://github.com/restic/restic/pull/1437
|
||||
|
||||
* We've added a local cache for metadata so that restic doesn't need to load
|
||||
all metadata (snapshots, indexes, ...) from the repo each time it starts. By
|
||||
default the cache is active, but there's a new global option `--no-cache`
|
||||
that can be used to disable the cache. By deafult, the cache a standard
|
||||
cache folder for the OS, which can be overridden with `--cache-dir`. The
|
||||
cache will automatically populate, indexes and snapshots are saved as they
|
||||
are loaded.
|
||||
https://github.com/restic/restic/pull/1040
|
||||
https://github.com/restic/restic/issues/29
|
||||
https://github.com/restic/restic/issues/738
|
||||
https://github.com/restic/restic/issues/282
|
||||
https://github.com/restic/restic/pull/1287
|
||||
|
||||
* A related change was to by default create pack files in the repo that
|
||||
contain either data or metadata, not both mixed together. This allows easy
|
||||
caching of only the metadata files. The next run of `restic prune` will
|
||||
untangle mixed files automatically.
|
||||
https://github.com/restic/restic/pull/1265
|
||||
|
||||
* The Google Cloud Storage backend no longer requires the service account to
|
||||
have the `storage.buckets.get` permission ("Storage Admin" role) in `restic
|
||||
init` if the bucket already exists.
|
||||
https://github.com/restic/restic/pull/1281
|
||||
|
||||
* Added support for rate limiting through `--limit-upload` and
|
||||
`--limit-download` flags.
|
||||
https://github.com/restic/restic/issues/1216
|
||||
https://github.com/restic/restic/pull/1336
|
||||
https://github.com/restic/restic/pull/1358
|
||||
|
||||
* Failed backend requests are now automatically retried.
|
||||
https://github.com/restic/restic/pull/1353
|
||||
|
||||
* We've added the `dump` command which prints a file from a snapshot to
|
||||
stdout. This can e.g. be used to restore files read with `backup --stdin`.
|
||||
https://github.com/restic/restic/issues/510
|
||||
https://github.com/restic/restic/pull/1346
|
||||
|
||||
Small changes
|
||||
-------------
|
||||
|
||||
* The directory structure in the fuse mount now exposes a symlink `latest`
|
||||
which points to the latest snapshot in that particular directory.
|
||||
https://github.com/restic/restic/pull/1249
|
||||
|
||||
* The option `--compact` was added to the `forget` command to provide the same
|
||||
compact view as the `snapshots` command.
|
||||
https://github.com/restic/restic/pull/1269
|
||||
|
||||
* We've re-enabled a workaround for `minio-go` (the library we're using to
|
||||
access s3 backends), this reduces memory usage.
|
||||
https://github.com/restic/restic/issues/1256
|
||||
https://github.com/restic/restic/pull/1267
|
||||
|
||||
* The sftp backend now prompts for the password if a password is necessary for
|
||||
login.
|
||||
https://github.com/restic/restic/issues/448
|
||||
https://github.com/restic/restic/pull/1270
|
||||
|
||||
* The `generate` command has been added, which replaces the now removed
|
||||
commands `manpage` and `autocomplete`. This release of restic contains the
|
||||
most recent manpages in `doc/man` and the auto-completion files for bash and
|
||||
zsh in `doc/bash-completion.sh` and `doc/zsh-completion.zsh`
|
||||
https://github.com/restic/restic/issues/1274
|
||||
https://github.com/restic/restic/pull/1282
|
||||
|
||||
* A bug was discovered in the library we're using to access Backblaze, it now
|
||||
reuses already established TCP connections which should be a lot faster and
|
||||
not cause network failures any more.
|
||||
https://github.com/restic/restic/issues/1291
|
||||
https://github.com/restic/restic/pull/1301
|
||||
|
||||
* Another bug in the `forget` command caused `prune` not to be run when
|
||||
`--prune` was specified without a policy, e.g. when only snapshot IDs that
|
||||
should be forgotten are listed manually. This is corrected now.
|
||||
https://github.com/restic/restic/pull/1317
|
||||
|
||||
* The `check` command now explicetly prints `No errors were found` when no
|
||||
errors could be found.
|
||||
https://github.com/restic/restic/pull/1319
|
||||
https://github.com/restic/restic/issues/1303
|
||||
|
||||
* The fuse mount now has an `ids` subdirectory which contains the snapshots
|
||||
below their (short) IDs.
|
||||
https://github.com/restic/restic/issues/1102
|
||||
https://github.com/restic/restic/pull/1299
|
||||
https://github.com/restic/restic/pull/1320
|
||||
|
||||
* The `backup` command was improved, it now caches the result of excludes for
|
||||
a directory.
|
||||
https://github.com/restic/restic/issues/1271
|
||||
https://github.com/restic/restic/pull/1326
|
||||
|
||||
* We've added the `--cacert` option which can be used to pass one (or more) CA
|
||||
certificates to restic. These are used in addition to the system CA
|
||||
certificates to verify HTTPS certificates (e.g. for the REST backend).
|
||||
https://github.com/restic/restic/issues/1114
|
||||
https://github.com/restic/restic/pull/1276
|
||||
|
||||
* When the list of files/dirs to be saved is read from a file with
|
||||
`--files-from`, comment lines (starting with `#`) are now ignored.
|
||||
https://github.com/restic/restic/issues/1367
|
||||
https://github.com/restic/restic/pull/1368
|
||||
|
||||
Important Changes in 0.7.3
|
||||
==========================
|
||||
|
||||
* For large backups stored in Google Cloud Storage, the `prune` command fails
|
||||
because listing only returns the first 1000 files. This has been corrected,
|
||||
no data is lost in the process. In addition, a plausibility check was added
|
||||
to `prune`.
|
||||
https://github.com/restic/restic/issues/1246
|
||||
https://github.com/restic/restic/pull/1247
|
||||
|
||||
|
||||
Important Changes in 0.7.2
|
||||
==========================
|
||||
|
||||
* We've added an official docker image and a Dockerfile to build this image in
|
||||
`docker/`.
|
||||
https://github.com/restic/restic/pull/1061
|
||||
|
||||
* The git repository layout was changed to resemble the layout typically used
|
||||
in Go projects, we're not using `gb` for building restic any more and
|
||||
vendoring the dependencies is now taken care of by `dep`.
|
||||
https://github.com/restic/restic/pull/1126
|
||||
|
||||
* We now support saving backups on Google Cloud Storage.
|
||||
https://github.com/restic/restic/pull/1134
|
||||
https://github.com/restic/restic/pull/1052
|
||||
https://github.com/restic/restic/issues/211
|
||||
|
||||
* We've added support for Microsoft Azure Blob Storage as a restic backend.
|
||||
https://github.com/restic/restic/pull/1149
|
||||
https://github.com/restic/restic/pull/1059
|
||||
https://github.com/restic/restic/issues/609
|
||||
|
||||
* In the course of supporting Microsoft Azure Blobe Storage Go 1.8 is now a
|
||||
requirement to build restic.
|
||||
|
||||
* The `restore` command has been improved: When dirs are excluded (or not
|
||||
included) in a restore, they are not loaded from the repo any more.
|
||||
https://github.com/restic/restic/pull/1044
|
||||
|
||||
* Name collisions are now resolved by appending a counter.
|
||||
https://github.com/restic/restic/issues/1179
|
||||
https://github.com/restic/restic/pull/1209
|
||||
|
||||
|
||||
Small changes
|
||||
-------------
|
||||
|
||||
* The `key` command now prompts for a password even if the original password
|
||||
to access a repo has been specified via the `RESTIC_PASSWORD` environment
|
||||
variable or a password file.
|
||||
https://github.com/restic/restic/issues/1132
|
||||
https://github.com/restic/restic/pull/1133
|
||||
|
||||
* Properly report errors when reading files with exclude patterns.
|
||||
https://github.com/restic/restic/pull/1144
|
||||
|
||||
* We now automatically generate man pages for all restic commands, see the
|
||||
subdir `doc/man`.
|
||||
https://github.com/restic/restic/issues/697
|
||||
https://github.com/restic/restic/pull/1147
|
||||
|
||||
* The `key remove` command was corrected and now works as documented.
|
||||
https://github.com/restic/restic/pull/1164
|
||||
|
||||
* When a restic command other than `init` is used with a local repository and
|
||||
the repository directory does not exist, restic creates the directory
|
||||
structure. That's an error, only the `init` command should create the dir.
|
||||
https://github.com/restic/restic/issues/1167
|
||||
https://github.com/restic/restic/pull/1182
|
||||
|
||||
* Restic now prints stats on all BSD systems (not only on darwin) when SIGINFO
|
||||
is received (usually when ctrl+t is pressed).
|
||||
https://github.com/restic/restic/pull/1203
|
||||
https://github.com/restic/restic/pull/1082#issuecomment-326279920
|
||||
|
||||
* Since a few releases restic had the ability to write profiling files for
|
||||
memory and CPU usage when `debug` is enabled. It was discovered that when
|
||||
restic is interrupted (ctrl+c is pressed), the proper shutdown hook is not
|
||||
run. This is now corrected.
|
||||
https://github.com/restic/restic/pull/1191
|
||||
|
||||
* A new option `--exclude-caches` was added that allows excluding cache
|
||||
directories (that are tagged as such). This is a special case of a more
|
||||
generic option `--exclude-if-present` which excludes a directory if a file
|
||||
with a specific name (and contents) is present.
|
||||
https://github.com/restic/restic/issues/317
|
||||
https://github.com/restic/restic/pull/1170
|
||||
https://github.com/restic/restic/pull/1224
|
||||
|
||||
* The `forget` command now has an option `--group-by` that allows flexible
|
||||
grouping policies.
|
||||
https://github.com/restic/restic/pull/1196
|
||||
|
||||
* The date and time restic records for a new backup can now be specified
|
||||
externally by passing `--time` to the `backup` command.
|
||||
https://github.com/restic/restic/pull/1205
|
||||
|
||||
* The option `--compact` was added to the `snapshots` command to get a better
|
||||
overview of the snapshots in a repo. It limits each snapshot to a single
|
||||
line.
|
||||
https://github.com/restic/restic/issues/1218
|
||||
https://github.com/restic/restic/pull/1223
|
||||
|
||||
|
||||
Important Changes in 0.7.1
|
||||
==========================
|
||||
|
||||
* The `migrate` command for chaning the `s3legacy` layout to the `default`
|
||||
layout for s3 backends has been improved: It can now be restarted with
|
||||
`restic migrate --force s3_layout` and automatically retries operations on
|
||||
error.
|
||||
https://github.com/restic/restic/issues/1073
|
||||
https://github.com/restic/restic/pull/1075
|
||||
|
||||
Small changes
|
||||
-------------
|
||||
|
||||
* The local and sftp backends now create the subdirs below `data/` on
|
||||
open/init. This way, restic makes sure that they always exist. This is
|
||||
connected to an issue for the sftp server:
|
||||
https://github.com/restic/rest-server/pull/11#issuecomment-309879710
|
||||
https://github.com/restic/restic/issues/1055
|
||||
https://github.com/restic/restic/pull/1077
|
||||
https://github.com/restic/restic/pull/1105
|
||||
|
||||
* When no S3 credentials are specified in the environment variables, restic
|
||||
now tries to load credentials from an IAM instance profile when the s3
|
||||
backend is used.
|
||||
https://github.com/restic/restic/issues/1067
|
||||
https://github.com/restic/restic/pull/1086
|
||||
|
||||
* On Darwin and FreeBSD, restic now prints stats when SIGINFO is received
|
||||
(usually when ctrl+t is pressed).
|
||||
https://github.com/restic/restic/pull/1082
|
||||
|
||||
* The dependencies have been updated.
|
||||
https://github.com/restic/restic/pull/1108
|
||||
https://github.com/restic/restic/pull/1124
|
||||
|
||||
* A bug was found (and corrected) in the index rebuilding after prune, which
|
||||
led to indexes which include blobs that were not present in the repo any
|
||||
more. There were already checks in place which detected this situation and
|
||||
aborted with an error message. A new run of either `prune` or
|
||||
`rebuild-index` corrected the index files. This is now fixed and a test has
|
||||
been added to detect this.
|
||||
https://github.com/restic/restic/pull/1115
|
||||
|
||||
* Errors for chmod() on Unix for filesystems which do not support it (e.g. smb
|
||||
mounted via gvfs) are now ignored.
|
||||
https://github.com/restic/restic/pull/1080
|
||||
https://github.com/restic/restic/pull/1112
|
||||
|
||||
* The semantic for the `--tags` option to `forget` and `snapshots` was
|
||||
clarified:
|
||||
https://github.com/restic/restic/issues/1081
|
||||
https://github.com/restic/restic/pull/1090
|
||||
|
||||
Important Changes in 0.7.0
|
||||
==========================
|
||||
|
||||
* New "swift" backend: A new backend for the OpenStack Swift cloud storage
|
||||
|
||||
@@ -60,50 +60,35 @@ uploading it somewhere or post only the parts that are really relevant.
|
||||
Development Environment
|
||||
=======================
|
||||
|
||||
For development you need the build tool [`gb`](https://getgb.io), it can be
|
||||
installed by running the following command:
|
||||
In order to compile restic with the `go` tool directly, it needs to be checked
|
||||
out at the right path within a `GOPATH`. The concept of a `GOPATH` is explained
|
||||
in ["How to write Go code"](https://golang.org/doc/code.html).
|
||||
|
||||
$ go get github.com/constabulary/gb/...
|
||||
|
||||
The repository contains two directories with code: `src/` contains the code
|
||||
written for restic, whereas `vendor/` contains copies of libraries restic
|
||||
depends on. The libraries are managed with the `gb vendor` command.
|
||||
|
||||
Just clone the repository, `cd` to it and run `gb build` to build the binary:
|
||||
If you do not have a directory with Go code yet, executing the following
|
||||
instructions in your shell will create one for you and check out the restic
|
||||
repo:
|
||||
|
||||
$ export GOPATH="$HOME/go"
|
||||
$ mkdir -p "$GOPATH/src/github.com/restic"
|
||||
$ cd "$GOPATH/src/github.com/restic"
|
||||
$ git clone https://github.com/restic/restic
|
||||
$ cd restic
|
||||
$ gb build
|
||||
[...]
|
||||
$ bin/restic version
|
||||
|
||||
You can then build restic as follows:
|
||||
|
||||
$ go build ./cmd/restic
|
||||
$ ./restic version
|
||||
restic compiled manually
|
||||
compiled at unknown time with go1.7
|
||||
compiled with go1.8.3 on linux/amd64
|
||||
|
||||
The following commands can be used to run all the tests:
|
||||
|
||||
$ gb test
|
||||
ok github.com/restic/restic 8.174s
|
||||
[...]
|
||||
$ go test ./cmd/... ./internal/...
|
||||
|
||||
If you want to run your tests on Linux, OpenBSD or FreeBSD, you can use
|
||||
[vagrant](https://www.vagrantup.com/) with the provided `Vagrantfile` to
|
||||
quickly set up VMs and run the tests, e.g.:
|
||||
|
||||
$ vagrant up freebsd
|
||||
[...]
|
||||
|
||||
$ 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`.
|
||||
The repository contains two sets of directories with code: `cmd/` and
|
||||
`internal/` contain the code written for restic, whereas `vendor/` contains
|
||||
copies of libraries restic depends on. The libraries are managed with the
|
||||
[`dep`](https://github.com/golang/dep) tool.
|
||||
|
||||
Providing Patches
|
||||
=================
|
||||
@@ -122,7 +107,8 @@ down to the following steps:
|
||||
|
||||
2. Clone the repository locally and create a new branch. If you are working on
|
||||
the code itself, please set up the development environment as described in
|
||||
the previous section.
|
||||
the previous section. Especially take care to place your forked repository
|
||||
at the correct path (`src/github.com/restic/restic`) within your `GOPATH`.
|
||||
|
||||
3. Then commit your changes as fine grained as possible, as smaller patches,
|
||||
that handle one and only one issue are easier to discuss and merge.
|
||||
@@ -144,7 +130,7 @@ down to the following steps:
|
||||
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!
|
||||
a lot for your contribution!
|
||||
|
||||
Please provide the patches for each bug or feature in a separate branch and
|
||||
open up a pull request for each.
|
||||
@@ -170,7 +156,7 @@ what the tests are there for.
|
||||
Git Commits
|
||||
-----------
|
||||
|
||||
I would be good if you could follow the same general style regarding Git
|
||||
It would be good if you could follow the same general style regarding Git
|
||||
commits as the rest of the project, this makes reviewing code, browsing the
|
||||
history and triaging bugs much easier.
|
||||
|
||||
|
||||
57
Dockerfile
57
Dockerfile
@@ -1,57 +0,0 @@
|
||||
# This Dockerfiles configures a container that is similar to the Travis CI
|
||||
# environment and can be used to run tests locally.
|
||||
#
|
||||
# build the image:
|
||||
# docker build -t restic/test .
|
||||
#
|
||||
# run all tests and cross-compile restic:
|
||||
# docker run --rm -v $PWD:/home/travis/restic restic/test go run run_integration_tests.go -minio minio
|
||||
#
|
||||
# run interactively:
|
||||
# docker run --interactive --tty --rm -v $PWD:/home/travis/restic restic/test /bin/bash
|
||||
#
|
||||
# run a subset of tests:
|
||||
# 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.6.4 -t restic/test:go1.6.4 .
|
||||
|
||||
FROM ubuntu:14.04
|
||||
|
||||
ARG GOVERSION=1.8.3
|
||||
ARG GOARCH=amd64
|
||||
|
||||
# install dependencies
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends ca-certificates wget git build-essential openssh-server
|
||||
|
||||
# add and configure user
|
||||
ENV HOME /home/travis
|
||||
RUN useradd -m -d $HOME -s /bin/bash travis
|
||||
|
||||
# run everything below as user travis
|
||||
USER travis
|
||||
WORKDIR $HOME
|
||||
|
||||
# download and install Go
|
||||
RUN wget -q -O /tmp/go.tar.gz https://storage.googleapis.com/golang/go${GOVERSION}.linux-${GOARCH}.tar.gz
|
||||
RUN tar xf /tmp/go.tar.gz && rm -f /tmp/go.tar.gz
|
||||
ENV GOROOT $HOME/go
|
||||
ENV GOPATH $HOME/gopath
|
||||
ENV PATH $PATH:$GOROOT/bin:$GOPATH/bin:$HOME/bin
|
||||
|
||||
RUN mkdir -p $HOME/restic
|
||||
|
||||
# pre-install tools, this speeds up running the tests itself
|
||||
RUN go get github.com/constabulary/gb/...
|
||||
RUN go get golang.org/x/tools/cmd/cover
|
||||
RUN go get github.com/mitchellh/gox
|
||||
RUN go get github.com/pierrre/gotestcover
|
||||
RUN mkdir $HOME/bin \
|
||||
&& wget -q -O $HOME/bin/minio https://dl.minio.io/server/minio/release/linux-${GOARCH}/minio \
|
||||
&& chmod +x $HOME/bin/minio
|
||||
|
||||
# set TRAVIS_BUILD_DIR for integration script
|
||||
ENV TRAVIS_BUILD_DIR $HOME/restic
|
||||
|
||||
WORKDIR $HOME/restic
|
||||
219
Gopkg.lock
generated
Normal file
219
Gopkg.lock
generated
Normal file
@@ -0,0 +1,219 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "bazil.org/fuse"
|
||||
packages = [".","fs","fuseutil"]
|
||||
revision = "371fbbdaa8987b715bdd21d6adc4c9b20155f748"
|
||||
|
||||
[[projects]]
|
||||
name = "cloud.google.com/go"
|
||||
packages = ["compute/metadata"]
|
||||
revision = "eaddaf6dd7ee35fd3c2420c8d27478db176b0485"
|
||||
version = "v0.15.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Azure/azure-sdk-for-go"
|
||||
packages = ["storage"]
|
||||
revision = "509eea43b93cec2f3f17acbe2578ef58703923f8"
|
||||
version = "v11.1.1-beta"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Azure/go-autorest"
|
||||
packages = ["autorest","autorest/adal","autorest/azure","autorest/date"]
|
||||
revision = "7aa5b8a6f18b5c15910c767ab005fc4585221177"
|
||||
version = "v9.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cenkalti/backoff"
|
||||
packages = ["."]
|
||||
revision = "61153c768f31ee5f130071d08fc82b85208528de"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cpuguy83/go-md2man"
|
||||
packages = ["md2man"]
|
||||
revision = "1d903dcb749992f3741d744c0f8376b4bd7eb3e1"
|
||||
version = "v1.0.7"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "dbeaa9332f19a944acb5736b4456cfcc02140e29"
|
||||
version = "v3.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "77ed807830b4df581417e7f89eb81d4872832b72"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/elithrar/simple-scrypt"
|
||||
packages = ["."]
|
||||
revision = "2325946f714c95de4a6088202c402fbdfa64163b"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-ini/ini"
|
||||
packages = ["."]
|
||||
revision = "5b3e00af70a9484542169a976dcab8d03e601a17"
|
||||
version = "v1.30.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/juju/ratelimit"
|
||||
packages = ["."]
|
||||
revision = "5b9ff866471762aa2ab2dced63c9fb6f53921342"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/fs"
|
||||
packages = ["."]
|
||||
revision = "2788f0dbd16903de03cb8186e5c7d97b69ad387b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/kurin/blazer"
|
||||
packages = ["b2","base","internal/b2types","internal/blog"]
|
||||
revision = "e269a1a17bb6aec278c06a57cb7e8f8d0d333e04"
|
||||
version = "v0.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/minio/go-homedir"
|
||||
packages = ["."]
|
||||
revision = "21304a94172ae3a09dee2cd86a12fb6f842138c7"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/minio/minio-go"
|
||||
packages = [".","pkg/credentials","pkg/encrypt","pkg/policy","pkg/s3signer","pkg/s3utils","pkg/set"]
|
||||
revision = "4e0f567303d4cc90ceb055a451959fb9fc391fb9"
|
||||
version = "3.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ncw/swift"
|
||||
packages = ["."]
|
||||
revision = "c95c6e5c2d1a3d37fc44c8c6dc9e231c7500667d"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/profile"
|
||||
packages = ["."]
|
||||
revision = "5b67d428864e92711fcbd2f8629456121a56d91f"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/sftp"
|
||||
packages = ["."]
|
||||
revision = "98203f5a8333288eb3163b7c667d4260fe1333e9"
|
||||
version = "1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/xattr"
|
||||
packages = ["."]
|
||||
revision = "23c75e3f6c1d8b13b3dd905b011a7f38a06044b7"
|
||||
version = "v0.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/restic/chunker"
|
||||
packages = ["."]
|
||||
revision = "db83917be3b88cc307464b7d8a221c173e34a0db"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/russross/blackfriday"
|
||||
packages = ["."]
|
||||
revision = "4048872b16cc0fc2c5fd9eacf0ed2c2fedaa0c8c"
|
||||
version = "v1.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/uuid"
|
||||
packages = ["."]
|
||||
revision = "879c5887cd475cd7864858769793b2ceb0d44feb"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = [".","doc"]
|
||||
revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["curve25519","ed25519","ed25519/internal/edwards25519","pbkdf2","poly1305","scrypt","ssh","ssh/terminal"]
|
||||
revision = "edd5e9b0879d13ee6970a50153d85b8fec9f7686"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","context/ctxhttp"]
|
||||
revision = "cd69bc3fc700721b709c3a59e16e24c67b58f6ff"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [".","google","internal","jws","jwt"]
|
||||
revision = "bb50c06baba3d0c76f9d125c0719093e315b5b44"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "8dbc5d05d6edcc104950cc299a1ce6641235bc86"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "google.golang.org/api"
|
||||
packages = ["gensupport","googleapi","googleapi/internal/uritemplates","storage/v1"]
|
||||
revision = "7afc123cf726cd2f253faa3e144d2ab65477b18f"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "f0a207197cb502238ac87ca8e07b2640c02ec380a50b036e09ef87e40e31ca2d"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
21
Gopkg.toml
Normal file
21
Gopkg.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
18
LICENSE
18
LICENSE
@@ -1,19 +1,21 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2014, Alexander Neumann <alexander@bumpern.de>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. 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.
|
||||
* 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
|
||||
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
|
||||
|
||||
5
Makefile
5
Makefile
@@ -6,7 +6,8 @@ restic:
|
||||
go run build.go
|
||||
|
||||
clean:
|
||||
rm -rf restic
|
||||
rm -f restic
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
go test ./cmd/... ./internal/...
|
||||
|
||||
|
||||
35
README.rst
35
README.rst
@@ -1,4 +1,4 @@
|
||||
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks|
|
||||
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |TestCoverage|
|
||||
|
||||
Introduction
|
||||
------------
|
||||
@@ -7,11 +7,13 @@ 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>`__.
|
||||
|
||||
You can ask questions in our `Discourse forum <https://forum.restic.net>`__.
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
Once you've `installed
|
||||
<https://restic.readthedocs.io/en/latest/installation.html>`__ restic, start
|
||||
<https://restic.readthedocs.io/en/latest/020_installation.html>`__ restic, start
|
||||
off with creating a repository for your backups:
|
||||
|
||||
.. code-block:: console
|
||||
@@ -35,7 +37,26 @@ and add some data:
|
||||
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>`__.
|
||||
Next you can either use ``restic restore`` to restore files or use ``restic
|
||||
mount`` to mount the repository via fuse and browse the files from previous
|
||||
snapshots.
|
||||
|
||||
For more options check out the `online documentation <https://restic.readthedocs.io/en/latest/>`__.
|
||||
|
||||
Backends
|
||||
--------
|
||||
|
||||
Saving a backup on the same machine is nice but not a real backup strategy.
|
||||
Therefore, restic supports the following backends for storing backups natively:
|
||||
|
||||
- `Local directory <https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#local>`__
|
||||
- `sftp server (via SSH) <https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#sftp>`__
|
||||
- `HTTP REST server <https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#rest-server>`__ (`protocol <doc/rest_backend.rst>`__ `rest-server <https://github.com/restic/rest-server>`__)
|
||||
- `AWS S3 <https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3>`__ (either from Amazon or using the `Minio <https://minio.io>`__ server)
|
||||
- `OpenStack Swift <https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#openstack-swift>`__
|
||||
- `BackBlaze B2 <https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#backblaze-b2>`__
|
||||
- `Microsoft Azure Blob Storage <https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#microsoft-azure-blob-storage>`__
|
||||
- `Google Cloud Storage <https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#google-cloud-storage>`__
|
||||
|
||||
Design Principles
|
||||
-----------------
|
||||
@@ -86,7 +107,7 @@ the `development blog <https://restic.github.io/blog/>`__.
|
||||
License
|
||||
-------
|
||||
|
||||
Restic is licensed under "BSD 2-Clause License". You can find the
|
||||
Restic is licensed under `BSD 2-Clause License <https://opensource.org/licenses/BSD-2-Clause>`__. You can find the
|
||||
complete text in ``LICENSE``.
|
||||
|
||||
.. |Documentation| image:: https://readthedocs.org/projects/restic/badge/?version=latest
|
||||
@@ -95,7 +116,9 @@ complete text in ``LICENSE``.
|
||||
: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
|
||||
.. |Report Card| image:: https://goreportcard.com/badge/github.com/restic/restic
|
||||
:target: https://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
|
||||
.. |TestCoverage| image:: https://codecov.io/gh/restic/restic/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/restic/restic
|
||||
|
||||
124
Vagrantfile
vendored
124
Vagrantfile
vendored
@@ -1,124 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
GO_VERSION = '1.7'
|
||||
|
||||
def packages_freebsd
|
||||
return <<-EOF
|
||||
pkg install -y git
|
||||
pkg install -y curl
|
||||
|
||||
echo 'fuse_load="YES"' >> /boot/loader.conf
|
||||
echo 'vfs.usermount=1' >> /etc/sysctl.conf
|
||||
|
||||
kldload fuse
|
||||
sysctl vfs.usermount=1
|
||||
pw groupmod operator -M vagrant
|
||||
EOF
|
||||
end
|
||||
|
||||
def packages_openbsd
|
||||
return <<-EOF
|
||||
. ~/.profile
|
||||
pkg_add git curl bash gtar--
|
||||
ln -sf /usr/local/bin/gtar /usr/local/bin/tar
|
||||
EOF
|
||||
end
|
||||
|
||||
def packages_linux
|
||||
return <<-EOF
|
||||
apt-get update
|
||||
apt-get install -y git curl
|
||||
EOF
|
||||
end
|
||||
|
||||
def packages_darwin
|
||||
return <<-EOF
|
||||
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
brew cask install osxfuse
|
||||
EOF
|
||||
end
|
||||
|
||||
def install_gimme
|
||||
return <<-EOF
|
||||
rm -rf /opt/gimme
|
||||
mkdir -p /opt/gimme || true
|
||||
git clone https://github.com/meatballhat/gimme /opt/gimme
|
||||
perl -p -i -e 's,/bin/bash,/usr/bin/env bash,' /opt/gimme/gimme
|
||||
ln -sf /opt/gimme/gimme /usr/bin/gimme
|
||||
EOF
|
||||
end
|
||||
|
||||
def prepare_user(boxname)
|
||||
return <<-EOF
|
||||
mkdir -p ~/go/src
|
||||
export PATH=/usr/local/bin:$PATH
|
||||
|
||||
gimme #{GO_VERSION} >> ~/.profile
|
||||
echo export 'GOPATH=/vagrant/go' >> ~/.profile
|
||||
echo export 'PATH=$GOPATH/bin:/usr/local/bin:$PATH' >> ~/.profile
|
||||
|
||||
. ~/.profile
|
||||
|
||||
go get golang.org/x/tools/cmd/cover
|
||||
go get github.com/constabulary/gb/...
|
||||
|
||||
echo
|
||||
echo "Run:"
|
||||
echo " vagrant rsync #{boxname}"
|
||||
echo " vagrant ssh #{boxname} -c 'cd /vagrant; gb build && gb test'"
|
||||
EOF
|
||||
end
|
||||
|
||||
def fix_perms
|
||||
return <<-EOF
|
||||
chown -R vagrant /vagrant
|
||||
EOF
|
||||
end
|
||||
|
||||
# All Vagrant configuration is done below. The "2" in Vagrant.configure
|
||||
# configures the configuration version (we support older styles for
|
||||
# backwards compatibility). Please don't change it unless you know what
|
||||
# you're doing.
|
||||
Vagrant.configure(2) do |config|
|
||||
# use rsync to copy content to the folder
|
||||
config.vm.synced_folder ".", "/vagrant", :type => "rsync"
|
||||
|
||||
# fix permissions on synced folder
|
||||
config.vm.provision "fix perms", :type => :shell, :inline => fix_perms
|
||||
|
||||
config.vm.define "linux" do |b|
|
||||
b.vm.box = "ubuntu/trusty64"
|
||||
b.vm.provision "packages linux", :type => :shell, :inline => packages_linux
|
||||
b.vm.provision "install gimme", :type => :shell, :inline => install_gimme
|
||||
b.vm.provision "prepare user", :type => :shell, :privileged => false, :inline => prepare_user("linux")
|
||||
|
||||
# fix network card
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
v.customize ["modifyvm", :id, "--nictype1", "virtio"]
|
||||
end
|
||||
end
|
||||
|
||||
config.vm.define "freebsd" do |b|
|
||||
b.vm.box = "geoffgarside/freebsd-10.1"
|
||||
b.vm.provision "packages freebsd", :type => :shell, :inline => packages_freebsd
|
||||
b.vm.provision "install gimme", :type => :shell, :inline => install_gimme
|
||||
b.vm.provision "prepare user", :type => :shell, :privileged => false, :inline => prepare_user("freebsd")
|
||||
end
|
||||
|
||||
config.vm.define "openbsd" do |b|
|
||||
b.vm.box = "tmatilai/openbsd-5.6"
|
||||
b.vm.provision "packages openbsd", :type => :shell, :inline => packages_openbsd
|
||||
b.vm.provision "install gimme", :type => :shell, :inline => install_gimme
|
||||
b.vm.provision "prepare user", :type => :shell, :privileged => false, :inline => prepare_user("openbsd")
|
||||
end
|
||||
|
||||
config.vm.define "darwin" do |b|
|
||||
#b.vm.box = "jhcook/osx-yosemite-10.10"
|
||||
b.vm.box = "jhcook/yosemite-clitools"
|
||||
b.vm.provision "packages darwin", :type => :shell, :privileged => false, :inline => packages_darwin
|
||||
b.vm.provision "install gimme", :type => :shell, :inline => install_gimme
|
||||
b.vm.provision "prepare user", :type => :shell, :privileged => false, :inline => prepare_user("darwin")
|
||||
end
|
||||
|
||||
end
|
||||
@@ -17,8 +17,8 @@ init:
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.8.1.windows-amd64.msi
|
||||
- msiexec /i go1.8.1.windows-amd64.msi /q
|
||||
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.9.windows-amd64.msi
|
||||
- msiexec /i go1.9.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
|
||||
|
||||
34
build.go
34
build.go
@@ -27,10 +27,12 @@ var config = struct {
|
||||
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
|
||||
Name: "restic", // name of the program executable and directory
|
||||
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
||||
Main: "github.com/restic/restic/cmd/restic", // package name for the main package
|
||||
Tests: []string{ // tests to run
|
||||
"github.com/restic/restic/internal/...",
|
||||
"github.com/restic/restic/cmd/..."},
|
||||
}
|
||||
|
||||
// specialDir returns true if the file begins with a special character ('.' or '_').
|
||||
@@ -77,7 +79,12 @@ func excludePath(name string) bool {
|
||||
// └── restic
|
||||
// └── foo.go
|
||||
func updateGopath(dst, src, prefix string) error {
|
||||
verbosePrintf("copy contents of %v to %v\n", src, filepath.Join(dst, prefix))
|
||||
return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
|
||||
if name == src {
|
||||
return err
|
||||
}
|
||||
|
||||
if specialDir(name) {
|
||||
if fi.IsDir() {
|
||||
return filepath.SkipDir
|
||||
@@ -86,6 +93,10 @@ func updateGopath(dst, src, prefix string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
@@ -291,8 +302,8 @@ func (cs Constants) LDFlags() string {
|
||||
|
||||
func main() {
|
||||
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)
|
||||
if strings.HasPrefix(ver, "go1") && ver < "go1.8" {
|
||||
fmt.Fprintf(os.Stderr, "Go version %s detected, restic requires at least Go 1.8\n", ver)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -368,13 +379,13 @@ func main() {
|
||||
}
|
||||
|
||||
verbosePrintf("create GOPATH at %v\n", gopath)
|
||||
if err = updateGopath(gopath, filepath.Join(root, "src"), config.Namespace); err != nil {
|
||||
if err = updateGopath(gopath, root, 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")
|
||||
vendor := filepath.Join(root, "vendor")
|
||||
if directoryExists(vendor) {
|
||||
if err = updateGopath(gopath, vendor, ""); err != nil {
|
||||
if err = updateGopath(gopath, vendor, filepath.Join(config.Namespace, "vendor")); err != nil {
|
||||
die("copying files from %v to %v failed: %v\n", root, gopath, err)
|
||||
}
|
||||
}
|
||||
@@ -401,7 +412,10 @@ func main() {
|
||||
if err != nil {
|
||||
die("Getwd() returned %v\n", err)
|
||||
}
|
||||
output := filepath.Join(cwd, outputFilename)
|
||||
output := outputFilename
|
||||
if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(cwd, output)
|
||||
}
|
||||
|
||||
version := getVersion()
|
||||
constants := Constants{}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"restic/debug"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
)
|
||||
|
||||
// IsProcessBackground returns true if it is running in the background or false if not
|
||||
92
cmd/restic/cleanup.go
Normal file
92
cmd/restic/cleanup.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
)
|
||||
|
||||
var cleanupHandlers struct {
|
||||
sync.Mutex
|
||||
list []func() error
|
||||
done bool
|
||||
ch chan os.Signal
|
||||
}
|
||||
|
||||
var stderr = os.Stderr
|
||||
|
||||
func init() {
|
||||
cleanupHandlers.ch = make(chan os.Signal)
|
||||
go CleanupHandler(cleanupHandlers.ch)
|
||||
InstallSignalHandler()
|
||||
}
|
||||
|
||||
// InstallSignalHandler listens for SIGINT and SIGPIPE, and triggers the cleanup handlers.
|
||||
func InstallSignalHandler() {
|
||||
signal.Notify(cleanupHandlers.ch, syscall.SIGINT)
|
||||
signal.Notify(cleanupHandlers.ch, syscall.SIGPIPE)
|
||||
}
|
||||
|
||||
// SuspendSignalHandler removes the signal handler for SIGINT and SIGPIPE.
|
||||
func SuspendSignalHandler() {
|
||||
signal.Reset(syscall.SIGINT)
|
||||
signal.Reset(syscall.SIGPIPE)
|
||||
}
|
||||
|
||||
// AddCleanupHandler adds the function f to the list of cleanup handlers so
|
||||
// that it is executed when all the cleanup handlers are run, e.g. when SIGINT
|
||||
// is received.
|
||||
func AddCleanupHandler(f func() error) {
|
||||
cleanupHandlers.Lock()
|
||||
defer cleanupHandlers.Unlock()
|
||||
|
||||
// reset the done flag for integration tests
|
||||
cleanupHandlers.done = false
|
||||
|
||||
cleanupHandlers.list = append(cleanupHandlers.list, f)
|
||||
}
|
||||
|
||||
// RunCleanupHandlers runs all registered cleanup handlers
|
||||
func RunCleanupHandlers() {
|
||||
cleanupHandlers.Lock()
|
||||
defer cleanupHandlers.Unlock()
|
||||
|
||||
if cleanupHandlers.done {
|
||||
return
|
||||
}
|
||||
cleanupHandlers.done = true
|
||||
|
||||
for _, f := range cleanupHandlers.list {
|
||||
err := f()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "error in cleanup handler: %v\n", err)
|
||||
}
|
||||
}
|
||||
cleanupHandlers.list = nil
|
||||
}
|
||||
|
||||
// CleanupHandler handles the SIGINT and SIGPIPE signals.
|
||||
func CleanupHandler(c <-chan os.Signal) {
|
||||
for s := range c {
|
||||
debug.Log("signal %v received, cleaning up", s)
|
||||
fmt.Fprintf(stderr, "%ssignal %v received, cleaning up\n", ClearLine(), s)
|
||||
|
||||
code := 0
|
||||
if s != syscall.SIGINT {
|
||||
code = 1
|
||||
}
|
||||
|
||||
Exit(code)
|
||||
}
|
||||
}
|
||||
|
||||
// Exit runs the cleanup handlers and then terminates the process with the
|
||||
// given exit code.
|
||||
func Exit(code int) {
|
||||
RunCleanupHandlers()
|
||||
os.Exit(code)
|
||||
}
|
||||
520
cmd/restic/cmd_backup.go
Normal file
520
cmd/restic/cmd_backup.go
Normal file
@@ -0,0 +1,520 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/restic/restic/internal/archiver"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
var cmdBackup = &cobra.Command{
|
||||
Use: "backup [flags] FILE/DIR [FILE/DIR] ...",
|
||||
Short: "Create a new backup of files and/or directories",
|
||||
Long: `
|
||||
The "backup" command creates a new snapshot and saves the files and directories
|
||||
given as the arguments.
|
||||
`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
if backupOptions.Hostname == "" {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
debug.Log("os.Hostname() returned err: %v", err)
|
||||
return
|
||||
}
|
||||
backupOptions.Hostname = hostname
|
||||
}
|
||||
},
|
||||
DisableAutoGenTag: true,
|
||||
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)
|
||||
}
|
||||
|
||||
return runBackup(backupOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// BackupOptions bundles all options for the backup command.
|
||||
type BackupOptions struct {
|
||||
Parent string
|
||||
Force bool
|
||||
Excludes []string
|
||||
ExcludeFiles []string
|
||||
ExcludeOtherFS bool
|
||||
ExcludeIfPresent []string
|
||||
ExcludeCaches bool
|
||||
Stdin bool
|
||||
StdinFilename string
|
||||
Tags []string
|
||||
Hostname string
|
||||
FilesFrom string
|
||||
TimeStamp string
|
||||
}
|
||||
|
||||
var backupOptions BackupOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdBackup)
|
||||
|
||||
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.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
|
||||
f.StringArrayVar(&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.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
||||
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file`)
|
||||
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
|
||||
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin")
|
||||
f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
|
||||
f.StringVar(&backupOptions.Hostname, "hostname", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
|
||||
f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)")
|
||||
f.StringVar(&backupOptions.TimeStamp, "time", "", "time of the backup (ex. '2012-11-01 22:08:41') (default: now)")
|
||||
}
|
||||
|
||||
func newScanProgress(gopts GlobalOptions) *restic.Progress {
|
||||
if gopts.Quiet {
|
||||
return nil
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
|
||||
if gopts.Quiet {
|
||||
return nil
|
||||
}
|
||||
|
||||
archiveProgress := restic.NewProgress()
|
||||
|
||||
var bps, eta uint64
|
||||
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
|
||||
if s.Bytes >= todo.Bytes {
|
||||
eta = 0
|
||||
} else if bps > 0 {
|
||||
eta = (todo.Bytes - s.Bytes) / bps
|
||||
}
|
||||
}
|
||||
|
||||
itemsDone := s.Files + s.Dirs
|
||||
|
||||
status1 := fmt.Sprintf("[%s] %s %s/s %s / %s %d / %d items %d errors ",
|
||||
formatDuration(d),
|
||||
formatPercent(s.Bytes, todo.Bytes),
|
||||
formatBytes(bps),
|
||||
formatBytes(s.Bytes), formatBytes(todo.Bytes),
|
||||
itemsDone, itemsTodo,
|
||||
s.Errors)
|
||||
status2 := fmt.Sprintf("ETA %s ", formatSeconds(eta))
|
||||
|
||||
if w := stdoutTerminalWidth(); w > 0 {
|
||||
maxlen := w - len(status2) - 1
|
||||
|
||||
if maxlen < 4 {
|
||||
status1 = ""
|
||||
} else if len(status1) > maxlen {
|
||||
status1 = status1[:maxlen-4]
|
||||
status1 += "... "
|
||||
}
|
||||
}
|
||||
|
||||
PrintProgress("%s%s", status1, status2)
|
||||
}
|
||||
|
||||
archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||
fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(todo.Bytes, d))
|
||||
}
|
||||
|
||||
return archiveProgress
|
||||
}
|
||||
|
||||
func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress {
|
||||
if gopts.Quiet {
|
||||
return nil
|
||||
}
|
||||
|
||||
archiveProgress := restic.NewProgress()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
status1 := fmt.Sprintf("[%s] %s %s/s", formatDuration(d),
|
||||
formatBytes(s.Bytes),
|
||||
formatBytes(bps))
|
||||
|
||||
if w := stdoutTerminalWidth(); w > 0 {
|
||||
maxlen := w - len(status1)
|
||||
|
||||
if maxlen < 4 {
|
||||
status1 = ""
|
||||
} else if len(status1) > maxlen {
|
||||
status1 = status1[:maxlen-4]
|
||||
status1 += "... "
|
||||
}
|
||||
}
|
||||
|
||||
PrintProgress("%s", status1)
|
||||
}
|
||||
|
||||
archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||
fmt.Printf("\nduration: %s, %s\n", formatDuration(d), formatRate(s.Bytes, d))
|
||||
}
|
||||
|
||||
return archiveProgress
|
||||
}
|
||||
|
||||
// filterExisting returns a slice of all existing items, or an error if no
|
||||
// items exist at all.
|
||||
func filterExisting(items []string) (result []string, err error) {
|
||||
for _, item := range items {
|
||||
_, err := fs.Lstat(item)
|
||||
if err != nil && os.IsNotExist(errors.Cause(err)) {
|
||||
Warnf("%v does not exist, skipping\n", item)
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return nil, errors.Fatal("all target directories/files do not exist")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error {
|
||||
if len(args) != 0 {
|
||||
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 == "" {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := &archiver.Reader{
|
||||
Repository: repo,
|
||||
Tags: opts.Tags,
|
||||
Hostname: opts.Hostname,
|
||||
}
|
||||
|
||||
_, id, err := r.Archive(context.TODO(), opts.StdinFilename, os.Stdin, newArchiveStdinProgress(gopts))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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()
|
||||
// ignore empty lines
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// strip comments
|
||||
if strings.HasPrefix(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 == "" {
|
||||
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
|
||||
}
|
||||
|
||||
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.Fatal("nothing to backup, please specify target files/dirs")
|
||||
}
|
||||
|
||||
target := make([]string, 0, len(args))
|
||||
for _, d := range args {
|
||||
if a, err := filepath.Abs(d); err == nil {
|
||||
d = a
|
||||
}
|
||||
target = append(target, d)
|
||||
}
|
||||
|
||||
target, err = filterExisting(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// rejectFuncs collect functions that can reject items from the backup
|
||||
var rejectFuncs []RejectFunc
|
||||
|
||||
// allowed devices
|
||||
if opts.ExcludeOtherFS {
|
||||
f, err := rejectByDevice(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rejectFuncs = append(rejectFuncs, f)
|
||||
}
|
||||
|
||||
// add patterns from file
|
||||
if len(opts.ExcludeFiles) > 0 {
|
||||
opts.Excludes = append(opts.Excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...)
|
||||
}
|
||||
|
||||
if len(opts.Excludes) > 0 {
|
||||
rejectFuncs = append(rejectFuncs, rejectByPattern(opts.Excludes))
|
||||
}
|
||||
|
||||
if opts.ExcludeCaches {
|
||||
opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55")
|
||||
}
|
||||
|
||||
rc := &rejectionCache{}
|
||||
for _, spec := range opts.ExcludeIfPresent {
|
||||
f, err := rejectIfPresent(spec, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rejectFuncs = append(rejectFuncs, f)
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// exclude restic cache
|
||||
if repo.Cache != nil {
|
||||
f, err := rejectResticCache(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rejectFuncs = append(rejectFuncs, f)
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var parentSnapshotID *restic.ID
|
||||
|
||||
// Force using a parent
|
||||
if !opts.Force && opts.Parent != "" {
|
||||
id, err := restic.FindSnapshot(repo, opts.Parent)
|
||||
if err != nil {
|
||||
return errors.Fatalf("invalid id %q: %v", opts.Parent, err)
|
||||
}
|
||||
|
||||
parentSnapshotID = &id
|
||||
}
|
||||
|
||||
// Find last snapshot to set it as parent, if not already set
|
||||
if !opts.Force && parentSnapshotID == nil {
|
||||
id, err := restic.FindLatestSnapshot(context.TODO(), repo, target, []restic.TagList{}, opts.Hostname)
|
||||
if err == nil {
|
||||
parentSnapshotID = &id
|
||||
} else if err != restic.ErrNoSnapshotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if parentSnapshotID != nil {
|
||||
Verbosef("using parent snapshot %v\n", parentSnapshotID.Str())
|
||||
}
|
||||
|
||||
Verbosef("scan %v\n", target)
|
||||
|
||||
selectFilter := func(item string, fi os.FileInfo) bool {
|
||||
for _, reject := range rejectFuncs {
|
||||
if reject(item, fi) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
stat, err := archiver.Scan(target, selectFilter, newScanProgress(gopts))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
arch := archiver.New(repo)
|
||||
arch.Excludes = opts.Excludes
|
||||
arch.SelectFilter = selectFilter
|
||||
|
||||
arch.Warn = func(dir string, fi os.FileInfo, err error) {
|
||||
// TODO: make ignoring errors configurable
|
||||
Warnf("%s\rwarning for %s: %v\n", ClearLine(), dir, err)
|
||||
}
|
||||
|
||||
timeStamp := time.Now()
|
||||
if opts.TimeStamp != "" {
|
||||
timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp)
|
||||
if err != nil {
|
||||
return errors.Fatalf("error in time option: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, id, err := arch.Snapshot(context.TODO(), newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID, timeStamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Verbosef("snapshot %s saved\n", id.Str())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readExcludePatternsFromFiles(excludeFiles []string) []string {
|
||||
var excludes []string
|
||||
for _, filename := range excludeFiles {
|
||||
err := func() (err error) {
|
||||
file, err := fs.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
// return pre-close error if there was one
|
||||
if errClose := file.Close(); err == nil {
|
||||
err = errClose
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
line = os.ExpandEnv(line)
|
||||
excludes = append(excludes, line)
|
||||
}
|
||||
return scanner.Err()
|
||||
}()
|
||||
if err != nil {
|
||||
Warnf("error reading exclude patterns: %v:", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return excludes
|
||||
}
|
||||
@@ -8,18 +8,19 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"restic"
|
||||
"restic/backend"
|
||||
"restic/errors"
|
||||
"restic/repository"
|
||||
"github.com/restic/restic/internal/backend"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
var cmdCat = &cobra.Command{
|
||||
Use: "cat [flags] [pack|blob|snapshot|index|key|masterkey|config|lock] ID",
|
||||
Short: "print internal objects to stdout",
|
||||
Short: "Print internal objects to stdout",
|
||||
Long: `
|
||||
The "cat" command is used to print internal objects to stdout.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runCat(globalOptions, args)
|
||||
},
|
||||
@@ -8,18 +8,22 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"restic"
|
||||
"restic/checker"
|
||||
"restic/errors"
|
||||
"github.com/restic/restic/internal/checker"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
var cmdCheck = &cobra.Command{
|
||||
Use: "check [flags]",
|
||||
Short: "check the repository for errors",
|
||||
Short: "Check the repository for errors",
|
||||
Long: `
|
||||
The "check" command tests the repository for errors and reports any errors it
|
||||
finds. It can also be used to read all data and therefore simulate a restore.
|
||||
|
||||
By default, the "check" command will always load all data directly from the
|
||||
repository and not use a local cache.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runCheck(checkOptions, globalOptions, args)
|
||||
},
|
||||
@@ -29,6 +33,7 @@ finds. It can also be used to read all data and therefore simulate a restore.
|
||||
type CheckOptions struct {
|
||||
ReadData bool
|
||||
CheckUnused bool
|
||||
WithCache bool
|
||||
}
|
||||
|
||||
var checkOptions CheckOptions
|
||||
@@ -39,6 +44,7 @@ func init() {
|
||||
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.WithCache, "with-cache", false, "use the cache")
|
||||
}
|
||||
|
||||
func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
|
||||
@@ -76,13 +82,18 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
return errors.Fatal("check has no arguments")
|
||||
}
|
||||
|
||||
if !opts.WithCache {
|
||||
// do not use a cache for the checker
|
||||
gopts.NoCache = true
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
Verbosef("Create exclusive lock for repository\n")
|
||||
Verbosef("create exclusive lock for repository\n")
|
||||
lock, err := lockRepoExclusive(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
@@ -92,7 +103,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
|
||||
chkr := checker.New(repo)
|
||||
|
||||
Verbosef("Load indexes\n")
|
||||
Verbosef("load indexes\n")
|
||||
hints, errs := chkr.LoadIndex(context.TODO())
|
||||
|
||||
dupFound := false
|
||||
@@ -117,7 +128,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
errorsFound := false
|
||||
errChan := make(chan error)
|
||||
|
||||
Verbosef("Check all packs\n")
|
||||
Verbosef("check all packs\n")
|
||||
go chkr.Packs(context.TODO(), errChan)
|
||||
|
||||
for err := range errChan {
|
||||
@@ -125,7 +136,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
}
|
||||
|
||||
Verbosef("Check snapshots, trees and blobs\n")
|
||||
Verbosef("check snapshots, trees and blobs\n")
|
||||
errChan = make(chan error)
|
||||
go chkr.Structure(context.TODO(), errChan)
|
||||
|
||||
@@ -149,7 +160,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
if opts.ReadData {
|
||||
Verbosef("Read all data\n")
|
||||
Verbosef("read all data\n")
|
||||
|
||||
p := newReadProgress(gopts, restic.Stat{Blobs: chkr.CountPacks()})
|
||||
errChan := make(chan error)
|
||||
@@ -165,5 +176,8 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
if errorsFound {
|
||||
return errors.Fatal("repository contains errors")
|
||||
}
|
||||
|
||||
Verbosef("no errors were found\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
217
cmd/restic/cmd_debug.go
Normal file
217
cmd/restic/cmd_debug.go
Normal file
@@ -0,0 +1,217 @@
|
||||
// +build debug
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/pack"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/restic/restic/internal/worker"
|
||||
)
|
||||
|
||||
var cmdDebug = &cobra.Command{
|
||||
Use: "debug",
|
||||
Short: "Debug commands",
|
||||
}
|
||||
|
||||
var cmdDebugDump = &cobra.Command{
|
||||
Use: "dump [indexes|snapshots|all|packs]",
|
||||
Short: "Dump data structures",
|
||||
Long: `
|
||||
The "dump" command dumps data structures from the repository as JSON objects. It
|
||||
is used for debugging purposes only.`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDebugDump(globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdDebug)
|
||||
cmdDebug.AddCommand(cmdDebugDump)
|
||||
}
|
||||
|
||||
func prettyPrintJSON(wr io.Writer, item interface{}) error {
|
||||
buf, err := json.MarshalIndent(item, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = wr.Write(append(buf, '\n'))
|
||||
return err
|
||||
}
|
||||
|
||||
func debugPrintSnapshots(repo *repository.Repository, wr io.Writer) error {
|
||||
for id := range repo.List(context.TODO(), restic.SnapshotFile) {
|
||||
snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "LoadSnapshot(%v): %v", id.Str(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(wr, "snapshot_id: %v\n", id)
|
||||
|
||||
err = prettyPrintJSON(wr, snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const dumpPackWorkers = 10
|
||||
|
||||
// Pack is the struct used in printPacks.
|
||||
type Pack struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
Blobs []Blob `json:"blobs"`
|
||||
}
|
||||
|
||||
// Blob is the struct used in printPacks.
|
||||
type Blob struct {
|
||||
Type restic.BlobType `json:"type"`
|
||||
Length uint `json:"length"`
|
||||
ID restic.ID `json:"id"`
|
||||
Offset uint `json:"offset"`
|
||||
}
|
||||
|
||||
func printPacks(repo *repository.Repository, wr io.Writer) error {
|
||||
f := func(ctx context.Context, job worker.Job) (interface{}, error) {
|
||||
name := job.Data.(string)
|
||||
|
||||
h := restic.Handle{Type: restic.DataFile, Name: name}
|
||||
|
||||
blobInfo, err := repo.Backend().Stat(ctx, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blobs, err := pack.List(repo.Key(), restic.ReaderAt(repo.Backend(), h), blobInfo.Size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blobs, nil
|
||||
}
|
||||
|
||||
jobCh := make(chan worker.Job)
|
||||
resCh := make(chan worker.Job)
|
||||
wp := worker.New(context.TODO(), dumpPackWorkers, f, jobCh, resCh)
|
||||
|
||||
go func() {
|
||||
for name := range repo.Backend().List(context.TODO(), restic.DataFile) {
|
||||
jobCh <- worker.Job{Data: name}
|
||||
}
|
||||
close(jobCh)
|
||||
}()
|
||||
|
||||
for job := range resCh {
|
||||
name := job.Data.(string)
|
||||
|
||||
if job.Error != nil {
|
||||
fmt.Fprintf(os.Stderr, "error for pack %v: %v\n", name, job.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
entries := job.Result.([]restic.Blob)
|
||||
p := Pack{
|
||||
Name: name,
|
||||
Blobs: make([]Blob, len(entries)),
|
||||
}
|
||||
for i, blob := range entries {
|
||||
p.Blobs[i] = Blob{
|
||||
Type: blob.Type,
|
||||
Length: blob.Length,
|
||||
ID: blob.ID,
|
||||
Offset: blob.Offset,
|
||||
}
|
||||
}
|
||||
|
||||
prettyPrintJSON(os.Stdout, p)
|
||||
}
|
||||
|
||||
wp.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dumpIndexes(repo restic.Repository) error {
|
||||
for id := range repo.List(context.TODO(), restic.IndexFile) {
|
||||
fmt.Printf("index_id: %v\n", id)
|
||||
|
||||
idx, err := repository.LoadIndex(context.TODO(), repo, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = idx.Dump(os.Stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDebugDump(gopts GlobalOptions, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.Fatal("type not specified")
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpe := args[0]
|
||||
|
||||
switch tpe {
|
||||
case "indexes":
|
||||
return dumpIndexes(repo)
|
||||
case "snapshots":
|
||||
return debugPrintSnapshots(repo, os.Stdout)
|
||||
case "packs":
|
||||
return printPacks(repo, os.Stdout)
|
||||
case "all":
|
||||
fmt.Printf("snapshots:\n")
|
||||
err := debugPrintSnapshots(repo, os.Stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("\nindexes:\n")
|
||||
err = dumpIndexes(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return errors.Fatalf("no such type %q", tpe)
|
||||
}
|
||||
}
|
||||
181
cmd/restic/cmd_dump.go
Normal file
181
cmd/restic/cmd_dump.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdDump = &cobra.Command{
|
||||
Use: "dump [flags] snapshotID file",
|
||||
Short: "Print a backed-up file to stdout",
|
||||
Long: `
|
||||
The "dump" command extracts a single file from a snapshot from the repository and
|
||||
prints its contents to stdout.
|
||||
|
||||
The special snapshot "latest" can be used to use the latest snapshot in the
|
||||
repository.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDump(dumpOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// DumpOptions collects all options for the dump command.
|
||||
type DumpOptions struct {
|
||||
Host string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
}
|
||||
|
||||
var dumpOptions DumpOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdDump)
|
||||
|
||||
flags := cmdDump.Flags()
|
||||
flags.StringVarP(&dumpOptions.Host, "host", "H", "", `only consider snapshots for this host when the snapshot ID is "latest"`)
|
||||
flags.Var(&dumpOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"")
|
||||
flags.StringArrayVar(&dumpOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
|
||||
}
|
||||
|
||||
func splitPath(path string) []string {
|
||||
d, f := filepath.Split(path)
|
||||
if d == "" || d == "/" {
|
||||
return []string{f}
|
||||
}
|
||||
s := splitPath(filepath.Clean(d))
|
||||
return append(s, f)
|
||||
}
|
||||
|
||||
func dumpNode(ctx context.Context, repo restic.Repository, node *restic.Node) error {
|
||||
var buf []byte
|
||||
for _, id := range node.Content {
|
||||
size, err := repo.LookupBlobSize(id, restic.DataBlob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf = buf[:cap(buf)]
|
||||
if len(buf) < restic.CiphertextLength(int(size)) {
|
||||
buf = restic.NewBlobBuffer(int(size))
|
||||
}
|
||||
|
||||
n, err := repo.LoadBlob(ctx, restic.DataBlob, id, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
_, err = os.Stdout.Write(buf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Write")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string) error {
|
||||
if tree == nil {
|
||||
return fmt.Errorf("called with a nil tree")
|
||||
}
|
||||
if repo == nil {
|
||||
return fmt.Errorf("called with a nil repository")
|
||||
}
|
||||
l := len(pathComponents)
|
||||
if l == 0 {
|
||||
return fmt.Errorf("empty path components")
|
||||
}
|
||||
item := filepath.Join(prefix, pathComponents[0])
|
||||
for _, node := range tree.Nodes {
|
||||
if node.Name == pathComponents[0] {
|
||||
switch {
|
||||
case l == 1 && node.Type == "file":
|
||||
return dumpNode(ctx, repo, node)
|
||||
case l > 1 && node.Type == "dir":
|
||||
subtree, err := repo.LoadTree(ctx, *node.Subtree)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
||||
}
|
||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:])
|
||||
case l > 1:
|
||||
return fmt.Errorf("%q should be a dir, but s a %q", item, node.Type)
|
||||
case node.Type != "file":
|
||||
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("path %q not found in snapshot", item)
|
||||
}
|
||||
|
||||
func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
||||
ctx := gopts.ctx
|
||||
|
||||
if len(args) != 2 {
|
||||
return errors.Fatal("no file and no snapshot ID specified")
|
||||
}
|
||||
|
||||
snapshotIDString := args[0]
|
||||
pathToPrint := args[1]
|
||||
|
||||
debug.Log("dump file %q from %q", pathToPrint, snapshotIDString)
|
||||
|
||||
splittedPath := splitPath(pathToPrint)
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var id restic.ID
|
||||
|
||||
if snapshotIDString == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host)
|
||||
if err != nil {
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
|
||||
}
|
||||
} else {
|
||||
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||
if err != nil {
|
||||
Exitf(1, "invalid id %q: %v", snapshotIDString, err)
|
||||
}
|
||||
}
|
||||
|
||||
sn, err := restic.LoadSnapshot(context.TODO(), repo, id)
|
||||
if err != nil {
|
||||
Exitf(2, "loading snapshot %q failed: %v", snapshotIDString, err)
|
||||
}
|
||||
|
||||
tree, err := repo.LoadTree(ctx, *sn.Tree)
|
||||
if err != nil {
|
||||
Exitf(2, "loading tree for snapshot %q failed: %v", snapshotIDString, err)
|
||||
}
|
||||
|
||||
err = printFromTree(ctx, tree, repo, "", splittedPath)
|
||||
if err != nil {
|
||||
Exitf(2, "cannot dump file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,17 +9,18 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"restic"
|
||||
"restic/debug"
|
||||
"restic/errors"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
var cmdFind = &cobra.Command{
|
||||
Use: "find [flags] PATTERN",
|
||||
Short: "find a file or directory",
|
||||
Short: "Find a file or directory",
|
||||
Long: `
|
||||
The "find" command searches for files or directories in snapshots stored in the
|
||||
repo. `,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runFind(findOptions, globalOptions, args)
|
||||
},
|
||||
@@ -34,7 +35,7 @@ type FindOptions struct {
|
||||
ListLong bool
|
||||
Host string
|
||||
Paths []string
|
||||
Tags []string
|
||||
Tags restic.TagLists
|
||||
}
|
||||
|
||||
var findOptions FindOptions
|
||||
@@ -45,13 +46,13 @@ func init() {
|
||||
f := cmdFind.Flags()
|
||||
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.StringArrayVarP(&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")
|
||||
f.Var(&findOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given")
|
||||
f.StringArrayVar(&findOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot-ID is given")
|
||||
}
|
||||
|
||||
type findPattern struct {
|
||||
242
cmd/restic/cmd_forget.go
Normal file
242
cmd/restic/cmd_forget.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdForget = &cobra.Command{
|
||||
Use: "forget [flags] [snapshot ID] [...]",
|
||||
Short: "Remove snapshots from the repository",
|
||||
Long: `
|
||||
The "forget" command removes snapshots according to a policy. Please note that
|
||||
this command really only deletes the snapshot object in the repository, which
|
||||
is a reference to data stored there. In order to remove this (now unreferenced)
|
||||
data after 'forget' was run successfully, see the 'prune' command. `,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runForget(forgetOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// ForgetOptions collects all options for the forget command.
|
||||
type ForgetOptions struct {
|
||||
Last int
|
||||
Hourly int
|
||||
Daily int
|
||||
Weekly int
|
||||
Monthly int
|
||||
Yearly int
|
||||
KeepTags restic.TagLists
|
||||
|
||||
Host string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Compact bool
|
||||
|
||||
// Grouping
|
||||
GroupBy string
|
||||
DryRun bool
|
||||
Prune bool
|
||||
}
|
||||
|
||||
var forgetOptions ForgetOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdForget)
|
||||
|
||||
f := cmdForget.Flags()
|
||||
f.IntVarP(&forgetOptions.Last, "keep-last", "l", 0, "keep the last `n` snapshots")
|
||||
f.IntVarP(&forgetOptions.Hourly, "keep-hourly", "H", 0, "keep the last `n` hourly snapshots")
|
||||
f.IntVarP(&forgetOptions.Daily, "keep-daily", "d", 0, "keep the last `n` daily snapshots")
|
||||
f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots")
|
||||
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.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
|
||||
// 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.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")
|
||||
f.StringArrayVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
|
||||
f.BoolVarP(&forgetOptions.Compact, "compact", "c", false, "use compact format")
|
||||
|
||||
f.StringVarP(&forgetOptions.GroupBy, "group-by", "g", "host,paths", "string for grouping snapshots by host,paths,tags")
|
||||
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")
|
||||
|
||||
f.SortFlags = false
|
||||
}
|
||||
|
||||
func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, err := lockRepoExclusive(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// group by hostname and dirs
|
||||
type key struct {
|
||||
Hostname string
|
||||
Paths []string
|
||||
Tags []string
|
||||
}
|
||||
snapshotGroups := make(map[string]restic.Snapshots)
|
||||
|
||||
var GroupByTag bool
|
||||
var GroupByHost bool
|
||||
var GroupByPath bool
|
||||
var GroupOptionList []string
|
||||
|
||||
GroupOptionList = strings.Split(opts.GroupBy, ",")
|
||||
|
||||
for _, option := range GroupOptionList {
|
||||
switch option {
|
||||
case "host":
|
||||
GroupByHost = true
|
||||
case "paths":
|
||||
GroupByPath = true
|
||||
case "tags":
|
||||
GroupByTag = true
|
||||
case "":
|
||||
default:
|
||||
return errors.Fatal("unknown grouping option: '" + option + "'")
|
||||
}
|
||||
}
|
||||
|
||||
removeSnapshots := 0
|
||||
|
||||
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(context.TODO(), h); err != nil {
|
||||
return err
|
||||
}
|
||||
Verbosef("removed snapshot %v\n", sn.ID().Str())
|
||||
removeSnapshots++
|
||||
} else {
|
||||
Verbosef("would have removed snapshot %v\n", sn.ID().Str())
|
||||
}
|
||||
} else {
|
||||
// Determining grouping-keys
|
||||
var tags []string
|
||||
var hostname string
|
||||
var paths []string
|
||||
|
||||
if GroupByTag {
|
||||
tags = sn.Tags
|
||||
sort.StringSlice(tags).Sort()
|
||||
}
|
||||
if GroupByHost {
|
||||
hostname = sn.Hostname
|
||||
}
|
||||
if GroupByPath {
|
||||
paths = sn.Paths
|
||||
}
|
||||
|
||||
sort.StringSlice(sn.Paths).Sort()
|
||||
var k []byte
|
||||
var err error
|
||||
|
||||
k, err = json.Marshal(key{Tags: tags, Hostname: hostname, Paths: paths})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
|
||||
}
|
||||
}
|
||||
if len(args) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
policy := restic.ExpirePolicy{
|
||||
Last: opts.Last,
|
||||
Hourly: opts.Hourly,
|
||||
Daily: opts.Daily,
|
||||
Weekly: opts.Weekly,
|
||||
Monthly: opts.Monthly,
|
||||
Yearly: opts.Yearly,
|
||||
Tags: opts.KeepTags,
|
||||
}
|
||||
|
||||
if policy.Empty() {
|
||||
Verbosef("no policy was specified, no snapshots will be removed\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
for k, snapshotGroup := range snapshotGroups {
|
||||
var key key
|
||||
if json.Unmarshal([]byte(k), &key) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Info
|
||||
Verbosef("snapshots")
|
||||
var infoStrings []string
|
||||
if GroupByTag {
|
||||
infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]")
|
||||
}
|
||||
if GroupByHost {
|
||||
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
|
||||
}
|
||||
if GroupByPath {
|
||||
infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
|
||||
}
|
||||
if infoStrings != nil {
|
||||
Verbosef(" for (" + strings.Join(infoStrings, ", ") + ")")
|
||||
}
|
||||
Verbosef(":\n\n")
|
||||
|
||||
keep, remove := restic.ApplyPolicy(snapshotGroup, policy)
|
||||
|
||||
if len(keep) != 0 && !gopts.Quiet {
|
||||
Printf("keep %d snapshots:\n", len(keep))
|
||||
PrintSnapshots(globalOptions.stdout, keep, opts.Compact)
|
||||
Printf("\n")
|
||||
}
|
||||
|
||||
if len(remove) != 0 && !gopts.Quiet {
|
||||
Printf("remove %d snapshots:\n", len(remove))
|
||||
PrintSnapshots(globalOptions.stdout, remove, opts.Compact)
|
||||
Printf("\n")
|
||||
}
|
||||
|
||||
removeSnapshots += len(remove)
|
||||
|
||||
if !opts.DryRun {
|
||||
for _, sn := range remove {
|
||||
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
||||
err = repo.Backend().Remove(context.TODO(), h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if removeSnapshots > 0 && opts.Prune {
|
||||
Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
|
||||
if !opts.DryRun {
|
||||
return pruneRepository(gopts, repo)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
94
cmd/restic/cmd_generate.go
Normal file
94
cmd/restic/cmd_generate.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
var cmdGenerate = &cobra.Command{
|
||||
Use: "generate [command]",
|
||||
Short: "Generate manual pages and auto-completion files (bash, zsh)",
|
||||
Long: `
|
||||
The "generate" command writes automatically generated files like the man pages
|
||||
and the auto-completion files for bash and zsh).
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: runGenerate,
|
||||
}
|
||||
|
||||
type generateOptions struct {
|
||||
ManDir string
|
||||
BashCompletionFile string
|
||||
ZSHCompletionFile string
|
||||
}
|
||||
|
||||
var genOpts generateOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdGenerate)
|
||||
fs := cmdGenerate.Flags()
|
||||
fs.StringVar(&genOpts.ManDir, "man", "", "write man pages to `directory`")
|
||||
fs.StringVar(&genOpts.BashCompletionFile, "bash-completion", "", "write bash completion `file`")
|
||||
fs.StringVar(&genOpts.ZSHCompletionFile, "zsh-completion", "", "write zsh completion `file`")
|
||||
}
|
||||
|
||||
func writeManpages(dir string) error {
|
||||
// use a fixed date for the man pages so that generating them is deterministic
|
||||
date, err := time.Parse("Jan 2006", "Jan 2017")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header := &doc.GenManHeader{
|
||||
Title: "restic backup",
|
||||
Section: "1",
|
||||
Source: "generated by `restic generate`",
|
||||
Date: &date,
|
||||
}
|
||||
|
||||
Verbosef("writing man pages to directory %v\n", dir)
|
||||
return doc.GenManTree(cmdRoot, header, dir)
|
||||
}
|
||||
|
||||
func writeBashCompletion(file string) error {
|
||||
Verbosef("writing bash completion file to %v\n", file)
|
||||
return cmdRoot.GenBashCompletionFile(file)
|
||||
}
|
||||
|
||||
func writeZSHCompletion(file string) error {
|
||||
Verbosef("writing zsh completion file to %v\n", file)
|
||||
return cmdRoot.GenZshCompletionFile(file)
|
||||
}
|
||||
|
||||
func runGenerate(cmd *cobra.Command, args []string) error {
|
||||
if genOpts.ManDir != "" {
|
||||
err := writeManpages(genOpts.ManDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if genOpts.BashCompletionFile != "" {
|
||||
err := writeBashCompletion(genOpts.BashCompletionFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if genOpts.ZSHCompletionFile != "" {
|
||||
err := writeZSHCompletion(genOpts.ZSHCompletionFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var empty generateOptions
|
||||
if genOpts == empty {
|
||||
return errors.Fatal("nothing to do, please specify at least one output file/dir")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,18 +2,20 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"restic/errors"
|
||||
"restic/repository"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdInit = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "initialize a new repository",
|
||||
Short: "Initialize a new repository",
|
||||
Long: `
|
||||
The "init" command initializes a new repository.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInit(globalOptions, args)
|
||||
},
|
||||
@@ -33,13 +35,11 @@ func runInit(gopts GlobalOptions, args []string) error {
|
||||
return errors.Fatalf("create backend at %s failed: %v\n", gopts.Repo, err)
|
||||
}
|
||||
|
||||
if gopts.password == "" {
|
||||
gopts.password, err = ReadPasswordTwice(gopts,
|
||||
"enter password for new backend: ",
|
||||
"enter password again: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gopts.password, err = ReadPasswordTwice(gopts,
|
||||
"enter password for new backend: ",
|
||||
"enter password again: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := repository.New(be)
|
||||
@@ -3,19 +3,21 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"restic"
|
||||
"restic/errors"
|
||||
"restic/repository"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdKey = &cobra.Command{
|
||||
Use: "key [list|add|rm|passwd] [ID]",
|
||||
Short: "manage keys (passwords)",
|
||||
Use: "key [list|add|remove|passwd] [ID]",
|
||||
Short: "Manage keys (passwords)",
|
||||
Long: `
|
||||
The "key" command manages keys (passwords) for accessing the repository.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runKey(globalOptions, args)
|
||||
},
|
||||
@@ -58,7 +60,12 @@ func getNewPassword(gopts GlobalOptions) (string, error) {
|
||||
return testKeyNewPassword, nil
|
||||
}
|
||||
|
||||
return ReadPasswordTwice(gopts,
|
||||
// Since we already have an open repository, temporary remove the password
|
||||
// to prompt the user for the passwd.
|
||||
newopts := gopts
|
||||
newopts.password = ""
|
||||
|
||||
return ReadPasswordTwice(newopts,
|
||||
"enter password for new key: ",
|
||||
"enter password again: ")
|
||||
}
|
||||
@@ -117,7 +124,7 @@ 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) || (args[0] != "rm" && len(args) != 1) {
|
||||
if len(args) < 1 || (args[0] == "remove" && len(args) != 2) || (args[0] != "remove" && len(args) != 1) {
|
||||
return errors.Fatal("wrong number of arguments")
|
||||
}
|
||||
|
||||
@@ -146,7 +153,7 @@ func runKey(gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
return addKey(gopts, repo)
|
||||
case "rm":
|
||||
case "remove":
|
||||
lock, err := lockRepoExclusive(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
@@ -3,19 +3,21 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"restic"
|
||||
"restic/errors"
|
||||
"restic/index"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/index"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdList = &cobra.Command{
|
||||
Use: "list [blobs|packs|index|snapshots|keys|locks]",
|
||||
Short: "list objects in the repository",
|
||||
Short: "List objects in the repository",
|
||||
Long: `
|
||||
The "list" command allows listing objects in the repository based on type.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(globalOptions, args)
|
||||
},
|
||||
@@ -6,19 +6,20 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"restic"
|
||||
"restic/errors"
|
||||
"restic/repository"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
var cmdLs = &cobra.Command{
|
||||
Use: "ls [flags] [snapshot-ID ...]",
|
||||
Short: "list files in a snapshot",
|
||||
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.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runLs(lsOptions, globalOptions, args)
|
||||
},
|
||||
@@ -28,7 +29,7 @@ The special snapshot-ID "latest" can be used to list files and directories of th
|
||||
type LsOptions struct {
|
||||
ListLong bool
|
||||
Host string
|
||||
Tags []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
}
|
||||
|
||||
@@ -41,8 +42,8 @@ func init() {
|
||||
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")
|
||||
flags.Var(&lsOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given")
|
||||
flags.StringArrayVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
|
||||
}
|
||||
|
||||
func printTree(repo *repository.Repository, id *restic.ID, prefix string) error {
|
||||
108
cmd/restic/cmd_migrate.go
Normal file
108
cmd/restic/cmd_migrate.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/restic/restic/internal/migrations"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdMigrate = &cobra.Command{
|
||||
Use: "migrate [name]",
|
||||
Short: "Apply migrations",
|
||||
Long: `
|
||||
The "migrate" command applies migrations to a repository. When no migration
|
||||
name is explicitly given, a list of migrations that can be applied is printed.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runMigrate(migrateOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// MigrateOptions bundles all options for the 'check' command.
|
||||
type MigrateOptions struct {
|
||||
Force bool
|
||||
}
|
||||
|
||||
var migrateOptions MigrateOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdMigrate)
|
||||
f := cmdMigrate.Flags()
|
||||
f.BoolVarP(&migrateOptions.Force, "force", "f", false, `apply a migration a second time`)
|
||||
}
|
||||
|
||||
func checkMigrations(opts MigrateOptions, gopts GlobalOptions, repo restic.Repository) error {
|
||||
ctx := gopts.ctx
|
||||
Printf("available migrations:\n")
|
||||
for _, m := range migrations.All {
|
||||
ok, err := m.Check(ctx, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ok {
|
||||
Printf(" %v: %v\n", m.Name(), m.Desc())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyMigrations(opts MigrateOptions, gopts GlobalOptions, repo restic.Repository, args []string) error {
|
||||
ctx := gopts.ctx
|
||||
|
||||
var firsterr error
|
||||
for _, name := range args {
|
||||
for _, m := range migrations.All {
|
||||
if m.Name() == name {
|
||||
ok, err := m.Check(ctx, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
if !opts.Force {
|
||||
Warnf("migration %v cannot be applied: check failed\nIf you want to apply this migration anyway, re-run with option --force\n", m.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
Warnf("check for migration %v failed, continuing anyway\n", m.Name())
|
||||
}
|
||||
|
||||
Printf("applying migration %v...\n", m.Name())
|
||||
if err = m.Apply(ctx, repo); err != nil {
|
||||
Warnf("migration %v failed: %v\n", m.Name(), err)
|
||||
if firsterr == nil {
|
||||
firsterr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
Printf("migration %v: success\n", m.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return firsterr
|
||||
}
|
||||
|
||||
func runMigrate(opts MigrateOptions, gopts GlobalOptions, args []string) error {
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, err := lockRepoExclusive(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return checkMigrations(opts, gopts, repo)
|
||||
}
|
||||
|
||||
return applyMigrations(opts, gopts, repo, args)
|
||||
}
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"restic/debug"
|
||||
"restic/errors"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
resticfs "restic/fs"
|
||||
"restic/fuse"
|
||||
resticfs "github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/fuse"
|
||||
|
||||
systemFuse "bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
@@ -21,11 +22,12 @@ import (
|
||||
|
||||
var cmdMount = &cobra.Command{
|
||||
Use: "mount [flags] mountpoint",
|
||||
Short: "mount the repository",
|
||||
Short: "Mount the repository",
|
||||
Long: `
|
||||
The "mount" command mounts the repository via fuse to a directory. This is a
|
||||
read-only mount.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runMount(mountOptions, globalOptions, args)
|
||||
},
|
||||
@@ -37,7 +39,7 @@ type MountOptions struct {
|
||||
AllowRoot bool
|
||||
AllowOther bool
|
||||
Host string
|
||||
Tags []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
}
|
||||
|
||||
@@ -52,8 +54,8 @@ func init() {
|
||||
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`")
|
||||
mountFlags.Var(&mountOptions.Tags, "tag", "only consider snapshots which include this `taglist`")
|
||||
mountFlags.StringArrayVar(&mountOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`")
|
||||
}
|
||||
|
||||
func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
@@ -65,6 +67,12 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -2,18 +2,20 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"restic/options"
|
||||
|
||||
"github.com/restic/restic/internal/options"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var optionsCmd = &cobra.Command{
|
||||
Use: "options",
|
||||
Short: "print list of extended options",
|
||||
Short: "Print list of extended options",
|
||||
Long: `
|
||||
The "options" command prints a list of extended options.
|
||||
`,
|
||||
Hidden: true,
|
||||
Hidden: true,
|
||||
DisableAutoGenTag: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("All Extended Options:\n")
|
||||
for _, opt := range options.List() {
|
||||
@@ -2,23 +2,25 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"restic"
|
||||
"restic/debug"
|
||||
"restic/errors"
|
||||
"restic/index"
|
||||
"restic/repository"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/index"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdPrune = &cobra.Command{
|
||||
Use: "prune [flags]",
|
||||
Short: "remove unneeded data from the repository",
|
||||
Short: "Remove unneeded data from the repository",
|
||||
Long: `
|
||||
The "prune" command checks the repository and removes data that is not
|
||||
referenced and therefore not needed any more.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runPrune(globalOptions)
|
||||
},
|
||||
@@ -83,6 +85,25 @@ func runPrune(gopts GlobalOptions) error {
|
||||
return pruneRepository(gopts, repo)
|
||||
}
|
||||
|
||||
func mixedBlobs(list []restic.Blob) bool {
|
||||
var tree, data bool
|
||||
|
||||
for _, pb := range list {
|
||||
switch pb.Type {
|
||||
case restic.TreeBlob:
|
||||
tree = true
|
||||
case restic.DataBlob:
|
||||
data = true
|
||||
}
|
||||
|
||||
if tree && data {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
ctx := gopts.ctx
|
||||
|
||||
@@ -120,7 +141,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
stats.bytes += pack.Size
|
||||
blobs += len(pack.Entries)
|
||||
}
|
||||
Verbosef("repository contains %v packs (%v blobs) with %v bytes\n",
|
||||
Verbosef("repository contains %v packs (%v blobs) with %v\n",
|
||||
len(idx.Packs), blobs, formatBytes(uint64(stats.bytes)))
|
||||
|
||||
blobCount := make(map[restic.BlobHandle]int)
|
||||
@@ -177,12 +198,23 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
}
|
||||
bar.Done()
|
||||
|
||||
if len(usedBlobs) > stats.blobs {
|
||||
return errors.Fatalf("number of used blobs is larger than number of available blobs!\n" +
|
||||
"Please report this error (along with the output of the 'prune' run) at\n" +
|
||||
"https://github.com/restic/restic/issues/new")
|
||||
}
|
||||
|
||||
Verbosef("found %d of %d data blobs still in use, removing %d blobs\n",
|
||||
len(usedBlobs), stats.blobs, stats.blobs-len(usedBlobs))
|
||||
|
||||
// find packs that need a rewrite
|
||||
rewritePacks := restic.NewIDSet()
|
||||
for _, pack := range idx.Packs {
|
||||
if mixedBlobs(pack.Entries) {
|
||||
rewritePacks.Insert(pack.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, blob := range pack.Entries {
|
||||
h := restic.BlobHandle{ID: blob.ID, Type: blob.Type}
|
||||
if !usedBlobs.Has(h) {
|
||||
@@ -235,22 +267,23 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
Verbosef("will delete %d packs and rewrite %d packs, this frees %s\n",
|
||||
len(removePacks), len(rewritePacks), formatBytes(uint64(removeBytes)))
|
||||
|
||||
var repackedBlobs restic.IDSet
|
||||
var obsoletePacks restic.IDSet
|
||||
if len(rewritePacks) != 0 {
|
||||
bar = newProgressMax(!gopts.Quiet, uint64(len(rewritePacks)), "packs rewritten")
|
||||
bar.Start()
|
||||
repackedBlobs, err = repository.Repack(ctx, repo, rewritePacks, usedBlobs, bar)
|
||||
obsoletePacks, err = repository.Repack(ctx, repo, rewritePacks, usedBlobs, bar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bar.Done()
|
||||
}
|
||||
|
||||
removePacks.Merge(obsoletePacks)
|
||||
|
||||
if err = rebuildIndex(ctx, repo, removePacks); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
removePacks.Merge(repackedBlobs)
|
||||
if len(removePacks) != 0 {
|
||||
bar = newProgressMax(!gopts.Quiet, uint64(len(removePacks)), "packs deleted")
|
||||
bar.Start()
|
||||
@@ -2,19 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"restic"
|
||||
"restic/index"
|
||||
|
||||
"github.com/restic/restic/internal/index"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdRebuildIndex = &cobra.Command{
|
||||
Use: "rebuild-index [flags]",
|
||||
Short: "build a new index file",
|
||||
Short: "Build a new index file",
|
||||
Long: `
|
||||
The "rebuild-index" command creates a new index based on the pack files in the
|
||||
repository.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRebuildIndex(globalOptions)
|
||||
},
|
||||
157
cmd/restic/cmd_restore.go
Normal file
157
cmd/restic/cmd_restore.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/filter"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdRestore = &cobra.Command{
|
||||
Use: "restore [flags] snapshotID",
|
||||
Short: "Extract the data from a snapshot",
|
||||
Long: `
|
||||
The "restore" command extracts the data from a snapshot from the repository to
|
||||
a directory.
|
||||
|
||||
The special snapshot "latest" can be used to restore the latest snapshot in the
|
||||
repository.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRestore(restoreOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// RestoreOptions collects all options for the restore command.
|
||||
type RestoreOptions struct {
|
||||
Exclude []string
|
||||
Include []string
|
||||
Target string
|
||||
Host string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
}
|
||||
|
||||
var restoreOptions RestoreOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdRestore)
|
||||
|
||||
flags := cmdRestore.Flags()
|
||||
flags.StringArrayVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
|
||||
flags.StringArrayVarP(&restoreOptions.Include, "include", "i", nil, "include a `pattern`, exclude everything else (can be specified multiple times)")
|
||||
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.Var(&restoreOptions.Tags, "tag", "only consider snapshots which include this `taglist` for snapshot ID \"latest\"")
|
||||
flags.StringArrayVar(&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 {
|
||||
ctx := gopts.ctx
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.Fatal("no snapshot ID specified")
|
||||
}
|
||||
|
||||
if opts.Target == "" {
|
||||
return errors.Fatal("please specify a directory to restore to (--target)")
|
||||
}
|
||||
|
||||
if len(opts.Exclude) > 0 && len(opts.Include) > 0 {
|
||||
return errors.Fatal("exclude and include patterns are mutually exclusive")
|
||||
}
|
||||
|
||||
snapshotIDString := args[0]
|
||||
|
||||
debug.Log("restore %v to %v", snapshotIDString, opts.Target)
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var id restic.ID
|
||||
|
||||
if snapshotIDString == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host)
|
||||
if err != nil {
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
|
||||
}
|
||||
} else {
|
||||
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||
if err != nil {
|
||||
Exitf(1, "invalid id %q: %v", snapshotIDString, err)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := restic.NewRestorer(repo, id)
|
||||
if err != nil {
|
||||
Exitf(2, "creating restorer failed: %v\n", err)
|
||||
}
|
||||
|
||||
totalErrors := 0
|
||||
res.Error = func(dir string, node *restic.Node, err error) error {
|
||||
Warnf("ignoring error for %s: %s\n", dir, err)
|
||||
totalErrors++
|
||||
return nil
|
||||
}
|
||||
|
||||
selectExcludeFilter := func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) {
|
||||
matched, _, err := filter.List(opts.Exclude, item)
|
||||
if err != nil {
|
||||
Warnf("error for exclude pattern: %v", err)
|
||||
}
|
||||
|
||||
// An exclude filter is basically a 'wildcard but foo',
|
||||
// so even if a childMayMatch, other children of a dir may not,
|
||||
// therefore childMayMatch does not matter, but we should not go down
|
||||
// unless the dir is selected for restore
|
||||
selectedForRestore = !matched
|
||||
childMayBeSelected = selectedForRestore && node.Type == "dir"
|
||||
|
||||
return selectedForRestore, childMayBeSelected
|
||||
}
|
||||
|
||||
selectIncludeFilter := func(item string, dstpath string, node *restic.Node) (selectedForRestore bool, childMayBeSelected bool) {
|
||||
matched, childMayMatch, err := filter.List(opts.Include, item)
|
||||
if err != nil {
|
||||
Warnf("error for include pattern: %v", err)
|
||||
}
|
||||
|
||||
selectedForRestore = matched
|
||||
childMayBeSelected = childMayMatch && node.Type == "dir"
|
||||
|
||||
return selectedForRestore, childMayBeSelected
|
||||
}
|
||||
|
||||
if len(opts.Exclude) > 0 {
|
||||
res.SelectFilter = selectExcludeFilter
|
||||
} else if len(opts.Include) > 0 {
|
||||
res.SelectFilter = selectIncludeFilter
|
||||
}
|
||||
|
||||
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
||||
|
||||
err = res.RestoreTo(ctx, opts.Target)
|
||||
if totalErrors > 0 {
|
||||
Printf("There were %d errors\n", totalErrors)
|
||||
}
|
||||
return err
|
||||
}
|
||||
239
cmd/restic/cmd_snapshots.go
Normal file
239
cmd/restic/cmd_snapshots.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdSnapshots = &cobra.Command{
|
||||
Use: "snapshots [snapshotID ...]",
|
||||
Short: "List all snapshots",
|
||||
Long: `
|
||||
The "snapshots" command lists all snapshots stored in the repository.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runSnapshots(snapshotOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// SnapshotOptions bundles all options for the snapshots command.
|
||||
type SnapshotOptions struct {
|
||||
Host string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Compact bool
|
||||
Last bool
|
||||
}
|
||||
|
||||
var snapshotOptions SnapshotOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdSnapshots)
|
||||
|
||||
f := cmdSnapshots.Flags()
|
||||
f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`")
|
||||
f.Var(&snapshotOptions.Tags, "tag", "only consider snapshots which include this `taglist` (can be specified multiple times)")
|
||||
f.StringArrayVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)")
|
||||
f.BoolVarP(&snapshotOptions.Compact, "compact", "c", false, "use compact format")
|
||||
f.BoolVar(&snapshotOptions.Last, "last", false, "only show the last snapshot for each host and path")
|
||||
}
|
||||
|
||||
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
|
||||
var list restic.Snapshots
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
list = append(list, sn)
|
||||
}
|
||||
|
||||
if opts.Last {
|
||||
list = FilterLastSnapshots(list)
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(list))
|
||||
|
||||
if gopts.JSON {
|
||||
err := printSnapshotsJSON(gopts.stdout, list)
|
||||
if err != nil {
|
||||
Warnf("error printing snapshot: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
PrintSnapshots(gopts.stdout, list, opts.Compact)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterLastSnapshotsKey is used by FilterLastSnapshots.
|
||||
type filterLastSnapshotsKey struct {
|
||||
Hostname string
|
||||
JoinedPaths string
|
||||
}
|
||||
|
||||
// newFilterLastSnapshotsKey initializes a filterLastSnapshotsKey from a Snapshot
|
||||
func newFilterLastSnapshotsKey(sn *restic.Snapshot) filterLastSnapshotsKey {
|
||||
// Shallow slice copy
|
||||
var paths = make([]string, len(sn.Paths))
|
||||
copy(paths, sn.Paths)
|
||||
sort.Strings(paths)
|
||||
return filterLastSnapshotsKey{sn.Hostname, strings.Join(paths, "|")}
|
||||
}
|
||||
|
||||
// FilterLastSnapshots filters a list of snapshots to only return the last
|
||||
// entry for each hostname and path. If the snapshot contains multiple paths,
|
||||
// they will be joined and treated as one item.
|
||||
func FilterLastSnapshots(list restic.Snapshots) restic.Snapshots {
|
||||
// Sort the snapshots so that the newer ones are listed first
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return list[i].Time.After(list[j].Time)
|
||||
})
|
||||
|
||||
var results restic.Snapshots
|
||||
seen := make(map[filterLastSnapshotsKey]bool)
|
||||
for _, sn := range list {
|
||||
key := newFilterLastSnapshotsKey(sn)
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
results = append(results, sn)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// PrintSnapshots prints a text table of the snapshots in list to stdout.
|
||||
func PrintSnapshots(stdout io.Writer, list restic.Snapshots, compact bool) {
|
||||
|
||||
// always sort the snapshots so that the newer ones are listed last
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
return list[i].Time.Before(list[j].Time)
|
||||
})
|
||||
|
||||
// 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()
|
||||
if !compact {
|
||||
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)
|
||||
} else {
|
||||
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags")
|
||||
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%s", -maxHost)
|
||||
}
|
||||
|
||||
for _, sn := range list {
|
||||
if len(sn.Paths) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
firstTag := ""
|
||||
if len(sn.Tags) > 0 {
|
||||
firstTag = sn.Tags[0]
|
||||
}
|
||||
|
||||
rows := len(sn.Paths)
|
||||
if rows < len(sn.Tags) {
|
||||
rows = len(sn.Tags)
|
||||
}
|
||||
|
||||
treeElement := " "
|
||||
if rows != 1 {
|
||||
treeElement = "┌──"
|
||||
}
|
||||
|
||||
if !compact {
|
||||
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, treeElement, sn.Paths[0]})
|
||||
} else {
|
||||
allTags := ""
|
||||
for _, tag := range sn.Tags {
|
||||
allTags += tag + " "
|
||||
}
|
||||
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, allTags})
|
||||
continue
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
treeElement := "│"
|
||||
if i == (rows - 1) {
|
||||
treeElement = "└──"
|
||||
}
|
||||
|
||||
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, treeElement, path})
|
||||
}
|
||||
}
|
||||
|
||||
tab.Footer = fmt.Sprintf("%d snapshots", len(list))
|
||||
|
||||
tab.Write(stdout)
|
||||
}
|
||||
|
||||
// Snapshot helps to print Snaphots as JSON with their ID included.
|
||||
type Snapshot struct {
|
||||
*restic.Snapshot
|
||||
|
||||
ID *restic.ID `json:"id"`
|
||||
ShortID string `json:"short_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(),
|
||||
ShortID: sn.ID().Str(),
|
||||
}
|
||||
snapshots = append(snapshots, k)
|
||||
}
|
||||
|
||||
return json.NewEncoder(stdout).Encode(snapshots)
|
||||
}
|
||||
@@ -5,15 +5,15 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"restic"
|
||||
"restic/debug"
|
||||
"restic/errors"
|
||||
"restic/repository"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
var cmdTag = &cobra.Command{
|
||||
Use: "tag [flags] [snapshot-ID ...]",
|
||||
Short: "modifies tags on snapshots",
|
||||
Short: "Modify tags on snapshots",
|
||||
Long: `
|
||||
The "tag" command allows you to modify tags on exiting snapshots.
|
||||
|
||||
@@ -22,6 +22,7 @@ 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.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runTag(tagOptions, globalOptions, args)
|
||||
},
|
||||
@@ -31,7 +32,7 @@ When no snapshot-ID is given, all snapshots matching the host, tag and path filt
|
||||
type TagOptions struct {
|
||||
Host string
|
||||
Paths []string
|
||||
Tags []string
|
||||
Tags restic.TagLists
|
||||
SetTags []string
|
||||
AddTags []string
|
||||
RemoveTags []string
|
||||
@@ -48,8 +49,8 @@ func init() {
|
||||
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")
|
||||
tagFlags.Var(&tagOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot-ID is given")
|
||||
tagFlags.StringArrayVar(&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) {
|
||||
@@ -112,7 +113,7 @@ func runTag(opts TagOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
Verbosef("Create exclusive lock for repository\n")
|
||||
Verbosef("create exclusive lock for repository\n")
|
||||
lock, err := lockRepoExclusive(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
@@ -134,9 +135,9 @@ func runTag(opts TagOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
}
|
||||
if changeCnt == 0 {
|
||||
Verbosef("No snapshots were modified\n")
|
||||
Verbosef("no snapshots were modified\n")
|
||||
} else {
|
||||
Verbosef("Modified tags on %v snapshots\n", changeCnt)
|
||||
Verbosef("modified tags on %v snapshots\n", changeCnt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,17 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"restic"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var unlockCmd = &cobra.Command{
|
||||
Use: "unlock",
|
||||
Short: "remove locks other processes created",
|
||||
Short: "Remove locks other processes created",
|
||||
Long: `
|
||||
The "unlock" command removes stale locks that have been created by other restic processes.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runUnlock(unlockOptions, globalOptions)
|
||||
},
|
||||
@@ -9,11 +9,12 @@ 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.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("restic %s\ncompiled with %v on %v/%v\n",
|
||||
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
266
cmd/restic/exclude.go
Normal file
266
cmd/restic/exclude.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/filter"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
)
|
||||
|
||||
type rejectionCache struct {
|
||||
m map[string]bool
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
// Lock locks the mutex in rc.
|
||||
func (rc *rejectionCache) Lock() {
|
||||
if rc != nil {
|
||||
rc.mtx.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock unlocks the mutex in rc.
|
||||
func (rc *rejectionCache) Unlock() {
|
||||
if rc != nil {
|
||||
rc.mtx.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the last stored value for dir and a second boolean that
|
||||
// indicates whether that value was actually written to the cache. It is the
|
||||
// callers responsibility to call rc.Lock and rc.Unlock before using this
|
||||
// method, otherwise data races may occur.
|
||||
func (rc *rejectionCache) Get(dir string) (bool, bool) {
|
||||
if rc == nil || rc.m == nil {
|
||||
return false, false
|
||||
}
|
||||
v, ok := rc.m[dir]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Store stores a new value for dir. It is the callers responsibility to call
|
||||
// rc.Lock and rc.Unlock before using this method, otherwise data races may
|
||||
// occur.
|
||||
func (rc *rejectionCache) Store(dir string, rejected bool) {
|
||||
if rc == nil {
|
||||
return
|
||||
}
|
||||
if rc.m == nil {
|
||||
rc.m = make(map[string]bool)
|
||||
}
|
||||
rc.m[dir] = rejected
|
||||
}
|
||||
|
||||
// RejectFunc is a function that takes a filename and os.FileInfo of a
|
||||
// file that would be included in the backup. The function returns true if it
|
||||
// should be excluded (rejected) from the backup.
|
||||
type RejectFunc func(path string, fi os.FileInfo) bool
|
||||
|
||||
// rejectByPattern returns a RejectFunc which rejects files that match
|
||||
// one of the patterns.
|
||||
func rejectByPattern(patterns []string) RejectFunc {
|
||||
return func(item string, fi os.FileInfo) bool {
|
||||
matched, _, err := filter.List(patterns, item)
|
||||
if err != nil {
|
||||
Warnf("error for exclude pattern: %v", err)
|
||||
}
|
||||
|
||||
if matched {
|
||||
debug.Log("path %q excluded by an exclude pattern", item)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// rejectIfPresent returns a RejectFunc which itself returns whether a path
|
||||
// should be excluded. The RejectFunc considers a file to be excluded when
|
||||
// it resides in a directory with an exclusion file, that is specified by
|
||||
// excludeFileSpec in the form "filename[:content]". The returned error is
|
||||
// non-nil if the filename component of excludeFileSpec is empty. If rc is
|
||||
// non-nil, it is going to be used in the RejectFunc to expedite the evaluation
|
||||
// of a directory based on previous visits.
|
||||
func rejectIfPresent(excludeFileSpec string, rc *rejectionCache) (RejectFunc, error) {
|
||||
if excludeFileSpec == "" {
|
||||
return nil, errors.New("name for exclusion tagfile is empty")
|
||||
}
|
||||
colon := strings.Index(excludeFileSpec, ":")
|
||||
if colon == 0 {
|
||||
return nil, fmt.Errorf("no name for exclusion tagfile provided")
|
||||
}
|
||||
tf, tc := "", ""
|
||||
if colon > 0 {
|
||||
tf = excludeFileSpec[:colon]
|
||||
tc = excludeFileSpec[colon+1:]
|
||||
} else {
|
||||
tf = excludeFileSpec
|
||||
}
|
||||
debug.Log("using %q as exclusion tagfile", tf)
|
||||
fn := func(filename string, _ os.FileInfo) bool {
|
||||
return isExcludedByFile(filename, tf, tc, rc)
|
||||
}
|
||||
return fn, nil
|
||||
}
|
||||
|
||||
// isExcludedByFile interprets filename as a path and returns true if that file
|
||||
// is in a excluded directory. A directory is identified as excluded if it contains a
|
||||
// tagfile which bears the name specified in tagFilename and starts with
|
||||
// header. If rc is non-nil, it is used to expedite the evaluation of a
|
||||
// directory based on previous visits.
|
||||
func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache) bool {
|
||||
if tagFilename == "" {
|
||||
return false
|
||||
}
|
||||
dir, base := filepath.Split(filename)
|
||||
if base == tagFilename {
|
||||
return false // do not exclude the tagfile itself
|
||||
}
|
||||
rc.Lock()
|
||||
defer rc.Unlock()
|
||||
|
||||
rejected, visited := rc.Get(dir)
|
||||
if visited {
|
||||
return rejected
|
||||
}
|
||||
rejected = isDirExcludedByFile(dir, tagFilename, header)
|
||||
rc.Store(dir, rejected)
|
||||
return rejected
|
||||
}
|
||||
|
||||
func isDirExcludedByFile(dir, tagFilename, header string) bool {
|
||||
tf := filepath.Join(dir, tagFilename)
|
||||
_, err := fs.Lstat(tf)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
Warnf("could not access exclusion tagfile: %v", err)
|
||||
return false
|
||||
}
|
||||
// when no signature is given, the mere presence of tf is enough reason
|
||||
// to exclude filename
|
||||
if len(header) == 0 {
|
||||
return true
|
||||
}
|
||||
// From this stage, errors mean tagFilename exists but it is malformed.
|
||||
// Warnings will be generated so that the user is informed that the
|
||||
// indented ignore-action is not performed.
|
||||
f, err := os.Open(tf)
|
||||
if err != nil {
|
||||
Warnf("could not open exclusion tagfile: %v", err)
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
buf := make([]byte, len(header))
|
||||
_, err = io.ReadFull(f, buf)
|
||||
// EOF is handled with a dedicated message, otherwise the warning were too cryptic
|
||||
if err == io.EOF {
|
||||
Warnf("invalid (too short) signature in exclusion tagfile %q\n", tf)
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
Warnf("could not read signature from exclusion tagfile %q: %v\n", tf, err)
|
||||
return false
|
||||
}
|
||||
if bytes.Compare(buf, []byte(header)) != 0 {
|
||||
Warnf("invalid signature in exclusion tagfile %q\n", tf)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// gatherDevices returns the set of unique device ids of the files and/or
|
||||
// directory paths listed in "items".
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
id, err := fs.DeviceID(fi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deviceMap[item] = id
|
||||
}
|
||||
if len(deviceMap) == 0 {
|
||||
return nil, errors.New("zero allowed devices")
|
||||
}
|
||||
return deviceMap, nil
|
||||
}
|
||||
|
||||
// rejectByDevice returns a RejectFunc that rejects files which are on a
|
||||
// different file systems than the files/dirs in samples.
|
||||
func rejectByDevice(samples []string) (RejectFunc, error) {
|
||||
allowed, err := gatherDevices(samples)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
debug.Log("allowed devices: %v\n", allowed)
|
||||
|
||||
return func(item string, fi os.FileInfo) bool {
|
||||
if fi == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
id, err := fs.DeviceID(fi)
|
||||
if err != nil {
|
||||
// This should never happen because gatherDevices() would have
|
||||
// errored out earlier. If it still does that's a reason to panic.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for dir := item; dir != ""; dir = filepath.Dir(dir) {
|
||||
debug.Log("item %v, test dir %v", item, dir)
|
||||
|
||||
allowedID, ok := allowed[dir]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if allowedID != id {
|
||||
debug.Log("path %q on disallowed device %d", item, id)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("item %v, device id %v not found, allowedDevs: %v", item, id, allowed))
|
||||
}, nil
|
||||
}
|
||||
|
||||
// rejectResticCache returns a RejectFunc that rejects the restic cache
|
||||
// directory (if set).
|
||||
func rejectResticCache(repo *repository.Repository) (RejectFunc, error) {
|
||||
if repo.Cache == nil {
|
||||
return func(string, os.FileInfo) bool {
|
||||
return false
|
||||
}, nil
|
||||
}
|
||||
cacheBase := repo.Cache.BaseDir()
|
||||
|
||||
if cacheBase == "" {
|
||||
return nil, errors.New("cacheBase is empty string")
|
||||
}
|
||||
|
||||
return func(item string, _ os.FileInfo) bool {
|
||||
if fs.HasPathPrefix(cacheBase, item) {
|
||||
debug.Log("rejecting restic cache directory %v", item)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}, nil
|
||||
}
|
||||
84
cmd/restic/exclude_test.go
Normal file
84
cmd/restic/exclude_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func TestRejectByPattern(t *testing.T) {
|
||||
var tests = []struct {
|
||||
filename string
|
||||
reject bool
|
||||
}{
|
||||
{filename: "/home/user/foo.go", reject: true},
|
||||
{filename: "/home/user/foo.c", reject: false},
|
||||
{filename: "/home/user/foobar", reject: false},
|
||||
{filename: "/home/user/foobar/x", reject: true},
|
||||
{filename: "/home/user/README", reject: false},
|
||||
{filename: "/home/user/README.md", reject: true},
|
||||
}
|
||||
|
||||
patterns := []string{"*.go", "README.md", "/home/user/foobar/*"}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
reject := rejectByPattern(patterns)
|
||||
res := reject(tc.filename, nil)
|
||||
if res != tc.reject {
|
||||
t.Fatalf("wrong result for filename %v: want %v, got %v",
|
||||
tc.filename, tc.reject, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsExcludedByFile(t *testing.T) {
|
||||
const (
|
||||
tagFilename = "CACHEDIR.TAG"
|
||||
header = "Signature: 8a477f597d28d172789f06886806bc55"
|
||||
)
|
||||
tests := []struct {
|
||||
name string
|
||||
tagFile string
|
||||
content string
|
||||
want bool
|
||||
}{
|
||||
{"NoTagfile", "", "", false},
|
||||
{"EmptyTagfile", tagFilename, "", true},
|
||||
{"UnnamedTagFile", "", header, false},
|
||||
{"WrongTagFile", "notatagfile", header, false},
|
||||
{"IncorrectSig", tagFilename, header[1:], false},
|
||||
{"ValidSig", tagFilename, header, true},
|
||||
{"ValidPlusStuff", tagFilename, header + "foo", true},
|
||||
{"ValidPlusNewlineAndStuff", tagFilename, header + "\nbar", true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tempDir, cleanup := test.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
foo := filepath.Join(tempDir, "foo")
|
||||
err := ioutil.WriteFile(foo, []byte("foo"), 0666)
|
||||
if err != nil {
|
||||
t.Fatalf("could not write file: %v", err)
|
||||
}
|
||||
if tc.tagFile != "" {
|
||||
tagFile := filepath.Join(tempDir, tc.tagFile)
|
||||
err = ioutil.WriteFile(tagFile, []byte(tc.content), 0666)
|
||||
if err != nil {
|
||||
t.Fatalf("could not write tagfile: %v", err)
|
||||
}
|
||||
}
|
||||
h := header
|
||||
if tc.content == "" {
|
||||
h = ""
|
||||
}
|
||||
if got := isExcludedByFile(foo, tagFilename, h, nil); tc.want != got {
|
||||
t.Fatalf("expected %v, got %v", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
31
cmd/restic/excludes
Normal file
31
cmd/restic/excludes
Normal file
@@ -0,0 +1,31 @@
|
||||
/boot
|
||||
/dev
|
||||
/etc
|
||||
/home
|
||||
/lost+found
|
||||
/mnt
|
||||
/proc
|
||||
/root
|
||||
/run
|
||||
/sys
|
||||
/tmp
|
||||
/usr
|
||||
/var
|
||||
/opt/android-sdk
|
||||
/opt/bullet
|
||||
/opt/dex2jar
|
||||
/opt/jameica
|
||||
/opt/google
|
||||
/opt/JDownloader
|
||||
/opt/JDownloaderScripts
|
||||
/opt/opencascade
|
||||
/opt/vagrant
|
||||
/opt/visual-studio-code
|
||||
/opt/vtk6
|
||||
/bin
|
||||
/fonts*
|
||||
/srv/ftp
|
||||
/srv/http
|
||||
/sbin
|
||||
/lib
|
||||
/lib64
|
||||
@@ -3,12 +3,12 @@ package main
|
||||
import (
|
||||
"context"
|
||||
|
||||
"restic"
|
||||
"restic/repository"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, host string, tags []restic.TagList, paths []string, snapshotIDs []string) <-chan *restic.Snapshot {
|
||||
out := make(chan *restic.Snapshot)
|
||||
go func() {
|
||||
defer close(out)
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"restic"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
func formatBytes(c uint64) string {
|
||||
590
cmd/restic/global.go
Normal file
590
cmd/restic/global.go
Normal file
@@ -0,0 +1,590 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/backend"
|
||||
"github.com/restic/restic/internal/backend/azure"
|
||||
"github.com/restic/restic/internal/backend/b2"
|
||||
"github.com/restic/restic/internal/backend/gs"
|
||||
"github.com/restic/restic/internal/backend/local"
|
||||
"github.com/restic/restic/internal/backend/location"
|
||||
"github.com/restic/restic/internal/backend/rest"
|
||||
"github.com/restic/restic/internal/backend/s3"
|
||||
"github.com/restic/restic/internal/backend/sftp"
|
||||
"github.com/restic/restic/internal/backend/swift"
|
||||
"github.com/restic/restic/internal/cache"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/limiter"
|
||||
"github.com/restic/restic/internal/options"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
var version = "compiled manually"
|
||||
|
||||
// GlobalOptions hold all global options for restic.
|
||||
type GlobalOptions struct {
|
||||
Repo string
|
||||
PasswordFile string
|
||||
Quiet bool
|
||||
NoLock bool
|
||||
JSON bool
|
||||
CacheDir string
|
||||
NoCache bool
|
||||
CACerts []string
|
||||
|
||||
LimitUploadKb int
|
||||
LimitDownloadKb int
|
||||
|
||||
ctx context.Context
|
||||
password string
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
|
||||
Options []string
|
||||
|
||||
extended options.Options
|
||||
}
|
||||
|
||||
var globalOptions = GlobalOptions{
|
||||
stdout: os.Stdout,
|
||||
stderr: os.Stderr,
|
||||
}
|
||||
|
||||
func init() {
|
||||
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", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
||||
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
|
||||
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.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory")
|
||||
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
|
||||
f.StringSliceVar(&globalOptions.CACerts, "cacert", nil, "path to load root certificates from (default: use system certificates)")
|
||||
f.IntVar(&globalOptions.LimitUploadKb, "limit-upload", 0, "limits uploads to a maximum rate in KiB/s. (default: unlimited)")
|
||||
f.IntVar(&globalOptions.LimitDownloadKb, "limit-download", 0, "limits downloads to a maximum rate in KiB/s. (default: unlimited)")
|
||||
f.StringSliceVarP(&globalOptions.Options, "option", "o", []string{}, "set extended option (`key=value`, can be specified multiple times)")
|
||||
|
||||
restoreTerminal()
|
||||
}
|
||||
|
||||
// checkErrno returns nil when err is set to syscall.Errno(0), since this is no
|
||||
// error condition.
|
||||
func checkErrno(err error) error {
|
||||
e, ok := err.(syscall.Errno)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if e == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func stdinIsTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdin.Fd()))
|
||||
}
|
||||
|
||||
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() {
|
||||
if !stdoutIsTerminal() {
|
||||
return
|
||||
}
|
||||
|
||||
fd := int(os.Stdout.Fd())
|
||||
state, err := terminal.GetState(fd)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
AddCleanupHandler(func() error {
|
||||
err := checkErrno(terminal.Restore(fd, state))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to get restore terminal state: %#+v\n", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// ClearLine creates a platform dependent string to clear the current
|
||||
// line, so it can be overwritten. ANSI sequences are not supported on
|
||||
// current windows cmd shell.
|
||||
func ClearLine() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
if w := stdoutTerminalWidth(); w > 0 {
|
||||
return strings.Repeat(" ", w-1) + "\r"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return "\x1b[2K"
|
||||
}
|
||||
|
||||
// Printf writes the message to the configured stdout stream.
|
||||
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)
|
||||
Exit(100)
|
||||
}
|
||||
}
|
||||
|
||||
// Verbosef calls Printf to write the message when the verbose flag is set.
|
||||
func Verbosef(format string, args ...interface{}) {
|
||||
if globalOptions.Quiet {
|
||||
return
|
||||
}
|
||||
|
||||
Printf(format, args...)
|
||||
}
|
||||
|
||||
// PrintProgress wraps fmt.Printf to handle the difference in writing progress
|
||||
// information to terminals and non-terminal stdout
|
||||
func PrintProgress(format string, args ...interface{}) {
|
||||
var (
|
||||
message string
|
||||
carriageControl string
|
||||
)
|
||||
message = fmt.Sprintf(format, args...)
|
||||
|
||||
if !(strings.HasSuffix(message, "\r") || strings.HasSuffix(message, "\n")) {
|
||||
if stdoutIsTerminal() {
|
||||
carriageControl = "\r"
|
||||
} else {
|
||||
carriageControl = "\n"
|
||||
}
|
||||
message = fmt.Sprintf("%s%s", message, carriageControl)
|
||||
}
|
||||
|
||||
if stdoutIsTerminal() {
|
||||
message = fmt.Sprintf("%s%s", ClearLine(), message)
|
||||
}
|
||||
|
||||
fmt.Print(message)
|
||||
}
|
||||
|
||||
// Warnf writes the message to the configured stderr stream.
|
||||
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)
|
||||
Exit(100)
|
||||
}
|
||||
}
|
||||
|
||||
// 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...)
|
||||
Exit(exitcode)
|
||||
}
|
||||
|
||||
// resolvePassword determines the password to be used for opening the repository.
|
||||
func resolvePassword(opts GlobalOptions, env string) (string, error) {
|
||||
if opts.PasswordFile != "" {
|
||||
s, err := ioutil.ReadFile(opts.PasswordFile)
|
||||
if os.IsNotExist(err) {
|
||||
return "", errors.Fatalf("%s does not exist", opts.PasswordFile)
|
||||
}
|
||||
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
||||
}
|
||||
|
||||
if pwd := os.Getenv(env); pwd != "" {
|
||||
return pwd, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// readPassword reads the password from the given reader directly.
|
||||
func readPassword(in io.Reader) (password string, err error) {
|
||||
buf := make([]byte, 1000)
|
||||
n, err := io.ReadFull(in, buf)
|
||||
buf = buf[:n]
|
||||
|
||||
if err != nil && errors.Cause(err) != io.ErrUnexpectedEOF {
|
||||
return "", errors.Wrap(err, "ReadFull")
|
||||
}
|
||||
|
||||
return strings.TrimRight(string(buf), "\r\n"), nil
|
||||
}
|
||||
|
||||
// readPasswordTerminal reads the password from the given reader which must be a
|
||||
// tty. Prompt is printed on the writer out before attempting to read the
|
||||
// password.
|
||||
func readPasswordTerminal(in *os.File, out io.Writer, prompt string) (password string, err error) {
|
||||
fmt.Fprint(out, prompt)
|
||||
buf, err := terminal.ReadPassword(int(in.Fd()))
|
||||
fmt.Fprintln(out)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "ReadPassword")
|
||||
}
|
||||
|
||||
password = string(buf)
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// ReadPassword reads the password from a password file, the environment
|
||||
// variable RESTIC_PASSWORD or prompts the user.
|
||||
func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
|
||||
if opts.password != "" {
|
||||
return opts.password, nil
|
||||
}
|
||||
|
||||
var (
|
||||
password string
|
||||
err error
|
||||
)
|
||||
|
||||
if stdinIsTerminal() {
|
||||
password, err = readPasswordTerminal(os.Stdin, os.Stderr, prompt)
|
||||
} else {
|
||||
password, err = readPassword(os.Stdin)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to read password")
|
||||
}
|
||||
|
||||
if len(password) == 0 {
|
||||
return "", errors.Fatal("an empty password is not a password")
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// ReadPasswordTwice calls ReadPassword two times and returns an error when the
|
||||
// passwords don't match.
|
||||
func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, error) {
|
||||
pw1, err := ReadPassword(gopts, prompt1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pw2, err := ReadPassword(gopts, prompt2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pw1 != pw2 {
|
||||
return "", errors.Fatal("passwords do not match")
|
||||
}
|
||||
|
||||
return pw1, nil
|
||||
}
|
||||
|
||||
const maxKeys = 20
|
||||
|
||||
// OpenRepository reads the password and opens the repository.
|
||||
func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
||||
if opts.Repo == "" {
|
||||
return nil, errors.Fatal("Please specify repository location (-r)")
|
||||
}
|
||||
|
||||
be, err := open(opts.Repo, opts.extended)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.LimitUploadKb > 0 || opts.LimitDownloadKb > 0 {
|
||||
debug.Log("rate limiting backend to %d KiB/s upload and %d KiB/s download", opts.LimitUploadKb, opts.LimitDownloadKb)
|
||||
be = limiter.LimitBackend(be, limiter.NewStaticLimiter(opts.LimitUploadKb, opts.LimitDownloadKb))
|
||||
}
|
||||
|
||||
be = backend.NewRetryBackend(be, 10, func(msg string, err error, d time.Duration) {
|
||||
Warnf("%v returned error, retrying after %v: %v\n", msg, d, err)
|
||||
})
|
||||
|
||||
s := repository.New(be)
|
||||
|
||||
opts.password, err = ReadPassword(opts, "enter password for repository: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.SearchKey(context.TODO(), opts.password, maxKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stdoutIsTerminal() {
|
||||
Verbosef("password is correct\n")
|
||||
}
|
||||
|
||||
if opts.NoCache {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
cache, err := cache.New(s.Config().ID, opts.CacheDir)
|
||||
if err != nil {
|
||||
Warnf("unable to open cache: %v\n", err)
|
||||
} else {
|
||||
s.UseCache(cache)
|
||||
}
|
||||
|
||||
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 "gs":
|
||||
cfg := loc.Config.(gs.Config)
|
||||
if cfg.ProjectID == "" {
|
||||
cfg.ProjectID = os.Getenv("GOOGLE_PROJECT_ID")
|
||||
}
|
||||
|
||||
if cfg.JSONKeyPath == "" {
|
||||
if path := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"); path != "" {
|
||||
// Check read access
|
||||
if _, err := ioutil.ReadFile(path); err != nil {
|
||||
return nil, errors.Fatalf("Failed to read google credential from file %v: %v", path, err)
|
||||
}
|
||||
cfg.JSONKeyPath = path
|
||||
} else {
|
||||
return nil, errors.Fatal("No credential file path is set")
|
||||
}
|
||||
}
|
||||
|
||||
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
debug.Log("opening gs repository at %#v", cfg)
|
||||
return cfg, nil
|
||||
|
||||
case "azure":
|
||||
cfg := loc.Config.(azure.Config)
|
||||
if cfg.AccountName == "" {
|
||||
cfg.AccountName = os.Getenv("AZURE_ACCOUNT_NAME")
|
||||
}
|
||||
|
||||
if cfg.AccountKey == "" {
|
||||
cfg.AccountKey = os.Getenv("AZURE_ACCOUNT_KEY")
|
||||
}
|
||||
|
||||
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
debug.Log("opening gs repository at %#v", cfg)
|
||||
return cfg, nil
|
||||
|
||||
case "swift":
|
||||
cfg := loc.Config.(swift.Config)
|
||||
|
||||
if err := swift.ApplyEnvironment("", &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
debug.Log("opening swift repository at %#v", cfg)
|
||||
return cfg, nil
|
||||
|
||||
case "b2":
|
||||
cfg := loc.Config.(b2.Config)
|
||||
|
||||
if cfg.AccountID == "" {
|
||||
cfg.AccountID = os.Getenv("B2_ACCOUNT_ID")
|
||||
}
|
||||
|
||||
if cfg.Key == "" {
|
||||
cfg.Key = os.Getenv("B2_ACCOUNT_KEY")
|
||||
}
|
||||
|
||||
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
debug.Log("opening b2 repository at %#v", cfg)
|
||||
return cfg, nil
|
||||
case "rest":
|
||||
cfg := loc.Config.(rest.Config)
|
||||
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
|
||||
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, opts options.Options) (restic.Backend, error) {
|
||||
debug.Log("parsing location %v", s)
|
||||
loc, err := location.Parse(s)
|
||||
if err != nil {
|
||||
return nil, errors.Fatalf("parsing repository location failed: %v", err)
|
||||
}
|
||||
|
||||
var be restic.Backend
|
||||
|
||||
cfg, err := parseConfig(loc, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rt, err := backend.Transport(globalOptions.CACerts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch loc.Scheme {
|
||||
case "local":
|
||||
be, err = local.Open(cfg.(local.Config))
|
||||
case "sftp":
|
||||
be, err = sftp.Open(cfg.(sftp.Config), SuspendSignalHandler, InstallSignalHandler)
|
||||
case "s3":
|
||||
be, err = s3.Open(cfg.(s3.Config), rt)
|
||||
case "gs":
|
||||
be, err = gs.Open(cfg.(gs.Config))
|
||||
case "azure":
|
||||
be, err = azure.Open(cfg.(azure.Config), rt)
|
||||
case "swift":
|
||||
be, err = swift.Open(cfg.(swift.Config), rt)
|
||||
case "b2":
|
||||
be, err = b2.Open(cfg.(b2.Config), rt)
|
||||
case "rest":
|
||||
be, err = rest.Open(cfg.(rest.Config), rt)
|
||||
|
||||
default:
|
||||
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Fatalf("unable to open repo at %v: %v", s, err)
|
||||
}
|
||||
|
||||
// check if config is there
|
||||
fi, err := be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile})
|
||||
if err != nil {
|
||||
return nil, errors.Fatalf("unable to open config file: %v\nIs there a repository at the following location?\n%v", err, s)
|
||||
}
|
||||
|
||||
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, 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
|
||||
}
|
||||
|
||||
rt, err := backend.Transport(globalOptions.CACerts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch loc.Scheme {
|
||||
case "local":
|
||||
return local.Create(cfg.(local.Config))
|
||||
case "sftp":
|
||||
return sftp.Create(cfg.(sftp.Config), SuspendSignalHandler, InstallSignalHandler)
|
||||
case "s3":
|
||||
return s3.Create(cfg.(s3.Config), rt)
|
||||
case "gs":
|
||||
return gs.Create(cfg.(gs.Config))
|
||||
case "azure":
|
||||
return azure.Create(cfg.(azure.Config), rt)
|
||||
case "swift":
|
||||
return swift.Open(cfg.(swift.Config), rt)
|
||||
case "b2":
|
||||
return b2.Create(cfg.(b2.Config), rt)
|
||||
case "rest":
|
||||
return rest.Create(cfg.(rest.Config), rt)
|
||||
}
|
||||
|
||||
debug.Log("invalid repository scheme: %v", s)
|
||||
return nil, errors.Fatalf("invalid scheme %q", loc.Scheme)
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"restic/errors"
|
||||
"restic/repository"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
|
||||
"github.com/pkg/profile"
|
||||
)
|
||||
@@ -18,10 +19,6 @@ var (
|
||||
memProfilePath string
|
||||
cpuProfilePath string
|
||||
insecure bool
|
||||
|
||||
prof interface {
|
||||
Stop()
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -53,10 +50,21 @@ func runDebug() error {
|
||||
return errors.Fatal("only one profile (memory or CPU) may be activated at the same time")
|
||||
}
|
||||
|
||||
var prof interface {
|
||||
Stop()
|
||||
}
|
||||
|
||||
if memProfilePath != "" {
|
||||
prof = profile.Start(profile.Quiet, profile.MemProfile, profile.ProfilePath(memProfilePath))
|
||||
prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.MemProfile, profile.ProfilePath(memProfilePath))
|
||||
} else if cpuProfilePath != "" {
|
||||
prof = profile.Start(profile.Quiet, profile.CPUProfile, profile.ProfilePath(cpuProfilePath))
|
||||
prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.CPUProfile, profile.ProfilePath(cpuProfilePath))
|
||||
}
|
||||
|
||||
if prof != nil {
|
||||
AddCleanupHandler(func() error {
|
||||
prof.Stop()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if insecure {
|
||||
@@ -65,9 +73,3 @@ func runDebug() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func shutdownDebug() {
|
||||
if prof != nil {
|
||||
prof.Stop()
|
||||
}
|
||||
}
|
||||
6
cmd/restic/global_release.go
Normal file
6
cmd/restic/global_release.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// +build !debug
|
||||
|
||||
package main
|
||||
|
||||
// runDebug is a noop without the debug tag.
|
||||
func runDebug() error { return nil }
|
||||
216
cmd/restic/integration_fuse_test.go
Normal file
216
cmd/restic/integration_fuse_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
// +build !openbsd
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
const (
|
||||
mountWait = 20
|
||||
mountSleep = 100 * time.Millisecond
|
||||
mountTestSubdir = "snapshots"
|
||||
)
|
||||
|
||||
func snapshotsDirExists(t testing.TB, dir string) bool {
|
||||
f, err := os.Open(filepath.Join(dir, mountTestSubdir))
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// waitForMount blocks (max mountWait * mountSleep) until the subdir
|
||||
// "snapshots" appears in the dir.
|
||||
func waitForMount(t testing.TB, dir string) {
|
||||
for i := 0; i < mountWait; i++ {
|
||||
if snapshotsDirExists(t, dir) {
|
||||
t.Log("mounted directory is ready")
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(mountSleep)
|
||||
}
|
||||
|
||||
t.Errorf("subdir %q of dir %s never appeared", mountTestSubdir, dir)
|
||||
}
|
||||
|
||||
func testRunMount(t testing.TB, gopts GlobalOptions, dir string) {
|
||||
opts := MountOptions{}
|
||||
rtest.OK(t, runMount(opts, gopts, []string{dir}))
|
||||
}
|
||||
|
||||
func testRunUmount(t testing.TB, gopts GlobalOptions, dir string) {
|
||||
var err error
|
||||
for i := 0; i < mountWait; i++ {
|
||||
if err = umount(dir); err == nil {
|
||||
t.Logf("directory %v umounted", dir)
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(mountSleep)
|
||||
}
|
||||
|
||||
t.Errorf("unable to umount dir %v, last error was: %v", dir, err)
|
||||
}
|
||||
|
||||
func listSnapshots(t testing.TB, dir string) []string {
|
||||
snapshotsDir, err := os.Open(filepath.Join(dir, "snapshots"))
|
||||
rtest.OK(t, err)
|
||||
names, err := snapshotsDir.Readdirnames(-1)
|
||||
rtest.OK(t, err)
|
||||
rtest.OK(t, snapshotsDir.Close())
|
||||
return names
|
||||
}
|
||||
|
||||
func checkSnapshots(t testing.TB, global GlobalOptions, repo *repository.Repository, mountpoint, repodir string, snapshotIDs restic.IDs, expectedSnapshotsInFuseDir int) {
|
||||
t.Logf("checking for %d snapshots: %v", len(snapshotIDs), snapshotIDs)
|
||||
|
||||
go testRunMount(t, global, mountpoint)
|
||||
waitForMount(t, mountpoint)
|
||||
defer testRunUmount(t, global, mountpoint)
|
||||
|
||||
if !snapshotsDirExists(t, mountpoint) {
|
||||
t.Fatal(`virtual directory "snapshots" doesn't exist`)
|
||||
}
|
||||
|
||||
ids := listSnapshots(t, repodir)
|
||||
t.Logf("found %v snapshots in repo: %v", len(ids), ids)
|
||||
|
||||
namesInSnapshots := listSnapshots(t, mountpoint)
|
||||
t.Logf("found %v snapshots in fuse mount: %v", len(namesInSnapshots), namesInSnapshots)
|
||||
rtest.Assert(t,
|
||||
expectedSnapshotsInFuseDir == len(namesInSnapshots),
|
||||
"Invalid number of snapshots: expected %d, got %d", expectedSnapshotsInFuseDir, len(namesInSnapshots))
|
||||
|
||||
namesMap := make(map[string]bool)
|
||||
for _, name := range namesInSnapshots {
|
||||
namesMap[name] = false
|
||||
}
|
||||
|
||||
// Is "latest" present?
|
||||
if len(namesMap) != 0 {
|
||||
_, ok := namesMap["latest"]
|
||||
if !ok {
|
||||
t.Errorf("Symlink latest isn't present in fuse dir")
|
||||
} else {
|
||||
namesMap["latest"] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range snapshotIDs {
|
||||
snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id)
|
||||
rtest.OK(t, err)
|
||||
|
||||
ts := snapshot.Time.Format(time.RFC3339)
|
||||
present, ok := namesMap[ts]
|
||||
if !ok {
|
||||
t.Errorf("Snapshot %v (%q) isn't present in fuse dir", id.Str(), ts)
|
||||
}
|
||||
|
||||
for i := 1; present; i++ {
|
||||
ts = fmt.Sprintf("%s-%d", snapshot.Time.Format(time.RFC3339), i)
|
||||
present, ok = namesMap[ts]
|
||||
if !ok {
|
||||
t.Errorf("Snapshot %v (%q) isn't present in fuse dir", id.Str(), ts)
|
||||
}
|
||||
|
||||
if !present {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
namesMap[ts] = true
|
||||
}
|
||||
|
||||
for name, present := range namesMap {
|
||||
rtest.Assert(t, present, "Directory %s is present in fuse dir but is not a snapshot", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMount(t *testing.T) {
|
||||
if !rtest.RunFuseTest {
|
||||
t.Skip("Skipping fuse tests")
|
||||
}
|
||||
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
repo, err := OpenRepository(env.gopts)
|
||||
rtest.OK(t, err)
|
||||
|
||||
// We remove the mountpoint now to check that cmdMount creates it
|
||||
rtest.RemoveAll(t, env.mountpoint)
|
||||
|
||||
checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, []restic.ID{}, 0)
|
||||
|
||||
rtest.SetupTarTestFixture(t, env.testdata, filepath.Join("testdata", "backup-data.tar.gz"))
|
||||
|
||||
// first backup
|
||||
testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts)
|
||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
||||
rtest.Assert(t, len(snapshotIDs) == 1,
|
||||
"expected one snapshot, got %v", snapshotIDs)
|
||||
|
||||
checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs, 2)
|
||||
|
||||
// second backup, implicit incremental
|
||||
testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts)
|
||||
snapshotIDs = testRunList(t, "snapshots", env.gopts)
|
||||
rtest.Assert(t, len(snapshotIDs) == 2,
|
||||
"expected two snapshots, got %v", snapshotIDs)
|
||||
|
||||
checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs, 3)
|
||||
|
||||
// third backup, explicit incremental
|
||||
bopts := BackupOptions{Parent: snapshotIDs[0].String()}
|
||||
testRunBackup(t, []string{env.testdata}, bopts, env.gopts)
|
||||
snapshotIDs = testRunList(t, "snapshots", env.gopts)
|
||||
rtest.Assert(t, len(snapshotIDs) == 3,
|
||||
"expected three snapshots, got %v", snapshotIDs)
|
||||
|
||||
checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs, 4)
|
||||
}
|
||||
|
||||
func TestMountSameTimestamps(t *testing.T) {
|
||||
if !rtest.RunFuseTest {
|
||||
t.Skip("Skipping fuse tests")
|
||||
}
|
||||
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
rtest.SetupTarTestFixture(t, env.base, filepath.Join("testdata", "repo-same-timestamps.tar.gz"))
|
||||
|
||||
repo, err := OpenRepository(env.gopts)
|
||||
rtest.OK(t, err)
|
||||
|
||||
ids := []restic.ID{
|
||||
restic.TestParseID("280303689e5027328889a06d718b729e96a1ce6ae9ef8290bff550459ae611ee"),
|
||||
restic.TestParseID("75ad6cdc0868e082f2596d5ab8705e9f7d87316f5bf5690385eeff8dbe49d9f5"),
|
||||
restic.TestParseID("5fd0d8b2ef0fa5d23e58f1e460188abb0f525c0f0c4af8365a1280c807a80a1b"),
|
||||
}
|
||||
|
||||
checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, ids, 4)
|
||||
}
|
||||
232
cmd/restic/integration_helpers_test.go
Normal file
232
cmd/restic/integration_helpers_test.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/options"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
type dirEntry struct {
|
||||
path string
|
||||
fi os.FileInfo
|
||||
link uint64
|
||||
}
|
||||
|
||||
func walkDir(dir string) <-chan *dirEntry {
|
||||
ch := make(chan *dirEntry, 100)
|
||||
|
||||
go func() {
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
name, err := filepath.Rel(dir, path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
ch <- &dirEntry{
|
||||
path: name,
|
||||
fi: info,
|
||||
link: nlink(info),
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Walk() error: %v\n", err)
|
||||
}
|
||||
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
// first element is root
|
||||
_ = <-ch
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func isSymlink(fi os.FileInfo) bool {
|
||||
mode := fi.Mode() & (os.ModeType | os.ModeCharDevice)
|
||||
return mode == os.ModeSymlink
|
||||
}
|
||||
|
||||
func sameModTime(fi1, fi2 os.FileInfo) bool {
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "freebsd", "openbsd":
|
||||
if isSymlink(fi1) && isSymlink(fi2) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
same := fi1.ModTime().Equal(fi2.ModTime())
|
||||
if !same && (runtime.GOOS == "darwin" || runtime.GOOS == "openbsd") {
|
||||
// Allow up to 1μs difference, because macOS <10.13 cannot restore
|
||||
// with nanosecond precision and the current version of Go (1.9.2)
|
||||
// does not yet support the new syscall. (#1087)
|
||||
mt1 := fi1.ModTime()
|
||||
mt2 := fi2.ModTime()
|
||||
usecDiff := (mt1.Nanosecond()-mt2.Nanosecond())/1000 + (mt1.Second()-mt2.Second())*1000000
|
||||
same = usecDiff <= 1 && usecDiff >= -1
|
||||
}
|
||||
return same
|
||||
}
|
||||
|
||||
// directoriesEqualContents checks if both directories contain exactly the same
|
||||
// contents.
|
||||
func directoriesEqualContents(dir1, dir2 string) bool {
|
||||
ch1 := walkDir(dir1)
|
||||
ch2 := walkDir(dir2)
|
||||
|
||||
changes := false
|
||||
|
||||
var a, b *dirEntry
|
||||
for {
|
||||
var ok bool
|
||||
|
||||
if ch1 != nil && a == nil {
|
||||
a, ok = <-ch1
|
||||
if !ok {
|
||||
ch1 = nil
|
||||
}
|
||||
}
|
||||
|
||||
if ch2 != nil && b == nil {
|
||||
b, ok = <-ch2
|
||||
if !ok {
|
||||
ch2 = nil
|
||||
}
|
||||
}
|
||||
|
||||
if ch1 == nil && ch2 == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if ch1 == nil {
|
||||
fmt.Printf("+%v\n", b.path)
|
||||
changes = true
|
||||
} else if ch2 == nil {
|
||||
fmt.Printf("-%v\n", a.path)
|
||||
changes = true
|
||||
} else if !a.equals(b) {
|
||||
if a.path < b.path {
|
||||
fmt.Printf("-%v\n", a.path)
|
||||
changes = true
|
||||
a = nil
|
||||
continue
|
||||
} else if a.path > b.path {
|
||||
fmt.Printf("+%v\n", b.path)
|
||||
changes = true
|
||||
b = nil
|
||||
continue
|
||||
} else {
|
||||
fmt.Printf("%%%v\n", a.path)
|
||||
changes = true
|
||||
}
|
||||
}
|
||||
|
||||
a, b = nil, nil
|
||||
}
|
||||
|
||||
if changes {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type dirStat struct {
|
||||
files, dirs, other uint
|
||||
size uint64
|
||||
}
|
||||
|
||||
func isFile(fi os.FileInfo) bool {
|
||||
return fi.Mode()&(os.ModeType|os.ModeCharDevice) == 0
|
||||
}
|
||||
|
||||
// dirStats walks dir and collects stats.
|
||||
func dirStats(dir string) (stat dirStat) {
|
||||
for entry := range walkDir(dir) {
|
||||
if isFile(entry.fi) {
|
||||
stat.files++
|
||||
stat.size += uint64(entry.fi.Size())
|
||||
continue
|
||||
}
|
||||
|
||||
if entry.fi.IsDir() {
|
||||
stat.dirs++
|
||||
continue
|
||||
}
|
||||
|
||||
stat.other++
|
||||
}
|
||||
|
||||
return stat
|
||||
}
|
||||
|
||||
type testEnvironment struct {
|
||||
base, cache, repo, mountpoint, testdata string
|
||||
gopts GlobalOptions
|
||||
}
|
||||
|
||||
// withTestEnvironment creates a test environment and returns a cleanup
|
||||
// function which removes it.
|
||||
func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
|
||||
if !rtest.RunIntegrationTest {
|
||||
t.Skip("integration tests disabled")
|
||||
}
|
||||
|
||||
repository.TestUseLowSecurityKDFParameters(t)
|
||||
|
||||
tempdir, err := ioutil.TempDir(rtest.TestTempDir, "restic-test-")
|
||||
rtest.OK(t, err)
|
||||
|
||||
env = &testEnvironment{
|
||||
base: tempdir,
|
||||
cache: filepath.Join(tempdir, "cache"),
|
||||
repo: filepath.Join(tempdir, "repo"),
|
||||
testdata: filepath.Join(tempdir, "testdata"),
|
||||
mountpoint: filepath.Join(tempdir, "mount"),
|
||||
}
|
||||
|
||||
rtest.OK(t, os.MkdirAll(env.mountpoint, 0700))
|
||||
rtest.OK(t, os.MkdirAll(env.testdata, 0700))
|
||||
rtest.OK(t, os.MkdirAll(env.cache, 0700))
|
||||
rtest.OK(t, os.MkdirAll(env.repo, 0700))
|
||||
|
||||
env.gopts = GlobalOptions{
|
||||
Repo: env.repo,
|
||||
Quiet: true,
|
||||
CacheDir: env.cache,
|
||||
ctx: context.Background(),
|
||||
password: rtest.TestPassword,
|
||||
stdout: os.Stdout,
|
||||
stderr: os.Stderr,
|
||||
extended: make(options.Options),
|
||||
}
|
||||
|
||||
// always overwrite global options
|
||||
globalOptions = env.gopts
|
||||
|
||||
cleanup = func() {
|
||||
if !rtest.TestCleanupTempDirs {
|
||||
t.Logf("leaving temporary directory %v used for test", tempdir)
|
||||
return
|
||||
}
|
||||
rtest.RemoveAll(t, tempdir)
|
||||
}
|
||||
|
||||
return env, cleanup
|
||||
}
|
||||
1313
cmd/restic/integration_test.go
Normal file
1313
cmd/restic/integration_test.go
Normal file
File diff suppressed because it is too large
Load Diff
41
cmd/restic/local_layout_test.go
Normal file
41
cmd/restic/local_layout_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func TestRestoreLocalLayout(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
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("..", "..", "internal", "backend", "testdata", test.filename)
|
||||
|
||||
rtest.SetupTarTestFixture(t, env.base, datafile)
|
||||
|
||||
env.gopts.extended["local.layout"] = test.layout
|
||||
|
||||
// check the repo
|
||||
testRunCheck(t, env.gopts)
|
||||
|
||||
// restore latest snapshot
|
||||
target := filepath.Join(env.base, "restore")
|
||||
testRunRestoreLatest(t, env.gopts, target, nil, "")
|
||||
|
||||
rtest.RemoveAll(t, filepath.Join(env.base, "repo"))
|
||||
rtest.RemoveAll(t, target)
|
||||
}
|
||||
}
|
||||
128
cmd/restic/lock.go
Normal file
128
cmd/restic/lock.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
var globalLocks struct {
|
||||
locks []*restic.Lock
|
||||
cancelRefresh chan struct{}
|
||||
refreshWG sync.WaitGroup
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func lockRepo(repo *repository.Repository) (*restic.Lock, error) {
|
||||
return lockRepository(repo, false)
|
||||
}
|
||||
|
||||
func lockRepoExclusive(repo *repository.Repository) (*restic.Lock, error) {
|
||||
return lockRepository(repo, true)
|
||||
}
|
||||
|
||||
func lockRepository(repo *repository.Repository, exclusive bool) (*restic.Lock, error) {
|
||||
lockFn := restic.NewLock
|
||||
if exclusive {
|
||||
lockFn = restic.NewExclusiveLock
|
||||
}
|
||||
|
||||
lock, err := lockFn(context.TODO(), repo)
|
||||
if err != nil {
|
||||
return nil, errors.Fatalf("unable to create lock in backend: %v", err)
|
||||
}
|
||||
debug.Log("create lock %p (exclusive %v)", lock, exclusive)
|
||||
|
||||
globalLocks.Lock()
|
||||
if globalLocks.cancelRefresh == nil {
|
||||
debug.Log("start goroutine for lock refresh")
|
||||
globalLocks.cancelRefresh = make(chan struct{})
|
||||
globalLocks.refreshWG = sync.WaitGroup{}
|
||||
globalLocks.refreshWG.Add(1)
|
||||
go refreshLocks(&globalLocks.refreshWG, globalLocks.cancelRefresh)
|
||||
}
|
||||
|
||||
globalLocks.locks = append(globalLocks.locks, lock)
|
||||
globalLocks.Unlock()
|
||||
|
||||
return lock, err
|
||||
}
|
||||
|
||||
var refreshInterval = 5 * time.Minute
|
||||
|
||||
func refreshLocks(wg *sync.WaitGroup, done <-chan struct{}) {
|
||||
debug.Log("start")
|
||||
defer func() {
|
||||
wg.Done()
|
||||
globalLocks.Lock()
|
||||
globalLocks.cancelRefresh = nil
|
||||
globalLocks.Unlock()
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(refreshInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
debug.Log("terminate")
|
||||
return
|
||||
case <-ticker.C:
|
||||
debug.Log("refreshing locks")
|
||||
globalLocks.Lock()
|
||||
for _, lock := range globalLocks.locks {
|
||||
err := lock.Refresh(context.TODO())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to refresh lock: %v\n", err)
|
||||
}
|
||||
}
|
||||
globalLocks.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unlockRepo(lock *restic.Lock) error {
|
||||
globalLocks.Lock()
|
||||
defer globalLocks.Unlock()
|
||||
|
||||
debug.Log("unlocking repository with lock %p", lock)
|
||||
if err := lock.Unlock(); err != nil {
|
||||
debug.Log("error while unlocking: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < len(globalLocks.locks); i++ {
|
||||
if lock == globalLocks.locks[i] {
|
||||
globalLocks.locks = append(globalLocks.locks[:i], globalLocks.locks[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unlockAll() error {
|
||||
globalLocks.Lock()
|
||||
defer globalLocks.Unlock()
|
||||
|
||||
debug.Log("unlocking %d locks", len(globalLocks.locks))
|
||||
for _, lock := range globalLocks.locks {
|
||||
if err := lock.Unlock(); err != nil {
|
||||
debug.Log("error while unlocking: %v", err)
|
||||
return err
|
||||
}
|
||||
debug.Log("successfully removed lock")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
AddCleanupHandler(unlockAll)
|
||||
}
|
||||
94
cmd/restic/main.go
Normal file
94
cmd/restic/main.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/options"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
)
|
||||
|
||||
// cmdRoot is the base command when no other command has been specified.
|
||||
var cmdRoot = &cobra.Command{
|
||||
Use: "restic",
|
||||
Short: "Backup and restore files",
|
||||
Long: `
|
||||
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,
|
||||
DisableAutoGenTag: true,
|
||||
|
||||
PersistentPreRunE: func(*cobra.Command, []string) error {
|
||||
// parse extended options
|
||||
opts, err := options.Parse(globalOptions.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalOptions.extended = opts
|
||||
|
||||
pwd, err := resolvePassword(globalOptions, "RESTIC_PASSWORD")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Resolving password failed: %v\n", err)
|
||||
Exit(1)
|
||||
}
|
||||
globalOptions.password = pwd
|
||||
|
||||
// run the debug functions for all subcommands (if build tag "debug" is
|
||||
// enabled)
|
||||
if err := runDebug(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var logBuffer = bytes.NewBuffer(nil)
|
||||
|
||||
func init() {
|
||||
// install custom global logger into a buffer, if an error occurs
|
||||
// we can show the logs
|
||||
log.SetOutput(logBuffer)
|
||||
}
|
||||
|
||||
func main() {
|
||||
debug.Log("main %#v", os.Args)
|
||||
debug.Log("restic %s, compiled with %v on %v/%v",
|
||||
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
err := cmdRoot.Execute()
|
||||
|
||||
switch {
|
||||
case restic.IsAlreadyLocked(errors.Cause(err)):
|
||||
fmt.Fprintf(os.Stderr, "%v\nthe `unlock` command can be used to remove stale locks\n", err)
|
||||
case errors.IsFatal(errors.Cause(err)):
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var exitCode int
|
||||
if err != nil {
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
Exit(exitCode)
|
||||
}
|
||||
63
cmd/restic/table.go
Normal file
63
cmd/restic/table.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Table contains data for a table to be printed.
|
||||
type Table struct {
|
||||
Header string
|
||||
Rows [][]interface{}
|
||||
Footer string
|
||||
|
||||
RowFormat string
|
||||
}
|
||||
|
||||
// NewTable initializes a new Table.
|
||||
func NewTable() Table {
|
||||
return Table{
|
||||
Rows: [][]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func (t Table) printSeparationLine(w io.Writer) error {
|
||||
_, err := fmt.Fprintln(w, strings.Repeat("-", 70))
|
||||
return err
|
||||
}
|
||||
|
||||
// Write prints the table to w.
|
||||
func (t Table) Write(w io.Writer) error {
|
||||
_, err := fmt.Fprintln(w, t.Header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = t.printSeparationLine(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, row := range t.Rows {
|
||||
_, err = fmt.Fprintf(w, t.RowFormat+"\n", row...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = t.printSeparationLine(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(w, t.Footer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TimeFormat is the format used for all timestamps printed by restic.
|
||||
const TimeFormat = "2006-01-02 15:04:05"
|
||||
BIN
cmd/restic/testdata/small-repo.tar.gz
vendored
Normal file
BIN
cmd/restic/testdata/small-repo.tar.gz
vendored
Normal file
Binary file not shown.
16
doc/010_introduction.rst
Normal file
16
doc/010_introduction.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
############
|
||||
Introduction
|
||||
############
|
||||
|
||||
149
doc/020_installation.rst
Normal file
149
doc/020_installation.rst
Normal file
@@ -0,0 +1,149 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
############
|
||||
Installation
|
||||
############
|
||||
|
||||
Packages
|
||||
********
|
||||
|
||||
Mac OS X
|
||||
========
|
||||
|
||||
If you are using Mac OS X, you can install restic using the
|
||||
`homebrew <http://brew.sh/>`__ package manager:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ brew install restic
|
||||
|
||||
Arch Linux
|
||||
==========
|
||||
|
||||
On `Arch Linux <https://www.archlinux.org/>`__, there is a package called ``restic-git``
|
||||
which can be installed from AUR, e.g. with ``pacaur``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pacaur -S restic-git
|
||||
|
||||
Nix & NixOS
|
||||
===========
|
||||
|
||||
If you are using `Nix <https://nixos.org/nix/>`__ or `NixOS <https://nixos.org/>`__
|
||||
there is a package available named ``restic``.
|
||||
It can be installed uisng ``nix-env``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ nix-env --install restic
|
||||
|
||||
Debian
|
||||
======
|
||||
|
||||
On Debian, there's a package called ``restic`` which can be
|
||||
installed from the official repos, e.g. with ``apt-get``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ apt-get install restic
|
||||
|
||||
|
||||
.. warning:: Please be aware that, at the time of writing, Debian *stable*
|
||||
has ``restic`` version 0.3.3 which is very old. The *testing* and *unstable*
|
||||
branches have recent versions of ``restic``.
|
||||
|
||||
Pre-compiled Binary
|
||||
*******************
|
||||
|
||||
You can download the latest pre-compiled binary from the `restic release
|
||||
page <https://github.com/restic/restic/releases/latest>`__.
|
||||
|
||||
Docker Container
|
||||
****************
|
||||
|
||||
.. note::
|
||||
| A docker container is available as a contribution (Thank you!).
|
||||
| You can find it at https://github.com/Lobaro/restic-backup-docker
|
||||
|
||||
From Source
|
||||
***********
|
||||
|
||||
restic is written in the Go programming language and you need at least
|
||||
Go version 1.8. 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.
|
||||
|
||||
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.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ 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)
|
||||
|
||||
|
||||
402
doc/030_preparing_a_new_repo.rst
Normal file
402
doc/030_preparing_a_new_repo.rst
Normal file
@@ -0,0 +1,402 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
##########################
|
||||
Preparing a new repository
|
||||
##########################
|
||||
|
||||
The place where your backups will be saved at is called a "repository".
|
||||
This chapter explains how to create ("init") such a repository.
|
||||
|
||||
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.
|
||||
|
||||
.. warning::
|
||||
|
||||
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_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.
|
||||
|
||||
.. note:: Please be aware that sftp servers do not expand the tilde character
|
||||
(``~``) normally used as an alias for a user's home directory. If you
|
||||
want to specify a path relative to the user's home directory, pass a
|
||||
relative path to the sftp backend.
|
||||
|
||||
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, restic will use the system's CA certificates to verify the
|
||||
server certificate. When the verification fails, restic refuses to proceed and
|
||||
exits with an error. If you have your own self-signed certificate, or a custom
|
||||
CA certificate should be used for verification, you can pass restic the
|
||||
certificate filename via the `--cacert` option.
|
||||
|
||||
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.
|
||||
|
||||
Until version 0.8.0, restic used a default prefix of ``restic``, so the files
|
||||
in the bucket were placed in a directory named ``restic``. If you want to
|
||||
access a repository created with an older version of restic, specify the path
|
||||
after the bucket name like this:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r s3:s3.amazonaws.com/bucket_name/restic [...]
|
||||
|
||||
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.
|
||||
|
||||
OpenStack Swift
|
||||
***************
|
||||
|
||||
Restic can backup data to an OpenStack Swift container. Because Swift supports
|
||||
various authentication methods, credentials are passed through environment
|
||||
variables. In order to help integration with existing OpenStack installations,
|
||||
the naming convention of those variables follows official python swift client:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
# For keystone v1 authentication
|
||||
$ export ST_AUTH=<MY_AUTH_URL>
|
||||
$ export ST_USER=<MY_USER_NAME>
|
||||
$ export ST_KEY=<MY_USER_PASSWORD>
|
||||
|
||||
# For keystone v2 authentication (some variables are optional)
|
||||
$ export OS_AUTH_URL=<MY_AUTH_URL>
|
||||
$ export OS_REGION_NAME=<MY_REGION_NAME>
|
||||
$ export OS_USERNAME=<MY_USERNAME>
|
||||
$ export OS_PASSWORD=<MY_PASSWORD>
|
||||
$ export OS_TENANT_ID=<MY_TENANT_ID>
|
||||
$ export OS_TENANT_NAME=<MY_TENANT_NAME>
|
||||
|
||||
# For keystone v3 authentication (some variables are optional)
|
||||
$ export OS_AUTH_URL=<MY_AUTH_URL>
|
||||
$ export OS_REGION_NAME=<MY_REGION_NAME>
|
||||
$ export OS_USERNAME=<MY_USERNAME>
|
||||
$ export OS_PASSWORD=<MY_PASSWORD>
|
||||
$ export OS_USER_DOMAIN_NAME=<MY_DOMAIN_NAME>
|
||||
$ export OS_PROJECT_NAME=<MY_PROJECT_NAME>
|
||||
$ export OS_PROJECT_DOMAIN_NAME=<MY_PROJECT_DOMAIN_NAME>
|
||||
|
||||
# For authentication based on tokens
|
||||
$ export OS_STORAGE_URL=<MY_STORAGE_URL>
|
||||
$ export OS_AUTH_TOKEN=<MY_AUTH_TOKEN>
|
||||
|
||||
|
||||
Restic should be compatible with `OpenStack RC file
|
||||
<https://docs.openstack.org/user-guide/common/cli-set-environment-variables-using-openstack-rc.html>`__
|
||||
in most cases.
|
||||
|
||||
Once environment variables are set up, a new repository can be created. The
|
||||
name of swift container and optional path can be specified. If
|
||||
the container does not exist, it will be created automatically:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r swift:container_name:/path init # path is optional
|
||||
enter password for new backend:
|
||||
enter password again:
|
||||
created restic backend eefee03bbd at swift:container_name:/path
|
||||
Please note that knowledge of your password is required to access the repository.
|
||||
Losing your password means that your data is irrecoverably lost.
|
||||
|
||||
The policy of new container created by restic can be changed using environment variable:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export SWIFT_DEFAULT_CONTAINER_POLICY=<MY_CONTAINER_POLICY>
|
||||
|
||||
|
||||
Backblaze B2
|
||||
************
|
||||
|
||||
Restic can backup data to any Backblaze B2 bucket. You need to first setup the
|
||||
following environment variables with the credentials you obtained when signed
|
||||
into your B2 account:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export B2_ACCOUNT_ID=<MY_ACCOUNT_ID>
|
||||
$ export B2_ACCOUNT_KEY=<MY_SECRET_ACCOUNT_KEY>
|
||||
|
||||
You can then easily initialize a repository stored at Backblaze B2. If the
|
||||
bucket does not exist yet, it will be created:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r b2:bucketname:path/to/repo init
|
||||
enter password for new backend:
|
||||
enter password again:
|
||||
created restic backend eefee03bbd at b2:bucketname:path/to/repo
|
||||
Please note that knowledge of your password is required to access the repository.
|
||||
Losing your password means that your data is irrecoverably lost.
|
||||
|
||||
The number of concurrent connections to the B2 service can be set with the `-o
|
||||
b2.connections=10`. By default, at most five parallel connections are
|
||||
established.
|
||||
|
||||
Microsoft Azure Blob Storage
|
||||
****************************
|
||||
|
||||
You can also store backups on Microsoft Azure Blob Storage. Export the Azure
|
||||
account name and key as follows:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export AZURE_ACCOUNT_NAME=<ACCOUNT_NAME>
|
||||
$ export AZURE_ACCOUNT_KEY=<SECRET_KEY>
|
||||
|
||||
Afterwards you can initialize a repository in a container called `foo` in the
|
||||
root path like this:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r azure:foo:/ init
|
||||
enter password for new backend:
|
||||
enter password again:
|
||||
|
||||
created restic backend a934bac191 at azure:foo:/
|
||||
[...]
|
||||
|
||||
The number of concurrent connections to the B2 service can be set with the
|
||||
`-o azure.connections=10`. By default, at most five parallel connections are
|
||||
established.
|
||||
|
||||
Google Cloud Storage
|
||||
********************
|
||||
|
||||
Restic supports Google Cloud Storage as a backend.
|
||||
|
||||
Restic connects to Google Cloud Storage via a `service account`_.
|
||||
|
||||
For normal restic operation, the service account must have the
|
||||
``storage.objects.{create,delete,get,list}`` permissions for the bucket. These
|
||||
are included in the "Storage Object Admin" role.
|
||||
``restic init`` can create the repository bucket. Doing so requires the
|
||||
``storage.buckets.create`` permission ("Storage Admin" role). If the bucket
|
||||
already exists, that permission is unnecessary.
|
||||
|
||||
To use the Google Cloud Storage backend, first `create a service account key`_
|
||||
and download the JSON credentials file.
|
||||
Second, find the Google Project ID that you can see in the Google Cloud
|
||||
Platform console at the "Storage/Settings" menu. Export the path to the JSON
|
||||
key file and the project ID as follows:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ export GOOGLE_PROJECT_ID=123123123123
|
||||
$ export GOOGLE_APPLICATION_CREDENTIALS=$HOME/.config/gs-secret-restic-key.json
|
||||
|
||||
Then you can use the ``gs:`` backend type to create a new repository in the
|
||||
bucket `foo` at the root path:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r gs:foo:/ init
|
||||
enter password for new backend:
|
||||
enter password again:
|
||||
|
||||
created restic backend bde47d6254 at gs:foo2/
|
||||
[...]
|
||||
|
||||
The number of concurrent connections to the GCS service can be set with the
|
||||
`-o gs.connections=10`. By default, at most five parallel connections are
|
||||
established.
|
||||
|
||||
.. _service account: https://cloud.google.com/storage/docs/authentication#service_accounts
|
||||
.. _create a service account key: https://cloud.google.com/storage/docs/authentication#generating-a-private-key
|
||||
|
||||
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
|
||||
|
||||
180
doc/040_backup.rst
Normal file
180
doc/040_backup.rst
Normal file
@@ -0,0 +1,180 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
##########
|
||||
Backing up
|
||||
##########
|
||||
|
||||
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 ~/work
|
||||
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 [/home/user/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.
|
||||
|
||||
Now is a good time to run ``restic check`` to verify that all data
|
||||
is properly stored in the repository. You should run this command regularly
|
||||
to make sure the internal structure of the repository is free of errors.
|
||||
|
||||
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.
|
||||
|
||||
or 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
|
||||
|
||||
Backing up special items
|
||||
************************
|
||||
|
||||
**Symlinks** are archived as symlinks, ``restic`` does not follow them.
|
||||
When you restore, you get the same symlink again, with the same link target
|
||||
and the same timestamps.
|
||||
|
||||
If there is a **bind-mount** below a directory that is to be saved, restic descends into it.
|
||||
|
||||
**Device files** are saved and restored as device files. This means that e.g. ``/dev/sda`` is
|
||||
archived as a block device file and restored as such. This also means that the content of the
|
||||
corresponding disk is not read, at least not from the device 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 for backup
|
||||
***************
|
||||
|
||||
Snapshots can have one or more tags, short strings which add identifying
|
||||
information. Just specify the tags for a snapshot one by one with ``--tag``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /tmp/backup backup --tag projectX --tag foo --tag bar ~/work
|
||||
[...]
|
||||
|
||||
The tags can later be used to keep (or forget) snapshots with the ``forget``
|
||||
command. The command ``tag`` can be used to modify tags on an existing
|
||||
snapshot.
|
||||
89
doc/045_working_with_repos.rst
Normal file
89
doc/045_working_with_repos.rst
Normal file
@@ -0,0 +1,89 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
|
||||
#########################
|
||||
Working with repositories
|
||||
#########################
|
||||
|
||||
Listing 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.
|
||||
|
||||
|
||||
Checking a repo's 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
|
||||
|
||||
82
doc/050_restore.rst
Normal file
82
doc/050_restore.rst
Normal file
@@ -0,0 +1,82 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
#####################
|
||||
Restoring from backup
|
||||
#####################
|
||||
|
||||
Restoring from 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-art --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-art
|
||||
|
||||
Use ``--exclude`` and ``--include`` to restrict the restore to a subset of
|
||||
files in the snapshot. For example, to restore a single file:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /tmp/backup restore 79766175 --target /tmp/restore-work --include /work/foo
|
||||
enter password for repository:
|
||||
restoring <Snapshot of [/home/user/work] at 2015-05-08 21:40:19.884408621 +0200 CEST> to /tmp/restore-work
|
||||
|
||||
This will restore the file ``foo`` to ``/tmp/restore-work/work/foo``.
|
||||
|
||||
Restore using mount
|
||||
===================
|
||||
|
||||
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 /mnt/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.
|
||||
|
||||
Printing files to stdout
|
||||
========================
|
||||
|
||||
Sometimes it's helpful to print files to stdout so that other programs can read
|
||||
the data directly. This can be achieved by using the `dump` command, like this:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic -r /tmp/backup dump latest production.sql | mysql
|
||||
208
doc/060_forget.rst
Normal file
208
doc/060_forget.rst
Normal file
@@ -0,0 +1,208 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
#########################
|
||||
Removing backup 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.
|
||||
|
||||
It is advisable to run ``restic check`` after pruning, to make sure
|
||||
you are alerted, should the internal data structures of the repository
|
||||
be damaged.
|
||||
|
||||
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 grouping
|
||||
options can be set with ``--group-by``, to only group snapshots by paths and
|
||||
tags use ``--group-by paths,tags``. 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. For example, the following command
|
||||
removes all but the latest snapshot of all snapshots that have the tag ``foo``:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic forget --tag foo --keep-last 1
|
||||
|
||||
This command removes all but the last snapshot of all snapshots that have
|
||||
either the ``foo`` or ``bar`` tag set:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic forget --tag foo --tag bar --keep-last 1
|
||||
|
||||
To only keep the last snapshot of all snapshots with both the tag ``foo`` and
|
||||
``bar`` set use:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ restic forget --tag foo,tag bar --keep-last 1
|
||||
|
||||
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.
|
||||
|
||||
51
doc/070_encryption.rst
Normal file
51
doc/070_encryption.rst
Normal file
@@ -0,0 +1,51 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
##########
|
||||
Encryption
|
||||
##########
|
||||
|
||||
|
||||
*"The design might not be perfect, but it’s good. Encryption is a first-class feature,
|
||||
the implementation looks sane and I guess the deduplication trade-off is worth it. So… I’m going to use restic for
|
||||
my personal backups.*" `Filippo Valsorda`_
|
||||
|
||||
.. _Filippo Valsorda: https://blog.filippo.io/restic-cryptography/
|
||||
|
||||
**********************
|
||||
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
|
||||
272
doc/080_examples.rst
Normal file
272
doc/080_examples.rst
Normal file
@@ -0,0 +1,272 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
########
|
||||
Examples
|
||||
########
|
||||
|
||||
********************************
|
||||
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 now 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="LaJtZPoVvGbXsaD2LsxvJZF/7LRi4FhT0TK4gDQq"
|
||||
$ 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.
|
||||
|
||||
140
doc/090_participating.rst
Normal file
140
doc/090_participating.rst
Normal file
@@ -0,0 +1,140 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
#############
|
||||
Participating
|
||||
#############
|
||||
|
||||
*********
|
||||
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
|
||||
|
||||
|
||||
************
|
||||
Contributing
|
||||
************
|
||||
|
||||
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 <https://github.com/restic/restic/blob/master/CONTRIBUTING.md>`__.
|
||||
A document describing the design of restic and the data structures stored on the
|
||||
back end is contained in `Design <https://restic.readthedocs.io/en/latest/design.html>`__.
|
||||
|
||||
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
|
||||
763
doc/100_references.rst
Normal file
763
doc/100_references.rst
Normal file
@@ -0,0 +1,763 @@
|
||||
..
|
||||
Normally, there are no heading levels assigned to certain characters as the structure is
|
||||
determined from the succession of headings. However, this convention is used in Python’s
|
||||
Style Guide for documenting which you may follow:
|
||||
|
||||
# with overline, for parts
|
||||
* for chapters
|
||||
= for sections
|
||||
- for subsections
|
||||
^ for subsubsections
|
||||
" for paragraphs
|
||||
|
||||
##########
|
||||
References
|
||||
##########
|
||||
|
||||
******
|
||||
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 keys are used to authenticate and decrypt the bytes contained in
|
||||
the JSON field ``data`` with AES-256 and Poly1305-AES as if they were
|
||||
any other blob (after removing the Base64 encoding). 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 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.
|
||||
|
||||
Local Cache
|
||||
===========
|
||||
|
||||
In order to speed up certain operations, restic manages a local cache of data.
|
||||
This document describes the data structures for the local cache with version 1.
|
||||
|
||||
Versions
|
||||
--------
|
||||
|
||||
The cache directory is selected according to the `XDG base dir specification
|
||||
<http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html>`__.
|
||||
Each repository has its own cache sub-directory, consting of the repository ID
|
||||
which is chosen at ``init``. All cache directories for different repos are
|
||||
independent of each other.
|
||||
|
||||
The cache dir for a repo contains a file named ``version``, which contains a
|
||||
single ASCII integer line that stands for the current version of the cache. If
|
||||
a lower version number is found the cache is recreated with the current
|
||||
version. If a higher version number is found the cache is ignored and left as
|
||||
is.
|
||||
|
||||
Snapshots and Indexes
|
||||
---------------------
|
||||
|
||||
Snapshot, Data and Index files are cached in the sub-directories ``snapshots``,
|
||||
``data`` and ``index``, as read from the repository.
|
||||
|
||||
|
||||
************
|
||||
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.
|
||||
|
||||
|
||||
*****
|
||||
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)
|
||||
@@ -1 +0,0 @@
|
||||
design.rst
|
||||
1193
doc/bash-completion.sh
Normal file
1193
doc/bash-completion.sh
Normal file
File diff suppressed because it is too large
Load Diff
26
doc/cache.rst
Normal file
26
doc/cache.rst
Normal file
@@ -0,0 +1,26 @@
|
||||
Local Cache
|
||||
===========
|
||||
|
||||
In order to speed up certain operations, restic manages a local cache of data.
|
||||
This document describes the data structures for the local cache with version 1.
|
||||
|
||||
Versions
|
||||
--------
|
||||
|
||||
The cache directory is selected according to the `XDG base dir specification
|
||||
<http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html>`__.
|
||||
Each repository has its own cache sub-directory, consting of the repository ID
|
||||
which is chosen at ``init``. All cache directories for different repos are
|
||||
independent of each other.
|
||||
|
||||
The cache dir for a repo contains a file named ``version``, which contains a
|
||||
single ASCII integer line that stands for the current version of the cache. If
|
||||
a lower version number is found the cache is recreated with the current
|
||||
version. If a higher version number is found the cache is ignored and left as
|
||||
is.
|
||||
|
||||
Snapshots and Indexes
|
||||
---------------------
|
||||
|
||||
Snapshot, Data and Index files are cached in the sub-directories ``snapshots``,
|
||||
``data`` and ``index``, as read from the repository.
|
||||
@@ -19,7 +19,7 @@ import os
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = []
|
||||
extensions = ['sphinx.ext.extlinks']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
@@ -104,3 +104,7 @@ html_static_path = ['_static']
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'resticdoc'
|
||||
|
||||
extlinks = {
|
||||
'issue': ('https://github.com/restic/restic/issues/%s', '#'),
|
||||
}
|
||||
|
||||
610
doc/design.rst
610
doc/design.rst
@@ -1,610 +0,0 @@
|
||||
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.
|
||||
@@ -1,69 +0,0 @@
|
||||
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 <https://github.com/restic/restic/blob/master/CONTRIBUTING.md>`__.
|
||||
A document describing the design of restic and the data structures stored on the
|
||||
back end is contained in `Design <https://restic.readthedocs.io/en/latest/design.html>`__.
|
||||
|
||||
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
|
||||
89
doc/faq.rst
89
doc/faq.rst
@@ -26,3 +26,92 @@ 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.
|
||||
|
||||
How can I specify encryption passwords automatically?
|
||||
-----------------------------------------------------
|
||||
|
||||
When you run ``restic backup``, you need to enter the passphrase on
|
||||
the console. This is not very convenient for automated backups, so you
|
||||
can also provide the password through the ``--password-file`` option, or one of
|
||||
the environment variables ``RESTIC_PASSWORD`` or ``RESTIC_PASSWORD_FILE``.
|
||||
A discussion is in progress over implementing unattended backups happens in
|
||||
:issue:`533`.
|
||||
|
||||
.. important:: Be careful how you set the environment; using the env
|
||||
command, a `system()` call or using inline shell
|
||||
scripts (e.g. `RESTIC_PASSWORD=password restic ...`)
|
||||
might expose the credentials in the process list
|
||||
directly and they will be readable to all users on a
|
||||
system. Using export in a shell script file should be
|
||||
safe, however, as the environment of a process is
|
||||
`accessible only to that user`_. Please make sure that
|
||||
the permissions on the files where the password is
|
||||
eventually stored are safe (e.g. `0600` and owned by
|
||||
root).
|
||||
|
||||
.. _accessible only to that user: https://security.stackexchange.com/questions/14000/environment-variable-accessibility-in-linux/14009#14009
|
||||
|
||||
How to prioritize restic's IO and CPU time
|
||||
------------------------------------------
|
||||
|
||||
If you'd like to change the **IO priority** of restic, run it in the following way
|
||||
|
||||
::
|
||||
|
||||
$ ionice -c2 -n0 ./restic -r /media/your/backup/ backup /home
|
||||
|
||||
This runs ``restic`` in the so-called best *effort class* (``-c2``),
|
||||
with the highest possible priority (``-n0``).
|
||||
|
||||
Take a look at the `ionice manpage`_ to learn about the other classes.
|
||||
|
||||
.. _ionice manpage: https://linux.die.net/man/1/ionice
|
||||
|
||||
|
||||
To change the **CPU scheduling priority** to a higher-than-standard
|
||||
value, use would run:
|
||||
|
||||
::
|
||||
|
||||
$ nice --10 ./restic -r /media/your/backup/ backup /home
|
||||
|
||||
Again, the `nice manpage`_ has more information.
|
||||
|
||||
.. _nice manpage: https://linux.die.net/man/1/nice
|
||||
|
||||
You can also **combine IO and CPU scheduling priority**:
|
||||
|
||||
::
|
||||
|
||||
$ ionice -c2 nice -n19 ./restic -r /media/gour/backup/ backup /home
|
||||
|
||||
This example puts restic in the IO class 2 (best effort) and tells the CPU
|
||||
scheduling algorithm to give it the least favorable niceness (19).
|
||||
|
||||
The above example makes sure that the system the backup runs on
|
||||
is not slowed down, which is particularly useful for servers.
|
||||
|
||||
Creating new repo on a Synology NAS via sftp fails
|
||||
--------------------------------------------------
|
||||
|
||||
Sometimes creating a new restic repository on a Synology NAS via sftp fails
|
||||
with an error similar to the following:
|
||||
|
||||
::
|
||||
|
||||
$ restic init -r sftp:user@nas:/volume1/restic-repo init
|
||||
create backend at sftp:user@nas:/volume1/restic-repo/ failed:
|
||||
mkdirAll(/volume1/restic-repo/index): unable to create directories: [...]
|
||||
|
||||
Although you can log into the NAS via SSH and see that the directory structure
|
||||
is there.
|
||||
|
||||
The reason for this behavior is that apparently Synology NAS expose a different
|
||||
directory structure via sftp, so the path that needs to be specified is
|
||||
different than the directory structure on the device and maybe even as exposed
|
||||
via other protocols.
|
||||
|
||||
The following may work:
|
||||
|
||||
::
|
||||
$ restic init -r sftp:user@nas:/restic-repo init
|
||||
|
||||
@@ -4,12 +4,16 @@ Restic Documentation
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
manual
|
||||
010_introduction
|
||||
020_installation
|
||||
030_preparing_a_new_repo
|
||||
040_backup
|
||||
045_working_with_repos
|
||||
050_restore
|
||||
060_forget
|
||||
070_encryption
|
||||
080_examples
|
||||
090_participating
|
||||
100_references
|
||||
faq
|
||||
tutorials
|
||||
development
|
||||
references
|
||||
talks
|
||||
|
||||
.. include:: ../README.rst
|
||||
manual_rest
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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.
|
||||
128
doc/man/restic-backup.1
Normal file
128
doc/man/restic-backup.1
Normal file
@@ -0,0 +1,128 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-backup \- Create a new backup of files and/or directories
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic backup [flags] FILE/DIR [FILE/DIR] ...\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "backup" command creates a new snapshot and saves the files and directories
|
||||
given as the arguments.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-e\fP, \fB\-\-exclude\fP=[]
|
||||
exclude a \fB\fCpattern\fR (can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-\-exclude\-caches\fP[=false]
|
||||
excludes cache directories that are marked with a CACHEDIR.TAG file
|
||||
|
||||
.PP
|
||||
\fB\-\-exclude\-file\fP=[]
|
||||
read exclude patterns from a \fB\fCfile\fR (can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-\-exclude\-if\-present\fP=[]
|
||||
takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-\-files\-from\fP=""
|
||||
read the files to backup from file (can be combined with file args)
|
||||
|
||||
.PP
|
||||
\fB\-f\fP, \fB\-\-force\fP[=false]
|
||||
force re\-reading the target files/directories (overrides the "parent" flag)
|
||||
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for backup
|
||||
|
||||
.PP
|
||||
\fB\-\-hostname\fP=""
|
||||
set the \fB\fChostname\fR for the snapshot manually. To prevent an expensive rescan use the "parent" flag
|
||||
|
||||
.PP
|
||||
\fB\-x\fP, \fB\-\-one\-file\-system\fP[=false]
|
||||
exclude other file systems
|
||||
|
||||
.PP
|
||||
\fB\-\-parent\fP=""
|
||||
use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)
|
||||
|
||||
.PP
|
||||
\fB\-\-stdin\fP[=false]
|
||||
read backup from stdin
|
||||
|
||||
.PP
|
||||
\fB\-\-stdin\-filename\fP="stdin"
|
||||
file name to use when reading from stdin
|
||||
|
||||
.PP
|
||||
\fB\-\-tag\fP=[]
|
||||
add a \fB\fCtag\fR for the new snapshot (can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-\-time\fP=""
|
||||
time of the backup (ex. '2012\-11\-01 22:08:41') (default: now)
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
75
doc/man/restic-cat.1
Normal file
75
doc/man/restic-cat.1
Normal file
@@ -0,0 +1,75 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-cat \- Print internal objects to stdout
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic cat [flags] [pack|blob|snapshot|index|key|masterkey|config|lock] ID\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "cat" command is used to print internal objects to stdout.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for cat
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
92
doc/man/restic-check.1
Normal file
92
doc/man/restic-check.1
Normal file
@@ -0,0 +1,92 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-check \- Check the repository for errors
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic check [flags]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "check" command tests the repository for errors and reports any errors it
|
||||
finds. It can also be used to read all data and therefore simulate a restore.
|
||||
|
||||
.PP
|
||||
By default, the "check" command will always load all data directly from the
|
||||
repository and not use a local cache.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-\-check\-unused\fP[=false]
|
||||
find unused blobs
|
||||
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for check
|
||||
|
||||
.PP
|
||||
\fB\-\-read\-data\fP[=false]
|
||||
read all data blobs
|
||||
|
||||
.PP
|
||||
\fB\-\-with\-cache\fP[=false]
|
||||
use the cache
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
92
doc/man/restic-dump.1
Normal file
92
doc/man/restic-dump.1
Normal file
@@ -0,0 +1,92 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-dump \- Print a backed\-up file to stdout
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic dump [flags] snapshotID file\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "dump" command extracts a single file from a snapshot from the repository and
|
||||
prints its contents to stdout.
|
||||
|
||||
.PP
|
||||
The special snapshot "latest" can be used to use the latest snapshot in the
|
||||
repository.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for dump
|
||||
|
||||
.PP
|
||||
\fB\-H\fP, \fB\-\-host\fP=""
|
||||
only consider snapshots for this host when the snapshot ID is "latest"
|
||||
|
||||
.PP
|
||||
\fB\-\-path\fP=[]
|
||||
only consider snapshots which include this (absolute) \fB\fCpath\fR for snapshot ID "latest"
|
||||
|
||||
.PP
|
||||
\fB\-\-tag\fP=[]
|
||||
only consider snapshots which include this \fB\fCtaglist\fR for snapshot ID "latest"
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
108
doc/man/restic-find.1
Normal file
108
doc/man/restic-find.1
Normal file
@@ -0,0 +1,108 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-find \- Find a file or directory
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic find [flags] PATTERN\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "find" command searches for files or directories in snapshots stored in the
|
||||
repo.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for find
|
||||
|
||||
.PP
|
||||
\fB\-H\fP, \fB\-\-host\fP=""
|
||||
only consider snapshots for this \fB\fChost\fR, when no snapshot ID is given
|
||||
|
||||
.PP
|
||||
\fB\-i\fP, \fB\-\-ignore\-case\fP[=false]
|
||||
ignore case for pattern
|
||||
|
||||
.PP
|
||||
\fB\-l\fP, \fB\-\-long\fP[=false]
|
||||
use a long listing format showing size and mode
|
||||
|
||||
.PP
|
||||
\fB\-N\fP, \fB\-\-newest\fP=""
|
||||
newest modification date/time
|
||||
|
||||
.PP
|
||||
\fB\-O\fP, \fB\-\-oldest\fP=""
|
||||
oldest modification date/time
|
||||
|
||||
.PP
|
||||
\fB\-\-path\fP=[]
|
||||
only consider snapshots which include this (absolute) \fB\fCpath\fR, when no snapshot\-ID is given
|
||||
|
||||
.PP
|
||||
\fB\-s\fP, \fB\-\-snapshot\fP=[]
|
||||
snapshot \fB\fCid\fR to search in (can be given multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-\-tag\fP=[]
|
||||
only consider snapshots which include this \fB\fCtaglist\fR, when no snapshot\-ID is given
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
138
doc/man/restic-forget.1
Normal file
138
doc/man/restic-forget.1
Normal file
@@ -0,0 +1,138 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-forget \- Remove snapshots from the repository
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic forget [flags] [snapshot ID] [...]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "forget" command removes snapshots according to a policy. Please note that
|
||||
this command really only deletes the snapshot object in the repository, which
|
||||
is a reference to data stored there. In order to remove this (now unreferenced)
|
||||
data after 'forget' was run successfully, see the 'prune' command.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-l\fP, \fB\-\-keep\-last\fP=0
|
||||
keep the last \fB\fCn\fR snapshots
|
||||
|
||||
.PP
|
||||
\fB\-H\fP, \fB\-\-keep\-hourly\fP=0
|
||||
keep the last \fB\fCn\fR hourly snapshots
|
||||
|
||||
.PP
|
||||
\fB\-d\fP, \fB\-\-keep\-daily\fP=0
|
||||
keep the last \fB\fCn\fR daily snapshots
|
||||
|
||||
.PP
|
||||
\fB\-w\fP, \fB\-\-keep\-weekly\fP=0
|
||||
keep the last \fB\fCn\fR weekly snapshots
|
||||
|
||||
.PP
|
||||
\fB\-m\fP, \fB\-\-keep\-monthly\fP=0
|
||||
keep the last \fB\fCn\fR monthly snapshots
|
||||
|
||||
.PP
|
||||
\fB\-y\fP, \fB\-\-keep\-yearly\fP=0
|
||||
keep the last \fB\fCn\fR yearly snapshots
|
||||
|
||||
.PP
|
||||
\fB\-\-keep\-tag\fP=[]
|
||||
keep snapshots with this \fB\fCtaglist\fR (can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-\-host\fP=""
|
||||
only consider snapshots with the given \fB\fChost\fR
|
||||
|
||||
.PP
|
||||
\fB\-\-hostname\fP=""
|
||||
only consider snapshots with the given \fB\fChostname\fR (deprecated)
|
||||
|
||||
.PP
|
||||
\fB\-\-tag\fP=[]
|
||||
only consider snapshots which include this \fB\fCtaglist\fR in the format \fB\fCtag[,tag,...]\fR (can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-\-path\fP=[]
|
||||
only consider snapshots which include this (absolute) \fB\fCpath\fR (can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-c\fP, \fB\-\-compact\fP[=false]
|
||||
use compact format
|
||||
|
||||
.PP
|
||||
\fB\-g\fP, \fB\-\-group\-by\fP="host,paths"
|
||||
string for grouping snapshots by host,paths,tags
|
||||
|
||||
.PP
|
||||
\fB\-n\fP, \fB\-\-dry\-run\fP[=false]
|
||||
do not delete anything, just print what would be done
|
||||
|
||||
.PP
|
||||
\fB\-\-prune\fP[=false]
|
||||
automatically run the 'prune' command if snapshots have been removed
|
||||
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for forget
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
88
doc/man/restic-generate.1
Normal file
88
doc/man/restic-generate.1
Normal file
@@ -0,0 +1,88 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-generate \- Generate manual pages and auto\-completion files (bash, zsh)
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic generate [command] [flags]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "generate" command writes automatically generated files like the man pages
|
||||
and the auto\-completion files for bash and zsh).
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-\-bash\-completion\fP=""
|
||||
write bash completion \fB\fCfile\fR
|
||||
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for generate
|
||||
|
||||
.PP
|
||||
\fB\-\-man\fP=""
|
||||
write man pages to \fB\fCdirectory\fR
|
||||
|
||||
.PP
|
||||
\fB\-\-zsh\-completion\fP=""
|
||||
write zsh completion \fB\fCfile\fR
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
75
doc/man/restic-init.1
Normal file
75
doc/man/restic-init.1
Normal file
@@ -0,0 +1,75 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-init \- Initialize a new repository
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic init [flags]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "init" command initializes a new repository.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for init
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
75
doc/man/restic-key.1
Normal file
75
doc/man/restic-key.1
Normal file
@@ -0,0 +1,75 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-key \- Manage keys (passwords)
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic key [list|add|remove|passwd] [ID] [flags]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "key" command manages keys (passwords) for accessing the repository.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for key
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
75
doc/man/restic-list.1
Normal file
75
doc/man/restic-list.1
Normal file
@@ -0,0 +1,75 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-list \- List objects in the repository
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic list [blobs|packs|index|snapshots|keys|locks] [flags]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "list" command allows listing objects in the repository based on type.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for list
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
94
doc/man/restic-ls.1
Normal file
94
doc/man/restic-ls.1
Normal file
@@ -0,0 +1,94 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-ls \- List files in a snapshot
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic ls [flags] [snapshot\-ID ...]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "ls" command allows listing files and directories in a snapshot.
|
||||
|
||||
.PP
|
||||
The special snapshot\-ID "latest" can be used to list files and directories of the latest snapshot in the repository.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for ls
|
||||
|
||||
.PP
|
||||
\fB\-H\fP, \fB\-\-host\fP=""
|
||||
only consider snapshots for this \fB\fChost\fR, when no snapshot ID is given
|
||||
|
||||
.PP
|
||||
\fB\-l\fP, \fB\-\-long\fP[=false]
|
||||
use a long listing format showing size and mode
|
||||
|
||||
.PP
|
||||
\fB\-\-path\fP=[]
|
||||
only consider snapshots which include this (absolute) \fB\fCpath\fR, when no snapshot ID is given
|
||||
|
||||
.PP
|
||||
\fB\-\-tag\fP=[]
|
||||
only consider snapshots which include this \fB\fCtaglist\fR, when no snapshot ID is given
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
80
doc/man/restic-migrate.1
Normal file
80
doc/man/restic-migrate.1
Normal file
@@ -0,0 +1,80 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-migrate \- Apply migrations
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic migrate [name] [flags]\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "migrate" command applies migrations to a repository. When no migration
|
||||
name is explicitly given, a list of migrations that can be applied is printed.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-f\fP, \fB\-\-force\fP[=false]
|
||||
apply a migration a second time
|
||||
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for migrate
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
100
doc/man/restic-mount.1
Normal file
100
doc/man/restic-mount.1
Normal file
@@ -0,0 +1,100 @@
|
||||
.TH "restic backup" "1" "Jan 2017" "generated by `restic generate`" ""
|
||||
.nh
|
||||
.ad l
|
||||
|
||||
|
||||
.SH NAME
|
||||
.PP
|
||||
restic\-mount \- Mount the repository
|
||||
|
||||
|
||||
.SH SYNOPSIS
|
||||
.PP
|
||||
\fBrestic mount [flags] mountpoint\fP
|
||||
|
||||
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
The "mount" command mounts the repository via fuse to a directory. This is a
|
||||
read\-only mount.
|
||||
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\fB\-\-allow\-other\fP[=false]
|
||||
allow other users to access the data in the mounted directory
|
||||
|
||||
.PP
|
||||
\fB\-\-allow\-root\fP[=false]
|
||||
allow root user to access the data in the mounted directory
|
||||
|
||||
.PP
|
||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||
help for mount
|
||||
|
||||
.PP
|
||||
\fB\-H\fP, \fB\-\-host\fP=""
|
||||
only consider snapshots for this host
|
||||
|
||||
.PP
|
||||
\fB\-\-owner\-root\fP[=false]
|
||||
use 'root' as the owner of files and dirs
|
||||
|
||||
.PP
|
||||
\fB\-\-path\fP=[]
|
||||
only consider snapshots which include this (absolute) \fB\fCpath\fR
|
||||
|
||||
.PP
|
||||
\fB\-\-tag\fP=[]
|
||||
only consider snapshots which include this \fB\fCtaglist\fR
|
||||
|
||||
|
||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||
.PP
|
||||
\fB\-\-cacert\fP=[]
|
||||
path to load root certificates from (default: use system certificates)
|
||||
|
||||
.PP
|
||||
\fB\-\-cache\-dir\fP=""
|
||||
set the cache directory
|
||||
|
||||
.PP
|
||||
\fB\-\-json\fP[=false]
|
||||
set output mode to JSON for commands that support it
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-download\fP=0
|
||||
limits downloads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-limit\-upload\fP=0
|
||||
limits uploads to a maximum rate in KiB/s. (default: unlimited)
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-cache\fP[=false]
|
||||
do not use a local cache
|
||||
|
||||
.PP
|
||||
\fB\-\-no\-lock\fP[=false]
|
||||
do not lock the repo, this allows some operations on read\-only repos
|
||||
|
||||
.PP
|
||||
\fB\-o\fP, \fB\-\-option\fP=[]
|
||||
set extended option (\fB\fCkey=value\fR, can be specified multiple times)
|
||||
|
||||
.PP
|
||||
\fB\-p\fP, \fB\-\-password\-file\fP=""
|
||||
read the repository password from a file (default: $RESTIC\_PASSWORD\_FILE)
|
||||
|
||||
.PP
|
||||
\fB\-q\fP, \fB\-\-quiet\fP[=false]
|
||||
do not output comprehensive progress report
|
||||
|
||||
.PP
|
||||
\fB\-r\fP, \fB\-\-repo\fP=""
|
||||
repository to backup to or restore from (default: $RESTIC\_REPOSITORY)
|
||||
|
||||
|
||||
.SH SEE ALSO
|
||||
.PP
|
||||
\fBrestic(1)\fP
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user