mirror of
https://github.com/restic/restic.git
synced 2026-02-22 16:56:24 +00:00
Compare commits
465 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40832b2927 | ||
|
|
c8a94eced7 | ||
|
|
ee6e981b4e | ||
|
|
96fd982f6a | ||
|
|
6ff0082c02 | ||
|
|
95c1d7d959 | ||
|
|
f003410402 | ||
|
|
655430550b | ||
|
|
1823b8195c | ||
|
|
311ad2d2d0 | ||
|
|
a10b44a265 | ||
|
|
baf3a9aa3b | ||
|
|
ffe6dce7e7 | ||
|
|
8ce0ce387f | ||
|
|
3c44598bf6 | ||
|
|
3bb55fd6bf | ||
|
|
36efefa7bd | ||
|
|
ac4b8c98ac | ||
|
|
4dcd6abf37 | ||
|
|
cb3f531050 | ||
|
|
23fcbb275a | ||
|
|
6e3215a80d | ||
|
|
b10dce541e | ||
|
|
4f221c4022 | ||
|
|
f5c448aa65 | ||
|
|
c0fc85d303 | ||
|
|
0c48e515f0 | ||
|
|
97950ab81a | ||
|
|
59fca85844 | ||
|
|
e207257714 | ||
|
|
82e1cbed4f | ||
|
|
8903b6c88a | ||
|
|
93583c01b1 | ||
|
|
88664ba222 | ||
|
|
121233e1b3 | ||
|
|
8cc9514879 | ||
|
|
2e7d475029 | ||
|
|
d3a286928a | ||
|
|
c46edcd9d6 | ||
|
|
1ede018ea6 | ||
|
|
b77e933d80 | ||
|
|
d0329cf3eb | ||
|
|
dc31529fc3 | ||
|
|
4784540f04 | ||
|
|
84ea2389ae | ||
|
|
b4a7ce86cf | ||
|
|
7ee0964880 | ||
|
|
f6f11400c2 | ||
|
|
e2dc5034d3 | ||
|
|
9a1b3cb5d9 | ||
|
|
b22655367c | ||
|
|
068a3ce23f | ||
|
|
ee05501ce7 | ||
|
|
014600bee6 | ||
|
|
d9a80e07b9 | ||
|
|
d19f05c960 | ||
|
|
460e2ffbf6 | ||
|
|
49b6aac3fa | ||
|
|
2f8335554c | ||
|
|
37113282ca | ||
|
|
337725c354 | ||
|
|
2ddb7ffb7e | ||
|
|
81dcfea11a | ||
|
|
55071ee367 | ||
|
|
dcf9ded977 | ||
|
|
bcf44a9c3f | ||
|
|
a7b4c19abf | ||
|
|
d3fcfeba3a | ||
|
|
e69449bf2c | ||
|
|
da4193c3ef | ||
|
|
fe6445e0f4 | ||
|
|
ea81a0e282 | ||
|
|
80a11960dd | ||
|
|
d6f739ec22 | ||
|
|
b98598e55f | ||
|
|
d5f86effa1 | ||
|
|
c34c731698 | ||
|
|
412623b848 | ||
|
|
bbe8b73f03 | ||
|
|
91e8d998cd | ||
|
|
9a4796594a | ||
|
|
15374d22e9 | ||
|
|
88ad58d6cd | ||
|
|
591a8c4cdf | ||
|
|
ec9a53b7e8 | ||
|
|
34a3adfd8d | ||
|
|
9867c4bbb4 | ||
|
|
efb4a981cf | ||
|
|
2447f3f110 | ||
|
|
b25978a53c | ||
|
|
b0a8c4ad6c | ||
|
|
908b23fda0 | ||
|
|
4508d406ef | ||
|
|
7048cc3e58 | ||
|
|
eb7c00387c | ||
|
|
bc0501d72c | ||
|
|
17995dec7a | ||
|
|
e915cedc3d | ||
|
|
cdcaecd27d | ||
|
|
b43ab67a22 | ||
|
|
7ddfd6cabe | ||
|
|
b1b3f1ecb6 | ||
|
|
fa135f72bf | ||
|
|
51c22f4223 | ||
|
|
1a5b66f33b | ||
|
|
da6a34e044 | ||
|
|
fe69b83074 | ||
|
|
08d24ff99e | ||
|
|
d8b80e9862 | ||
|
|
1c84aceb39 | ||
|
|
575ed9a47e | ||
|
|
8f811642c3 | ||
|
|
f4b9544ab2 | ||
|
|
367449dede | ||
|
|
7042bafea5 | ||
|
|
744a15247d | ||
|
|
3ba19869be | ||
|
|
0fed6a8dfc | ||
|
|
643bbbe156 | ||
|
|
eca0f0ad24 | ||
|
|
08dee8a52b | ||
|
|
b112533812 | ||
|
|
5e63294355 | ||
|
|
84b6f1ec53 | ||
|
|
06fb4ea3f0 | ||
|
|
e38d415173 | ||
|
|
d81a396944 | ||
|
|
0b21ec44b7 | ||
|
|
bd36731119 | ||
|
|
38a2f9c07b | ||
|
|
5af2815627 | ||
|
|
b55de2260d | ||
|
|
9be4fe3e84 | ||
|
|
05116e4787 | ||
|
|
04f79b9642 | ||
|
|
8b358935a0 | ||
|
|
66d089e239 | ||
|
|
49d3efe547 | ||
|
|
9762bec091 | ||
|
|
0eb8553c87 | ||
|
|
d3692f5b81 | ||
|
|
1c0b61204b | ||
|
|
2ee654763b | ||
|
|
b7b479b668 | ||
|
|
4cf9656f12 | ||
|
|
2580eef2aa | ||
|
|
2d7ab9115f | ||
|
|
a178e5628e | ||
|
|
af66a62c04 | ||
|
|
9ea1a78bd4 | ||
|
|
184103647a | ||
|
|
c81b122374 | ||
|
|
48f97f3567 | ||
|
|
3ce9893e0b | ||
|
|
248c7c3828 | ||
|
|
f8316948d1 | ||
|
|
be54ceff66 | ||
|
|
ea97ff1ba4 | ||
|
|
01b9581453 | ||
|
|
3cd927d180 | ||
|
|
bf7b1f12ea | ||
|
|
8554332894 | ||
|
|
3e93b36ca4 | ||
|
|
573a2fb240 | ||
|
|
c847aace35 | ||
|
|
9d1fb94c6c | ||
|
|
020cab8e08 | ||
|
|
07da61baee | ||
|
|
37c95bf5da | ||
|
|
c86d2f23aa | ||
|
|
96ec04d74d | ||
|
|
9c3414374a | ||
|
|
3d530dfc91 | ||
|
|
c43f5b2664 | ||
|
|
38087e40d9 | ||
|
|
bbc960f957 | ||
|
|
309598c237 | ||
|
|
03d23e6faa | ||
|
|
b10acd2af7 | ||
|
|
9175795fdb | ||
|
|
5d8d70542f | ||
|
|
7c23381a2b | ||
|
|
34181b13a2 | ||
|
|
bcd47ec3a2 | ||
|
|
a666a6d576 | ||
|
|
e388d962a5 | ||
|
|
3b7a3711e6 | ||
|
|
9b0e718852 | ||
|
|
82c908871d | ||
|
|
ddf0b8cd0b | ||
|
|
2d0c138c9b | ||
|
|
ef325ffc02 | ||
|
|
0f67ae813a | ||
|
|
7a165f32a9 | ||
|
|
36c69e3ca7 | ||
|
|
35d8413639 | ||
|
|
c66a0e408c | ||
|
|
70f4c014ef | ||
|
|
f0d8710611 | ||
|
|
bd3e280f6d | ||
|
|
2746dcdb5f | ||
|
|
5729d967f5 | ||
|
|
f9f6124558 | ||
|
|
8074879c5f | ||
|
|
7bda28f31f | ||
|
|
255ba83c4b | ||
|
|
7dc200c593 | ||
|
|
9ac90cf5cd | ||
|
|
b84f5177cb | ||
|
|
4cf1c8e8da | ||
|
|
58719e1f47 | ||
|
|
d42c169458 | ||
|
|
8598bb042b | ||
|
|
c6b74962df | ||
|
|
2c72924ffb | ||
|
|
02bec13ef2 | ||
|
|
64976b1a4d | ||
|
|
6a607d6ded | ||
|
|
6fedf1a7f4 | ||
|
|
df946fd9f8 | ||
|
|
4e6a9767de | ||
|
|
1bc80c3c8d | ||
|
|
0fcef2ec23 | ||
|
|
212607dc8a | ||
|
|
190d8e2f51 | ||
|
|
f4cd2a7120 | ||
|
|
aba270df7e | ||
|
|
b5543cff5d | ||
|
|
285b5236c2 | ||
|
|
bb1e258bb7 | ||
|
|
182655bc88 | ||
|
|
74bc7141c1 | ||
|
|
1361341c58 | ||
|
|
ce4a2f4ca6 | ||
|
|
cf979e2b81 | ||
|
|
d92e2c5769 | ||
|
|
7419844885 | ||
|
|
1d66bb9e62 | ||
|
|
0b2c31b05b | ||
|
|
dd7b4f54f5 | ||
|
|
6896c6449b | ||
|
|
735a8074d5 | ||
|
|
70347e95d5 | ||
|
|
0fa3091c78 | ||
|
|
91906911b0 | ||
|
|
fae7f78057 | ||
|
|
ac9ec4b990 | ||
|
|
087c770161 | ||
|
|
6856d1e422 | ||
|
|
8c1261ff02 | ||
|
|
26704be17f | ||
|
|
2c3360db98 | ||
|
|
cba6ad8d8e | ||
|
|
2a3312ac35 | ||
|
|
c35c4e0cbf | ||
|
|
84475aa3a8 | ||
|
|
f12f9ae240 | ||
|
|
5cc1760fdf | ||
|
|
32ac5486e9 | ||
|
|
c4336978eb | ||
|
|
649cbec6c5 | ||
|
|
b17bd7f860 | ||
|
|
68f1e9c524 | ||
|
|
1ee2306033 | ||
|
|
c882a92cd6 | ||
|
|
f54db5d796 | ||
|
|
843e7f404e | ||
|
|
d465b5b9ad | ||
|
|
9f7cd69f13 | ||
|
|
f97a680887 | ||
|
|
42a3db05b0 | ||
|
|
070d43e290 | ||
|
|
d4bd32a37e | ||
|
|
e7d7b85d59 | ||
|
|
be5a0ff59f | ||
|
|
c5100d5632 | ||
|
|
956a1b0f96 | ||
|
|
fab626a3df | ||
|
|
4f00564574 | ||
|
|
f77477129f | ||
|
|
2e31120f89 | ||
|
|
8fb2c0d3c1 | ||
|
|
072cf7b02d | ||
|
|
df66daa5c9 | ||
|
|
9790d8ce1c | ||
|
|
16710454f4 | ||
|
|
08ec6c9f17 | ||
|
|
7910ff4c0e | ||
|
|
c4da9d1e90 | ||
|
|
3ed61987a2 | ||
|
|
9dba7a2577 | ||
|
|
a1352906e2 | ||
|
|
b7c0d4d8bf | ||
|
|
f033850aa0 | ||
|
|
bdf7ba20cb | ||
|
|
3ee6b8ec63 | ||
|
|
4a400f94bb | ||
|
|
1a1c572bac | ||
|
|
5a7c27ddb6 | ||
|
|
fb842759fc | ||
|
|
7aa2f8a61e | ||
|
|
08bf3bae79 | ||
|
|
e7b741b2d7 | ||
|
|
6bee62e346 | ||
|
|
bc74cd3ae5 | ||
|
|
90243ed1c4 | ||
|
|
0ce81d88b6 | ||
|
|
c03bc88b29 | ||
|
|
b8da7b1f4d | ||
|
|
f1b4d97945 | ||
|
|
90fc639a67 | ||
|
|
2b5a6d255a | ||
|
|
f004dbe605 | ||
|
|
9efbe98879 | ||
|
|
7d9300efca | ||
|
|
c38aaaa768 | ||
|
|
8941041355 | ||
|
|
18fee4806f | ||
|
|
47d4d5bf1b | ||
|
|
74a64c47e4 | ||
|
|
a23e9c86ba | ||
|
|
4de12bf593 | ||
|
|
c542a509f0 | ||
|
|
8cf3bb8737 | ||
|
|
b46cc6d57e | ||
|
|
4a2156d3f0 | ||
|
|
41fee11f66 | ||
|
|
b592614061 | ||
|
|
b7c3039eb2 | ||
|
|
a307797c11 | ||
|
|
a03f107144 | ||
|
|
a141ab1bda | ||
|
|
52abec967f | ||
|
|
2828a9c2b0 | ||
|
|
cec7d581f3 | ||
|
|
12aa1e61da | ||
|
|
95da6c1c1d | ||
|
|
0c03a80fc4 | ||
|
|
b1c77172c2 | ||
|
|
c0373cd307 | ||
|
|
28121090c2 | ||
|
|
44a57d66c3 | ||
|
|
266f9dbe16 | ||
|
|
60e4a88f17 | ||
|
|
c50f91b7f9 | ||
|
|
e851d29565 | ||
|
|
b67b7ebfe6 | ||
|
|
751eba0e68 | ||
|
|
7447c44484 | ||
|
|
c8a672fa29 | ||
|
|
863ba76494 | ||
|
|
8526cc6647 | ||
|
|
694b7a17e7 | ||
|
|
5cd0bce452 | ||
|
|
58bd165253 | ||
|
|
65d3fb6b33 | ||
|
|
f165048172 | ||
|
|
de5516a90e | ||
|
|
4f6fd9fb98 | ||
|
|
9a9101d144 | ||
|
|
616f9499ae | ||
|
|
a40ac37550 | ||
|
|
99fd80a585 | ||
|
|
2464f7c4d1 | ||
|
|
b5c7778428 | ||
|
|
c52198d12c | ||
|
|
f17ffa0283 | ||
|
|
5e2afd91e7 | ||
|
|
79b882e901 | ||
|
|
1b502fa9ef | ||
|
|
e1969d1e33 | ||
|
|
6ac6bca7a1 | ||
|
|
3a6feb0596 | ||
|
|
2f8aa2ce30 | ||
|
|
f2bf06a419 | ||
|
|
71900248ab | ||
|
|
23055aaadf | ||
|
|
8bf6a3af97 | ||
|
|
27d6e5e186 | ||
|
|
4214b1746e | ||
|
|
f6f240573a | ||
|
|
ecdf49679e | ||
|
|
e1f722d266 | ||
|
|
2d47381b1d | ||
|
|
294ca55967 | ||
|
|
78c518ccac | ||
|
|
42a3292bcf | ||
|
|
ef70a2fcb3 | ||
|
|
af20015725 | ||
|
|
680a14afa1 | ||
|
|
bf199e5ca7 | ||
|
|
ae127a412d | ||
|
|
5ae7e6f945 | ||
|
|
d8da9c4401 | ||
|
|
daf3bfc882 | ||
|
|
94f4f13388 | ||
|
|
4615bdfd70 | ||
|
|
299f5971f2 | ||
|
|
7a1352ae87 | ||
|
|
72734d59b5 | ||
|
|
3679669aae | ||
|
|
3ed54e762e | ||
|
|
fea835b4e2 | ||
|
|
16b321b140 | ||
|
|
b58dfda212 | ||
|
|
b229aa5cf1 | ||
|
|
81c0b031f9 | ||
|
|
760863e7f9 | ||
|
|
a135699397 | ||
|
|
94a4d45dfb | ||
|
|
4de384087d | ||
|
|
57815d9cd6 | ||
|
|
644673bcf2 | ||
|
|
e7cdf2acbb | ||
|
|
0f22f008ed | ||
|
|
c184da21d0 | ||
|
|
ef5efc6a82 | ||
|
|
688014487b | ||
|
|
38ddfbc4d3 | ||
|
|
da48b925ff | ||
|
|
4bcd56fbae | ||
|
|
6e9778ae0e | ||
|
|
63c67be908 | ||
|
|
d70a4a9350 | ||
|
|
2cd9c7ef16 | ||
|
|
6ec5dc8016 | ||
|
|
fe430a680a | ||
|
|
69a0d0ee90 | ||
|
|
4557881066 | ||
|
|
9b5d069ade | ||
|
|
c56cbfe95b | ||
|
|
97e5ce4344 | ||
|
|
72bd467e23 | ||
|
|
d818b7618b | ||
|
|
1c3812a6f6 | ||
|
|
ec91b80f09 | ||
|
|
18a336c154 | ||
|
|
d21a13f1ee | ||
|
|
e0eac30ee5 | ||
|
|
fccb579471 | ||
|
|
6c700f95b5 | ||
|
|
95d070c147 | ||
|
|
da4473aba6 | ||
|
|
6e85a58045 | ||
|
|
476e2e8762 | ||
|
|
246bb46e82 | ||
|
|
fe0361ffcb | ||
|
|
952469473f | ||
|
|
02108f202e | ||
|
|
b02357f542 | ||
|
|
f2aeaef8f1 | ||
|
|
547702d285 | ||
|
|
599831f874 | ||
|
|
14c90d9e85 | ||
|
|
c41bbb3761 | ||
|
|
8a54a0f5ac | ||
|
|
c0a5530950 | ||
|
|
77ef92b95c | ||
|
|
69f75bbf70 | ||
|
|
30519f01ff | ||
|
|
7cacba0394 | ||
|
|
1596d06f8e | ||
|
|
db20c0b8d0 | ||
|
|
3ca306d69a | ||
|
|
5d272e5c08 |
27
.github/ISSUE_TEMPLATE.md
vendored
27
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,27 +0,0 @@
|
||||
<!--
|
||||
|
||||
Welcome! If you have a question or are unsure if you should open an issue,
|
||||
please use the forum instead!
|
||||
|
||||
https://forum.restic.net
|
||||
|
||||
The forum is a better place for questions about restic or general suggestions
|
||||
and topics, e.g. usage or documentation questions! This issue tracker is mainly
|
||||
for tracking bugs and feature requests directly relating to the development of
|
||||
the software itself, rather than the project.
|
||||
|
||||
Thanks for understanding, and for contributing to the project!
|
||||
-->
|
||||
|
||||
|
||||
Output of `restic version`
|
||||
--------------------------
|
||||
|
||||
<!--
|
||||
Please add the version of restic you're currently using here, this helps us
|
||||
later to see what has changed in restic when we revisit this issue after some
|
||||
time.
|
||||
-->
|
||||
|
||||
Describe the issue
|
||||
------------------
|
||||
4
.github/ISSUE_TEMPLATE/Bug.md
vendored
4
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -83,8 +83,8 @@ Do you have an idea how to solve the issue?
|
||||
|
||||
|
||||
|
||||
Did restic help you or made you happy in any way?
|
||||
-------------------------------------------------
|
||||
Did restic help you today? Did it make you happy in any way?
|
||||
------------------------------------------------------------
|
||||
|
||||
<!--
|
||||
Answering this question is not required, but if you have anything positive to share, please do so here!
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/Feature.md
vendored
8
.github/ISSUE_TEMPLATE/Feature.md
vendored
@@ -39,16 +39,16 @@ Please describe the feature you'd like us to add here.
|
||||
-->
|
||||
|
||||
|
||||
What are you trying to do?
|
||||
--------------------------
|
||||
What are you trying to do? What problem would this solve?
|
||||
---------------------------------------------------------
|
||||
|
||||
<!--
|
||||
This section should contain a brief description what you're trying to do, which
|
||||
would be possible after implementing the new feature.
|
||||
-->
|
||||
|
||||
Did restic help you or made you happy in any way?
|
||||
-------------------------------------------------
|
||||
Did restic help you today? Did it make you happy in any way?
|
||||
------------------------------------------------------------
|
||||
|
||||
<!--
|
||||
Answering this question is not required, but if you have anything positive to share, please do so here!
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: restic forum
|
||||
url: https://forum.restic.net
|
||||
about: Please ask questions about using restic here, do not open an issue for questions.
|
||||
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -10,11 +10,11 @@ your time and add more commits. If you're done and ready for review, please
|
||||
check the last box.
|
||||
-->
|
||||
|
||||
What is the purpose of this change? What does it change?
|
||||
--------------------------------------------------------
|
||||
What does this PR change? What problem does it solve?
|
||||
-----------------------------------------------------
|
||||
|
||||
<!--
|
||||
Describe the changes here, as detailed as needed.
|
||||
Describe the changes and their purpose here, as detailed as needed.
|
||||
-->
|
||||
|
||||
Was the change discussed in an issue or in the forum before?
|
||||
@@ -23,14 +23,15 @@ Was the change discussed in an issue or in the forum before?
|
||||
<!--
|
||||
Link issues and relevant forum posts here.
|
||||
|
||||
If this PR resolves an issue on GitHub, use "closes #1234" so that the issue is
|
||||
closed automatically when this PR is merged.
|
||||
If this PR resolves an issue on GitHub, use "Closes #1234" so that the issue
|
||||
is closed automatically when this PR is merged.
|
||||
-->
|
||||
|
||||
Checklist
|
||||
---------
|
||||
|
||||
- [ ] I have read the [Contribution Guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches)
|
||||
- [ ] I have enabled [maintainer edits for this PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)
|
||||
- [ ] I have added tests for all changes in this PR
|
||||
- [ ] I have added documentation for the changes (in the manual)
|
||||
- [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (template [here](https://github.com/restic/restic/blob/master/changelog/TEMPLATE))
|
||||
|
||||
16
.travis.yml
16
.travis.yml
@@ -4,7 +4,7 @@ sudo: false
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
go: "1.10.x"
|
||||
go: "1.13.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
@@ -12,15 +12,7 @@ matrix:
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
- os: linux
|
||||
go: "1.11.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
- os: linux
|
||||
go: "1.12.x"
|
||||
go: "1.14.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
@@ -29,7 +21,7 @@ matrix:
|
||||
|
||||
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
|
||||
- os: linux
|
||||
go: "1.13.x"
|
||||
go: "1.15.x"
|
||||
sudo: true
|
||||
cache:
|
||||
directories:
|
||||
@@ -37,7 +29,7 @@ matrix:
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
- os: osx
|
||||
go: "1.13.x"
|
||||
go: "1.15.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
|
||||
504
CHANGELOG.md
504
CHANGELOG.md
@@ -1,3 +1,467 @@
|
||||
Changelog for restic 0.10.0 (2020-09-19)
|
||||
=======================================
|
||||
|
||||
The following sections list the changes in restic 0.10.0 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
* Fix #1863: Report correct number of directories processed by backup
|
||||
* Fix #2254: Fix tar issues when dumping `/`
|
||||
* Fix #2281: Handle format verbs like '%' properly in `find` output
|
||||
* Fix #2298: Do not hang when run as a background job
|
||||
* Fix #2389: Fix mangled json output of backup command
|
||||
* Fix #2390: Refresh lock timestamp
|
||||
* Fix #2429: Backup --json reports total_bytes_processed as 0
|
||||
* Fix #2469: Fix incorrect bytes stats in `diff` command
|
||||
* Fix #2518: Do not crash with Synology NAS sftp server
|
||||
* Fix #2531: Fix incorrect size calculation in `stats --mode restore-size`
|
||||
* Fix #2537: Fix incorrect file counts in `stats --mode restore-size`
|
||||
* Fix #2592: SFTP backend supports IPv6 addresses
|
||||
* Fix #2607: Honor RESTIC_CACHE_DIR environment variable on Mac and Windows
|
||||
* Fix #2668: Don't abort the stats command when data blobs are missing
|
||||
* Fix #2674: Add stricter prune error checks
|
||||
* Fix #2899: Fix possible crash in the progress bar of check --read-data
|
||||
* Chg #2482: Remove vendored dependencies
|
||||
* Chg #2546: Return exit code 3 when failing to backup all source data
|
||||
* Chg #2600: Update dependencies, require Go >= 1.13
|
||||
* Chg #1597: Honor the --no-lock flag in the mount command
|
||||
* Enh #1570: Support specifying multiple host flags for various commands
|
||||
* Enh #1680: Optimize `restic mount`
|
||||
* Enh #2072: Display snapshot date when using `restic find`
|
||||
* Enh #2175: Allow specifying user and host when creating keys
|
||||
* Enh #2277: Add support for ppc64le
|
||||
* Enh #2395: Ignore sync errors when operation not supported by local filesystem
|
||||
* Enh #2427: Add flag `--iexclude-file` to backup command
|
||||
* Enh #2569: Support excluding files by their size
|
||||
* Enh #2571: Self-heal missing file parts during backup of unchanged files
|
||||
* Enh #2858: Support filtering snapshots by tag and path in the stats command
|
||||
* Enh #323: Add command for copying snapshots between repositories
|
||||
* Enh #551: Use optimized library for hash calculation of file chunks
|
||||
* Enh #2195: Simplify and improve restore performance
|
||||
* Enh #2328: Improve speed of check command
|
||||
* Enh #2423: Support user@domain parsing as user
|
||||
* Enh #2576: Improve the chunking algorithm
|
||||
* Enh #2598: Improve speed of diff command
|
||||
* Enh #2599: Slightly reduce memory usage of prune and stats commands
|
||||
* Enh #2733: S3 backend: Add support for WebIdentityTokenFile
|
||||
* Enh #2773: Optimize handling of new index entries
|
||||
* Enh #2781: Reduce memory consumption of in-memory index
|
||||
* Enh #2786: Optimize `list blobs` command
|
||||
* Enh #2790: Optimized file access in restic mount
|
||||
* Enh #2840: Speed-up file deletion in forget, prune and rebuild-index
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
* Bugfix #1863: Report correct number of directories processed by backup
|
||||
|
||||
The directory statistics calculation was fixed to report the actual number of processed
|
||||
directories instead of always zero.
|
||||
|
||||
https://github.com/restic/restic/issues/1863
|
||||
|
||||
* Bugfix #2254: Fix tar issues when dumping `/`
|
||||
|
||||
We've fixed an issue with dumping either `/` or files on the first sublevel e.g. `/foo` to tar.
|
||||
This also fixes tar dumping issues on Windows where this issue could also happen.
|
||||
|
||||
https://github.com/restic/restic/issues/2254
|
||||
https://github.com/restic/restic/issues/2357
|
||||
https://github.com/restic/restic/pull/2255
|
||||
|
||||
* Bugfix #2281: Handle format verbs like '%' properly in `find` output
|
||||
|
||||
The JSON or "normal" output of the `find` command can now deal with file names that contain
|
||||
substrings which the Golang `fmt` package considers "format verbs" like `%s`.
|
||||
|
||||
https://github.com/restic/restic/issues/2281
|
||||
|
||||
* Bugfix #2298: Do not hang when run as a background job
|
||||
|
||||
Restic did hang on exit while restoring the terminal configuration when it was started as a
|
||||
background job, for example using `restic ... &`. This has been fixed by only restoring the
|
||||
terminal configuration when restic is interrupted while reading a password from the
|
||||
terminal.
|
||||
|
||||
https://github.com/restic/restic/issues/2298
|
||||
|
||||
* Bugfix #2389: Fix mangled json output of backup command
|
||||
|
||||
We've fixed a race condition in the json output of the backup command that could cause multiple
|
||||
lines to get mixed up. We've also ensured that the backup summary is printed last.
|
||||
|
||||
https://github.com/restic/restic/issues/2389
|
||||
https://github.com/restic/restic/pull/2545
|
||||
|
||||
* Bugfix #2390: Refresh lock timestamp
|
||||
|
||||
Long-running operations did not refresh lock timestamp, resulting in locks becoming stale.
|
||||
This is now fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/2390
|
||||
|
||||
* Bugfix #2429: Backup --json reports total_bytes_processed as 0
|
||||
|
||||
We've fixed the json output of total_bytes_processed. The non-json output was already fixed
|
||||
with pull request #2138 but left the json output untouched.
|
||||
|
||||
https://github.com/restic/restic/issues/2429
|
||||
|
||||
* Bugfix #2469: Fix incorrect bytes stats in `diff` command
|
||||
|
||||
In some cases, the wrong number of bytes (e.g. 16777215.998 TiB) were reported by the `diff`
|
||||
command. This is now fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/2469
|
||||
|
||||
* Bugfix #2518: Do not crash with Synology NAS sftp server
|
||||
|
||||
It was found that when restic is used to store data on an sftp server on a Synology NAS with a
|
||||
relative path (one which does not start with a slash), it may go into an endless loop trying to
|
||||
create directories on the server. We've fixed this bug by using a function in the sftp library
|
||||
instead of our own implementation.
|
||||
|
||||
The bug was discovered because the Synology sftp server behaves erratic with non-absolute
|
||||
path (e.g. `home/restic-repo`). This can be resolved by just using an absolute path instead
|
||||
(`/home/restic-repo`). We've also added a paragraph in the FAQ.
|
||||
|
||||
https://github.com/restic/restic/issues/2518
|
||||
https://github.com/restic/restic/issues/2363
|
||||
https://github.com/restic/restic/pull/2530
|
||||
|
||||
* Bugfix #2531: Fix incorrect size calculation in `stats --mode restore-size`
|
||||
|
||||
The restore-size mode of stats was counting hard-linked files as if they were independent.
|
||||
|
||||
https://github.com/restic/restic/issues/2531
|
||||
|
||||
* Bugfix #2537: Fix incorrect file counts in `stats --mode restore-size`
|
||||
|
||||
The restore-size mode of stats was failing to count empty directories and some files with hard
|
||||
links.
|
||||
|
||||
https://github.com/restic/restic/issues/2537
|
||||
|
||||
* Bugfix #2592: SFTP backend supports IPv6 addresses
|
||||
|
||||
The SFTP backend now supports IPv6 addresses natively, without relying on aliases in the
|
||||
external SSH configuration.
|
||||
|
||||
https://github.com/restic/restic/pull/2592
|
||||
|
||||
* Bugfix #2607: Honor RESTIC_CACHE_DIR environment variable on Mac and Windows
|
||||
|
||||
On Mac and Windows, the RESTIC_CACHE_DIR environment variable was ignored. This variable can
|
||||
now be used on all platforms to set the directory where restic stores caches.
|
||||
|
||||
https://github.com/restic/restic/pull/2607
|
||||
|
||||
* Bugfix #2668: Don't abort the stats command when data blobs are missing
|
||||
|
||||
Runing the stats command in the blobs-per-file mode on a repository with missing data blobs
|
||||
previously resulted in a crash.
|
||||
|
||||
https://github.com/restic/restic/pull/2668
|
||||
|
||||
* Bugfix #2674: Add stricter prune error checks
|
||||
|
||||
Additional checks were added to the prune command in order to improve resiliency to backend,
|
||||
hardware and/or networking issues. The checks now detect a few more cases where such outside
|
||||
factors could potentially cause data loss.
|
||||
|
||||
https://github.com/restic/restic/pull/2674
|
||||
|
||||
* Bugfix #2899: Fix possible crash in the progress bar of check --read-data
|
||||
|
||||
We've fixed a possible crash while displaying the progress bar for the check --read-data
|
||||
command. The crash occurred when the length of the progress bar status exceeded the terminal
|
||||
width, which only happened for very narrow terminal windows.
|
||||
|
||||
https://github.com/restic/restic/pull/2899
|
||||
https://forum.restic.net/t/restic-rclone-pcloud-connection-issues/2963/15
|
||||
|
||||
* Change #2482: Remove vendored dependencies
|
||||
|
||||
We've removed the vendored dependencies (in the subdir `vendor/`). When building restic, the
|
||||
Go compiler automatically fetches the dependencies. It will also cryptographically verify
|
||||
that the correct code has been fetched by using the hashes in `go.sum` (see the link to the
|
||||
documentation below).
|
||||
|
||||
https://github.com/restic/restic/issues/2482
|
||||
https://golang.org/cmd/go/#hdr-Module_downloading_and_verification
|
||||
|
||||
* Change #2546: Return exit code 3 when failing to backup all source data
|
||||
|
||||
The backup command used to return a zero exit code as long as a snapshot could be created
|
||||
successfully, even if some of the source files could not be read (in which case the snapshot
|
||||
would contain the rest of the files).
|
||||
|
||||
This made it hard for automation/scripts to detect failures/incomplete backups by looking at
|
||||
the exit code. Restic now returns the following exit codes for the backup command:
|
||||
|
||||
- 0 when the command was successful - 1 when there was a fatal error (no snapshot created) - 3 when
|
||||
some source data could not be read (incomplete snapshot created)
|
||||
|
||||
https://github.com/restic/restic/issues/956
|
||||
https://github.com/restic/restic/issues/2064
|
||||
https://github.com/restic/restic/issues/2526
|
||||
https://github.com/restic/restic/issues/2364
|
||||
https://github.com/restic/restic/pull/2546
|
||||
|
||||
* Change #2600: Update dependencies, require Go >= 1.13
|
||||
|
||||
Restic now requires Go to be at least 1.13. This allows simplifications in the build process and
|
||||
removing workarounds.
|
||||
|
||||
This is also probably the last version of restic still supporting mounting repositories via
|
||||
fuse on macOS. The library we're using for fuse does not support macOS any more and osxfuse is not
|
||||
open source any more.
|
||||
|
||||
https://github.com/bazil/fuse/issues/224
|
||||
https://github.com/osxfuse/osxfuse/issues/590
|
||||
https://github.com/restic/restic/pull/2600
|
||||
https://github.com/restic/restic/pull/2852
|
||||
https://github.com/restic/restic/pull/2927
|
||||
|
||||
* Change #1597: Honor the --no-lock flag in the mount command
|
||||
|
||||
The mount command now does not lock the repository if given the --no-lock flag. This allows to
|
||||
mount repositories which are archived on a read only backend/filesystem.
|
||||
|
||||
https://github.com/restic/restic/issues/1597
|
||||
https://github.com/restic/restic/pull/2821
|
||||
|
||||
* Enhancement #1570: Support specifying multiple host flags for various commands
|
||||
|
||||
Previously commands didn't take more than one `--host` or `-H` argument into account, which
|
||||
could be limiting with e.g. the `forget` command.
|
||||
|
||||
The `dump`, `find`, `forget`, `ls`, `mount`, `restore`, `snapshots`, `stats` and `tag`
|
||||
commands will now take into account multiple `--host` and `-H` flags.
|
||||
|
||||
https://github.com/restic/restic/issues/1570
|
||||
|
||||
* Enhancement #1680: Optimize `restic mount`
|
||||
|
||||
We've optimized the FUSE implementation used within restic. `restic mount` is now more
|
||||
responsive and uses less memory.
|
||||
|
||||
https://github.com/restic/restic/issues/1680
|
||||
https://github.com/restic/restic/pull/2587
|
||||
https://github.com/restic/restic/pull/2787
|
||||
|
||||
* Enhancement #2072: Display snapshot date when using `restic find`
|
||||
|
||||
Added the respective snapshot date to the output of `restic find`.
|
||||
|
||||
https://github.com/restic/restic/issues/2072
|
||||
|
||||
* Enhancement #2175: Allow specifying user and host when creating keys
|
||||
|
||||
When adding a new key to the repository, the username and hostname for the new key can be
|
||||
specified on the command line. This allows overriding the defaults, for example if you would
|
||||
prefer to use the FQDN to identify the host or if you want to add keys for several different hosts
|
||||
without having to run the key add command on those hosts.
|
||||
|
||||
https://github.com/restic/restic/issues/2175
|
||||
|
||||
* Enhancement #2277: Add support for ppc64le
|
||||
|
||||
Adds support for ppc64le, the processor architecture from IBM.
|
||||
|
||||
https://github.com/restic/restic/issues/2277
|
||||
|
||||
* Enhancement #2395: Ignore sync errors when operation not supported by local filesystem
|
||||
|
||||
The local backend has been modified to work with filesystems which doesn't support the `sync`
|
||||
operation. This operation is normally used by restic to ensure that data files are fully
|
||||
written to disk before continuing.
|
||||
|
||||
For these limited filesystems, saving a file in the backend would previously fail with an
|
||||
"operation not supported" error. This error is now ignored, which means that e.g. an SMB mount
|
||||
on macOS can now be used as storage location for a repository.
|
||||
|
||||
https://github.com/restic/restic/issues/2395
|
||||
https://forum.restic.net/t/sync-errors-on-mac-over-smb/1859
|
||||
|
||||
* Enhancement #2427: Add flag `--iexclude-file` to backup command
|
||||
|
||||
The backup command now supports the flag `--iexclude-file` which is a case-insensitive
|
||||
version of `--exclude-file`.
|
||||
|
||||
https://github.com/restic/restic/issues/2427
|
||||
https://github.com/restic/restic/pull/2898
|
||||
|
||||
* Enhancement #2569: Support excluding files by their size
|
||||
|
||||
The `backup` command now supports the `--exclude-larger-than` option to exclude files which
|
||||
are larger than the specified maximum size. This can for example be useful to exclude
|
||||
unimportant files with a large file size.
|
||||
|
||||
https://github.com/restic/restic/issues/2569
|
||||
https://github.com/restic/restic/pull/2914
|
||||
|
||||
* Enhancement #2571: Self-heal missing file parts during backup of unchanged files
|
||||
|
||||
We've improved the resilience of restic to certain types of repository corruption.
|
||||
|
||||
For files that are unchanged since the parent snapshot, the backup command now verifies that
|
||||
all parts of the files still exist in the repository. Parts that are missing, e.g. from a damaged
|
||||
repository, are backed up again. This verification was already run for files that were
|
||||
modified since the parent snapshot, but is now also done for unchanged files.
|
||||
|
||||
Note that restic will not backup file parts that are referenced in the index but where the actual
|
||||
data is not present on disk, as this situation can only be detected by restic check. Please
|
||||
ensure that you run `restic check` regularly.
|
||||
|
||||
https://github.com/restic/restic/issues/2571
|
||||
https://github.com/restic/restic/pull/2827
|
||||
|
||||
* Enhancement #2858: Support filtering snapshots by tag and path in the stats command
|
||||
|
||||
We've added filtering snapshots by `--tag tagList` and by `--path path` to the `stats`
|
||||
command. This includes filtering of only 'latest' snapshots or all snapshots in a repository.
|
||||
|
||||
https://github.com/restic/restic/issues/2858
|
||||
https://github.com/restic/restic/pull/2859
|
||||
https://forum.restic.net/t/stats-for-a-host-and-filtered-snapshots/3020
|
||||
|
||||
* Enhancement #323: Add command for copying snapshots between repositories
|
||||
|
||||
We've added a copy command, allowing you to copy snapshots from one repository to another.
|
||||
|
||||
Note that this process will have to read (download) and write (upload) the entire snapshot(s)
|
||||
due to the different encryption keys used on the source and destination repository. Also, the
|
||||
transferred files are not re-chunked, which may break deduplication between files already
|
||||
stored in the destination repo and files copied there using this command.
|
||||
|
||||
To fully support deduplication between repositories when the copy command is used, the init
|
||||
command now supports the `--copy-chunker-params` option, which initializes the new
|
||||
repository with identical parameters for splitting files into chunks as an already existing
|
||||
repository. This allows copied snapshots to be equally deduplicated in both repositories.
|
||||
|
||||
https://github.com/restic/restic/issues/323
|
||||
https://github.com/restic/restic/pull/2606
|
||||
https://github.com/restic/restic/pull/2928
|
||||
|
||||
* Enhancement #551: Use optimized library for hash calculation of file chunks
|
||||
|
||||
We've switched the library used to calculate the hashes of file chunks, which are used for
|
||||
deduplication, to the optimized Minio SHA-256 implementation.
|
||||
|
||||
Depending on the CPU it improves the hashing throughput by 10-30%. Modern x86 CPUs with the SHA
|
||||
Extension should be about two to three times faster.
|
||||
|
||||
https://github.com/restic/restic/issues/551
|
||||
https://github.com/restic/restic/pull/2709
|
||||
|
||||
* Enhancement #2195: Simplify and improve restore performance
|
||||
|
||||
Significantly improves restore performance of large files (i.e. 50M+):
|
||||
https://github.com/restic/restic/issues/2074
|
||||
https://forum.restic.net/t/restore-using-rclone-gdrive-backend-is-slow/1112/8
|
||||
https://forum.restic.net/t/degraded-restore-performance-s3-backend/1400
|
||||
|
||||
Fixes "not enough cache capacity" error during restore:
|
||||
https://github.com/restic/restic/issues/2244
|
||||
|
||||
NOTE: This new implementation does not guarantee order in which blobs are written to the target
|
||||
files and, for example, the last blob of a file can be written to the file before any of the
|
||||
preceeding file blobs. It is therefore possible to have gaps in the data written to the target
|
||||
files if restore fails or interrupted by the user.
|
||||
|
||||
The implementation will try to preallocate space for the restored files on the filesystem to
|
||||
prevent file fragmentation. This ensures good read performance for large files, like for
|
||||
example VM images. If preallocating space is not supported by the filesystem, then this step is
|
||||
silently skipped.
|
||||
|
||||
https://github.com/restic/restic/pull/2195
|
||||
https://github.com/restic/restic/pull/2893
|
||||
|
||||
* Enhancement #2328: Improve speed of check command
|
||||
|
||||
We've improved the check command to traverse trees only once independent of whether they are
|
||||
contained in multiple snapshots. The check command is now much faster for repositories with a
|
||||
large number of snapshots.
|
||||
|
||||
https://github.com/restic/restic/issues/2284
|
||||
https://github.com/restic/restic/pull/2328
|
||||
|
||||
* Enhancement #2423: Support user@domain parsing as user
|
||||
|
||||
Added the ability for user@domain-like users to be authenticated over SFTP servers.
|
||||
|
||||
https://github.com/restic/restic/pull/2423
|
||||
|
||||
* Enhancement #2576: Improve the chunking algorithm
|
||||
|
||||
We've updated the chunker library responsible for splitting files into smaller blocks. It
|
||||
should improve the chunking throughput by 5-15% depending on the CPU.
|
||||
|
||||
https://github.com/restic/restic/issues/2820
|
||||
https://github.com/restic/restic/pull/2576
|
||||
https://github.com/restic/restic/pull/2845
|
||||
|
||||
* Enhancement #2598: Improve speed of diff command
|
||||
|
||||
We've improved the performance of the diff command when comparing snapshots with similar
|
||||
content. It should run up to twice as fast as before.
|
||||
|
||||
https://github.com/restic/restic/pull/2598
|
||||
|
||||
* Enhancement #2599: Slightly reduce memory usage of prune and stats commands
|
||||
|
||||
The prune and the stats command kept directory identifiers in memory twice while searching for
|
||||
used blobs.
|
||||
|
||||
https://github.com/restic/restic/pull/2599
|
||||
|
||||
* Enhancement #2733: S3 backend: Add support for WebIdentityTokenFile
|
||||
|
||||
We've added support for EKS IAM roles for service accounts feature to the S3 backend.
|
||||
|
||||
https://github.com/restic/restic/issues/2703
|
||||
https://github.com/restic/restic/pull/2733
|
||||
|
||||
* Enhancement #2773: Optimize handling of new index entries
|
||||
|
||||
Restic now uses less memory for backups which add a lot of data, e.g. large initial backups. In
|
||||
addition, we've improved the stability in some edge cases.
|
||||
|
||||
https://github.com/restic/restic/pull/2773
|
||||
|
||||
* Enhancement #2781: Reduce memory consumption of in-memory index
|
||||
|
||||
We've improved how the index is stored in memory. This change can reduce memory usage for large
|
||||
repositories by up to 50% (depending on the operation).
|
||||
|
||||
https://github.com/restic/restic/pull/2781
|
||||
https://github.com/restic/restic/pull/2812
|
||||
|
||||
* Enhancement #2786: Optimize `list blobs` command
|
||||
|
||||
We've changed the implementation of `list blobs` which should be now a bit faster and consume
|
||||
almost no memory even for large repositories.
|
||||
|
||||
https://github.com/restic/restic/pull/2786
|
||||
|
||||
* Enhancement #2790: Optimized file access in restic mount
|
||||
|
||||
Reading large (> 100GiB) files from restic mountpoints is now faster, and the speedup is
|
||||
greater for larger files.
|
||||
|
||||
https://github.com/restic/restic/pull/2790
|
||||
|
||||
* Enhancement #2840: Speed-up file deletion in forget, prune and rebuild-index
|
||||
|
||||
We've sped up the file deletion for the commands forget, prune and rebuild-index, especially
|
||||
for remote repositories. Deletion was sequential before and is now run in parallel.
|
||||
|
||||
https://github.com/restic/restic/pull/2840
|
||||
|
||||
|
||||
Changelog for restic 0.9.6 (2019-11-22)
|
||||
=======================================
|
||||
|
||||
@@ -12,6 +476,7 @@ Summary
|
||||
* Fix #2249: Read fresh metadata for unmodified files
|
||||
* Fix #2301: Add upper bound for t in --read-data-subset=n/t
|
||||
* Fix #2321: Check errors when loading index files
|
||||
* Enh #2179: Use ctime when checking for file changes
|
||||
* Enh #2306: Allow multiple retries for interactive password input
|
||||
* Enh #2330: Make `--group-by` accept both singular and plural
|
||||
* Enh #2350: Add option to configure S3 region
|
||||
@@ -60,6 +525,21 @@ Details
|
||||
https://github.com/restic/restic/pull/2321
|
||||
https://forum.restic.net/t/check-rebuild-index-prune/1848/13
|
||||
|
||||
* Enhancement #2179: Use ctime when checking for file changes
|
||||
|
||||
Previously, restic only checked a file's mtime (along with other non-timestamp metadata) to
|
||||
decide if a file has changed. This could cause restic to not notice that a file has changed (and
|
||||
therefore continue to store the old version, as opposed to the modified version) if something
|
||||
edits the file and then resets the timestamp. Restic now also checks the ctime of files, so any
|
||||
modifications to a file should be noticed, and the modified file will be backed up. The ctime
|
||||
check will be disabled if the --ignore-inode flag was given.
|
||||
|
||||
If this change causes problems for you, please open an issue, and we can look in to adding a
|
||||
seperate flag to disable just the ctime check.
|
||||
|
||||
https://github.com/restic/restic/issues/2179
|
||||
https://github.com/restic/restic/pull/2212
|
||||
|
||||
* Enhancement #2306: Allow multiple retries for interactive password input
|
||||
|
||||
Restic used to quit if the repository password was typed incorrectly once. Restic will now ask
|
||||
@@ -101,7 +581,6 @@ Summary
|
||||
* Enh #1895: Add case insensitive include & exclude options
|
||||
* Enh #1937: Support streaming JSON output for backup
|
||||
* Enh #2155: Add Openstack application credential auth for Swift
|
||||
* Enh #2179: Use ctime when checking for file changes
|
||||
* Enh #2184: Add --json support to forget command
|
||||
* Enh #2037: Add group-by option to snapshots command
|
||||
* Enh #2124: Ability to dump folders to tar via stdout
|
||||
@@ -167,21 +646,6 @@ Details
|
||||
|
||||
https://github.com/restic/restic/issues/2155
|
||||
|
||||
* Enhancement #2179: Use ctime when checking for file changes
|
||||
|
||||
Previously, restic only checked a file's mtime (along with other non-timestamp metadata) to
|
||||
decide if a file has changed. This could cause restic to not notice that a file has changed (and
|
||||
therefore continue to store the old version, as opposed to the modified version) if something
|
||||
edits the file and then resets the timestamp. Restic now also checks the ctime of files, so any
|
||||
modifications to a file should be noticed, and the modified file will be backed up. The ctime
|
||||
check will be disabled if the --ignore-inode flag was given.
|
||||
|
||||
If this change causes problems for you, please open an issue, and we can look in to adding a
|
||||
seperate flag to disable just the ctime check.
|
||||
|
||||
https://github.com/restic/restic/issues/2179
|
||||
https://github.com/restic/restic/pull/2212
|
||||
|
||||
* Enhancement #2184: Add --json support to forget command
|
||||
|
||||
The forget command now supports the --json argument, outputting the information about what is
|
||||
@@ -1361,10 +1825,10 @@ Details
|
||||
|
||||
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.
|
||||
to 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!
|
||||
|
||||
|
||||
@@ -46,15 +46,12 @@ Remember, the easier it is for us to reproduce the bug, the earlier it will be
|
||||
corrected!
|
||||
|
||||
In addition, you can compile restic with debug support by running
|
||||
`go run -mod=vendor build.go -tags debug` and instructing it to create a debug
|
||||
`go run build.go -tags debug` and instructing it to create a debug
|
||||
log by setting the environment variable `DEBUG_LOG` to a file, e.g. like this:
|
||||
|
||||
$ export DEBUG_LOG=/tmp/restic-debug.log
|
||||
$ restic backup ~/work
|
||||
|
||||
For Go < 1.11, you need to remove the `-mod=vendor` option from the build
|
||||
command.
|
||||
|
||||
Please be aware that the debug log file will contain potentially sensitive
|
||||
things like file and directory names, so please either redact it before
|
||||
uploading it somewhere or post only the parts that are really relevant.
|
||||
@@ -63,16 +60,11 @@ uploading it somewhere or post only the parts that are really relevant.
|
||||
Development Environment
|
||||
=======================
|
||||
|
||||
The repository contains several 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
|
||||
command `go mod vendor`.
|
||||
The repository contains the code written for restic in the directories
|
||||
`cmd/` and `internal/`.
|
||||
|
||||
Go >= 1.11
|
||||
----------
|
||||
|
||||
For Go version 1.11 or later, you should clone the repo (without having
|
||||
`$GOPATH` set) and `cd` into the directory:
|
||||
Restic requires Go version 1.12 or later for compiling. Clone the repo (without
|
||||
having `$GOPATH` set) and `cd` into the directory:
|
||||
|
||||
$ unset GOPATH
|
||||
$ git clone https://github.com/restic/restic
|
||||
@@ -82,40 +74,12 @@ Then use the `go` tool to build restic:
|
||||
|
||||
$ go build ./cmd/restic
|
||||
$ ./restic version
|
||||
restic 0.9.2-dev (compiled manually) compiled with go1.11 on linux/amd64
|
||||
restic 0.9.6-dev (compiled manually) compiled with go1.14 on linux/amd64
|
||||
|
||||
You can run all tests with the following command:
|
||||
|
||||
$ go test ./...
|
||||
|
||||
Go < 1.11
|
||||
---------
|
||||
|
||||
In order to compile restic with Go before 1.11, 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).
|
||||
|
||||
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
|
||||
|
||||
You can then build restic as follows:
|
||||
|
||||
$ go build ./cmd/restic
|
||||
$ ./restic version
|
||||
restic compiled manually
|
||||
compiled with go1.8.3 on linux/amd64
|
||||
|
||||
The following commands can be used to run all the tests:
|
||||
|
||||
$ go test ./...
|
||||
|
||||
Providing Patches
|
||||
=================
|
||||
|
||||
@@ -128,20 +92,19 @@ down to the following steps:
|
||||
GitHub. For a new feature, please add an issue before starting to work on
|
||||
it, so that duplicate work is prevented.
|
||||
|
||||
1. First we would kindly ask you to fork our project on GitHub if you haven't
|
||||
done so already.
|
||||
1. Next, fork our project on GitHub if you haven't done so already.
|
||||
|
||||
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.
|
||||
2. Clone your fork of the repository locally and **create a new branch** for
|
||||
your changes. If you are working on the code itself, please set up the
|
||||
development environment as described in the previous section.
|
||||
|
||||
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.
|
||||
3. Commit your changes to the new branch as fine grained as possible, as
|
||||
smaller patches, for individual changes, are easier to discuss and merge.
|
||||
|
||||
4. Push the new branch with your changes to your fork of the repository.
|
||||
|
||||
5. Create a pull request by visiting the GitHub website, it will guide you
|
||||
through the process.
|
||||
through the process. Please [allow edits from maintainers](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
|
||||
|
||||
6. You will receive comments on your code and the feature or bug that they
|
||||
address. Maybe you need to rework some minor things, in this case push new
|
||||
@@ -149,20 +112,19 @@ down to the following steps:
|
||||
existing commit, use common sense to decide which is better), they will be
|
||||
automatically added to the pull request.
|
||||
|
||||
7. If your pull request changes anything that users should be aware
|
||||
of (a bugfix, a new feature, ...) please add an entry as a new
|
||||
file in `changelog/unreleased` including the issue number in the
|
||||
filename (e.g. `issue-8756`). Use the template in
|
||||
`changelog/TEMPLATE` for the content. It will be used in the
|
||||
announcement of the next stable release. While writing, ask
|
||||
yourself: If I were the user, what would I need to be aware of
|
||||
with this change.
|
||||
7. If your pull request changes anything that users should be aware of
|
||||
(a bugfix, a new feature, ...) please add an entry as a new file in
|
||||
`changelog/unreleased` including the issue number in the filename (e.g.
|
||||
`issue-8756`). Use the template in `changelog/TEMPLATE` for the content.
|
||||
It will be used in the announcement of the next stable release. While
|
||||
writing, ask yourself: If I were the user, what would I need to be aware
|
||||
of with this change?
|
||||
|
||||
8. Once your code looks good and passes all the tests, we'll merge it. Thanks
|
||||
a 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.
|
||||
open up a pull request for each, as this simplifies discussion and merging.
|
||||
|
||||
The restic project uses the `gofmt` tool for Go source indentation, so please
|
||||
run
|
||||
|
||||
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@
|
||||
all: restic
|
||||
|
||||
restic:
|
||||
go run -mod=vendor build.go || go run build.go
|
||||
go run build.go
|
||||
|
||||
clean:
|
||||
rm -f restic
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |TestCoverage| |Reviewed by Hound|
|
||||
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |Reviewed by Hound|
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
@@ -20,8 +20,8 @@ init:
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://dl.google.com/go/go1.13.4.windows-amd64.msi
|
||||
- msiexec /i go1.13.4.windows-amd64.msi /q
|
||||
- appveyor DownloadFile https://dl.google.com/go/go1.15.2.windows-amd64.msi
|
||||
- msiexec /i go1.15.2.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
|
||||
@@ -29,4 +29,4 @@ install:
|
||||
- set PATH=bin/;%PATH%
|
||||
|
||||
build_script:
|
||||
- go run -mod=vendor run_integration_tests.go
|
||||
- go run run_integration_tests.go
|
||||
|
||||
273
build.go
273
build.go
@@ -3,22 +3,15 @@
|
||||
// This program aims to make building Go programs for end users easier by just
|
||||
// calling it with `go run`, without having to setup a GOPATH.
|
||||
//
|
||||
// For Go < 1.11, it'll create a new GOPATH in a temporary directory, then run
|
||||
// `go build` on the package configured as Main in the Config struct.
|
||||
//
|
||||
// For Go >= 1.11 if the file go.mod is present, it'll use Go modules and not
|
||||
// setup a GOPATH. It builds the package configured as Main in the Config
|
||||
// struct with `go build -mod=vendor` to use the vendored dependencies.
|
||||
// The variable GOPROXY is set to `off` so that no network calls are made. All
|
||||
// files are copied to a temporary directory before `go build` is called within
|
||||
// that directory.
|
||||
// This program needs Go >= 1.12. It'll use Go modules for compilation. It
|
||||
// builds the package configured as Main in the Config struct.
|
||||
|
||||
// BSD 2-Clause License
|
||||
//
|
||||
// Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
|
||||
// All rights reserved.
|
||||
//
|
||||
// This file has been copied from the repository at:
|
||||
// This file has been derived from the repository at:
|
||||
// https://github.com/fd0/build-go
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
@@ -65,7 +58,7 @@ var config = Config{
|
||||
Main: "./cmd/restic", // package name for the main package
|
||||
DefaultBuildTags: []string{"selfupdate"}, // specify build tags which are always used
|
||||
Tests: []string{"./..."}, // tests to run
|
||||
MinVersion: GoVersion{Major: 1, Minor: 10, Patch: 0}, // minimum Go version supported
|
||||
MinVersion: GoVersion{Major: 1, Minor: 11, Patch: 0}, // minimum Go version supported
|
||||
}
|
||||
|
||||
// Config configures the build.
|
||||
@@ -79,125 +72,13 @@ type Config struct {
|
||||
}
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
keepGopath bool
|
||||
runTests bool
|
||||
enableCGO bool
|
||||
enablePIE bool
|
||||
goVersion = ParseGoVersion(runtime.Version())
|
||||
verbose bool
|
||||
runTests bool
|
||||
enableCGO bool
|
||||
enablePIE bool
|
||||
goVersion = ParseGoVersion(runtime.Version())
|
||||
)
|
||||
|
||||
// copy all Go files in src to dst, creating directories on the fly, so calling
|
||||
//
|
||||
// copy("/tmp/gopath/src/github.com/restic/restic", "/home/u/restic")
|
||||
//
|
||||
// with "/home/u/restic" containing the file "foo.go" yields the following tree
|
||||
// at "/tmp/gopath":
|
||||
//
|
||||
// /tmp/gopath
|
||||
// └── src
|
||||
// └── github.com
|
||||
// └── restic
|
||||
// └── restic
|
||||
// └── foo.go
|
||||
func copy(dst, src string) error {
|
||||
verbosePrintf("copy contents of %v to %v\n", src, dst)
|
||||
return filepath.Walk(src, func(name string, fi os.FileInfo, err error) error {
|
||||
if name == src {
|
||||
return err
|
||||
}
|
||||
|
||||
if name == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
intermediatePath, err := filepath.Rel(src, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileSrc := filepath.Join(src, intermediatePath)
|
||||
fileDst := filepath.Join(dst, intermediatePath)
|
||||
|
||||
return copyFile(fileDst, fileSrc)
|
||||
})
|
||||
}
|
||||
|
||||
func directoryExists(dirname string) bool {
|
||||
stat, err := os.Stat(dirname)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return stat.IsDir()
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
stat, err := os.Stat(filename)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
|
||||
return stat.Mode().IsRegular()
|
||||
}
|
||||
|
||||
// copyFile creates dst from src, preserving file attributes and timestamps.
|
||||
func copyFile(dst, src string) error {
|
||||
fi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fsrc, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
fmt.Printf("MkdirAll(%v)\n", filepath.Dir(dst))
|
||||
return err
|
||||
}
|
||||
|
||||
fdst, err := os.Create(dst)
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(fdst, fsrc)
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
_ = fdst.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
err = fdst.Close()
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
err = fsrc.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Chmod(dst, fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Chtimes(dst, fi.ModTime(), fi.ModTime())
|
||||
}
|
||||
|
||||
// die prints the message with fmt.Fprintf() to stderr and exits with an error
|
||||
// code.
|
||||
func die(message string, args ...interface{}) {
|
||||
@@ -211,7 +92,6 @@ func showUsage(output io.Writer) {
|
||||
fmt.Fprintf(output, "OPTIONS:\n")
|
||||
fmt.Fprintf(output, " -v --verbose output more messages\n")
|
||||
fmt.Fprintf(output, " -t --tags specify additional build tags\n")
|
||||
fmt.Fprintf(output, " -k --keep-tempdir do not remove the temporary directory after build\n")
|
||||
fmt.Fprintf(output, " -T --test run tests\n")
|
||||
fmt.Fprintf(output, " -o --output set output file name\n")
|
||||
fmt.Fprintf(output, " --enable-cgo use CGO to link against libc\n")
|
||||
@@ -219,7 +99,6 @@ func showUsage(output io.Writer) {
|
||||
fmt.Fprintf(output, " --goos value set GOOS for cross-compilation\n")
|
||||
fmt.Fprintf(output, " --goarch value set GOARCH for cross-compilation\n")
|
||||
fmt.Fprintf(output, " --goarm value set GOARM for cross-compilation\n")
|
||||
fmt.Fprintf(output, " --tempdir dir use a specific directory for compilation\n")
|
||||
}
|
||||
|
||||
func verbosePrintf(message string, args ...interface{}) {
|
||||
@@ -230,49 +109,39 @@ func verbosePrintf(message string, args ...interface{}) {
|
||||
fmt.Printf("build: "+message, args...)
|
||||
}
|
||||
|
||||
// cleanEnv returns a clean environment with GOPATH, GOBIN and GO111MODULE
|
||||
// removed (if present).
|
||||
func cleanEnv() (env []string) {
|
||||
removeKeys := map[string]struct{}{
|
||||
"GOPATH": struct{}{},
|
||||
"GOBIN": struct{}{},
|
||||
"GO111MODULE": struct{}{},
|
||||
}
|
||||
|
||||
for _, v := range os.Environ() {
|
||||
data := strings.SplitN(v, "=", 2)
|
||||
name := data[0]
|
||||
|
||||
if _, ok := removeKeys[name]; ok {
|
||||
// printEnv prints Go-relevant environment variables in a nice way using verbosePrintf.
|
||||
func printEnv(env []string) {
|
||||
verbosePrintf("environment (GO*):\n")
|
||||
for _, v := range env {
|
||||
// ignore environment variables which do not start with GO*.
|
||||
if !strings.HasPrefix(v, "GO") {
|
||||
continue
|
||||
}
|
||||
|
||||
env = append(env, v)
|
||||
verbosePrintf(" %s\n", v)
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// build runs "go build args..." with GOPATH set to gopath.
|
||||
func build(cwd string, env map[string]string, args ...string) error {
|
||||
a := []string{"build"}
|
||||
|
||||
if goVersion.AtLeast(GoVersion{1, 10, 0}) {
|
||||
verbosePrintf("Go version is at least 1.10, using new syntax for -gcflags\n")
|
||||
// use new prefix
|
||||
// try to remove all absolute paths from resulting binary
|
||||
if goVersion.AtLeast(GoVersion{1, 13, 0}) {
|
||||
// use the new flag introduced by Go 1.13
|
||||
a = append(a, "-trimpath")
|
||||
} else {
|
||||
// otherwise try to trim as many paths as possible
|
||||
a = append(a, "-asmflags", fmt.Sprintf("all=-trimpath=%s", cwd))
|
||||
a = append(a, "-gcflags", fmt.Sprintf("all=-trimpath=%s", cwd))
|
||||
} else {
|
||||
a = append(a, "-asmflags", fmt.Sprintf("-trimpath=%s", cwd))
|
||||
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", cwd))
|
||||
}
|
||||
|
||||
if enablePIE {
|
||||
a = append(a, "-buildmode=pie")
|
||||
}
|
||||
|
||||
a = append(a, args...)
|
||||
cmd := exec.Command("go", a...)
|
||||
cmd.Env = append(cleanEnv(), "GOPROXY=off")
|
||||
cmd.Env = os.Environ()
|
||||
for k, v := range env {
|
||||
cmd.Env = append(cmd.Env, k+"="+v)
|
||||
}
|
||||
@@ -280,6 +149,8 @@ func build(cwd string, env map[string]string, args ...string) error {
|
||||
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
|
||||
}
|
||||
|
||||
printEnv(cmd.Env)
|
||||
|
||||
cmd.Dir = cwd
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -294,7 +165,7 @@ func build(cwd string, env map[string]string, args ...string) error {
|
||||
func test(cwd string, env map[string]string, args ...string) error {
|
||||
args = append([]string{"test", "-count", "1"}, args...)
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Env = append(cleanEnv(), "GOPROXY=off")
|
||||
cmd.Env = os.Environ()
|
||||
for k, v := range env {
|
||||
cmd.Env = append(cmd.Env, k+"="+v)
|
||||
}
|
||||
@@ -305,6 +176,8 @@ func test(cwd string, env map[string]string, args ...string) error {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
printEnv(cmd.Env)
|
||||
|
||||
verbosePrintf("chdir %q\n", cwd)
|
||||
verbosePrintf("go %q\n", args)
|
||||
|
||||
@@ -454,6 +327,10 @@ func (v GoVersion) String() string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
if !goVersion.AtLeast(GoVersion{1, 12, 0}) {
|
||||
die("Go version (%v) is too old, restic requires Go >= 1.12\n", goVersion)
|
||||
}
|
||||
|
||||
if !goVersion.AtLeast(config.MinVersion) {
|
||||
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion)
|
||||
os.Exit(1)
|
||||
@@ -464,15 +341,13 @@ func main() {
|
||||
skipNext := false
|
||||
params := os.Args[1:]
|
||||
|
||||
goEnv := map[string]string{}
|
||||
buildEnv := map[string]string{
|
||||
"GOOS": runtime.GOOS,
|
||||
"GOARCH": runtime.GOARCH,
|
||||
"GOARM": "",
|
||||
env := map[string]string{
|
||||
"GO111MODULE": "on", // make sure we build in Module mode
|
||||
"GOOS": runtime.GOOS,
|
||||
"GOARCH": runtime.GOARCH,
|
||||
"GOARM": "",
|
||||
}
|
||||
|
||||
tempdir := ""
|
||||
|
||||
var outputFilename string
|
||||
|
||||
for i, arg := range params {
|
||||
@@ -484,8 +359,6 @@ func main() {
|
||||
switch arg {
|
||||
case "-v", "--verbose":
|
||||
verbose = true
|
||||
case "-k", "--keep-gopath":
|
||||
keepGopath = true
|
||||
case "-t", "-tags", "--tags":
|
||||
if i+1 >= len(params) {
|
||||
die("-t given but no tag specified")
|
||||
@@ -495,9 +368,6 @@ func main() {
|
||||
case "-o", "--output":
|
||||
skipNext = true
|
||||
outputFilename = params[i+1]
|
||||
case "--tempdir":
|
||||
skipNext = true
|
||||
tempdir = params[i+1]
|
||||
case "-T", "--test":
|
||||
runTests = true
|
||||
case "--enable-cgo":
|
||||
@@ -506,13 +376,13 @@ func main() {
|
||||
enablePIE = true
|
||||
case "--goos":
|
||||
skipNext = true
|
||||
buildEnv["GOOS"] = params[i+1]
|
||||
env["GOOS"] = params[i+1]
|
||||
case "--goarch":
|
||||
skipNext = true
|
||||
buildEnv["GOARCH"] = params[i+1]
|
||||
env["GOARCH"] = params[i+1]
|
||||
case "--goarm":
|
||||
skipNext = true
|
||||
buildEnv["GOARM"] = params[i+1]
|
||||
env["GOARM"] = params[i+1]
|
||||
case "-h":
|
||||
showUsage(os.Stdout)
|
||||
return
|
||||
@@ -525,8 +395,12 @@ func main() {
|
||||
|
||||
verbosePrintf("detected Go version %v\n", goVersion)
|
||||
|
||||
preserveSymbols := false
|
||||
for i := range buildTags {
|
||||
buildTags[i] = strings.TrimSpace(buildTags[i])
|
||||
if buildTags[i] == "debug" || buildTags[i] == "profile" {
|
||||
preserveSymbols = true
|
||||
}
|
||||
}
|
||||
|
||||
verbosePrintf("build tags: %s\n", buildTags)
|
||||
@@ -538,7 +412,7 @@ func main() {
|
||||
|
||||
if outputFilename == "" {
|
||||
outputFilename = config.Name
|
||||
if buildEnv["GOOS"] == "windows" {
|
||||
if env["GOOS"] == "windows" {
|
||||
outputFilename += ".exe"
|
||||
}
|
||||
}
|
||||
@@ -553,7 +427,11 @@ func main() {
|
||||
if version != "" {
|
||||
constants["main.version"] = version
|
||||
}
|
||||
ldflags := "-s -w " + constants.LDFlags()
|
||||
ldflags := constants.LDFlags()
|
||||
if !preserveSymbols {
|
||||
// Strip debug symbols.
|
||||
ldflags = "-s -w " + ldflags
|
||||
}
|
||||
verbosePrintf("ldflags: %s\n", ldflags)
|
||||
|
||||
var (
|
||||
@@ -567,57 +445,18 @@ func main() {
|
||||
}
|
||||
|
||||
buildTarget := filepath.FromSlash(mainPackage)
|
||||
buildCWD := ""
|
||||
|
||||
if goVersion.AtLeast(GoVersion{1, 11, 0}) && fileExists("go.mod") {
|
||||
verbosePrintf("Go >= 1.11 and 'go.mod' found, building with modules\n")
|
||||
buildCWD = root
|
||||
|
||||
buildArgs = append(buildArgs, "-mod=vendor")
|
||||
testArgs = append(testArgs, "-mod=vendor")
|
||||
|
||||
goEnv["GO111MODULE"] = "on"
|
||||
buildEnv["GO111MODULE"] = "on"
|
||||
} else {
|
||||
if tempdir == "" {
|
||||
tempdir, err = ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
|
||||
if err != nil {
|
||||
die("TempDir(): %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
verbosePrintf("Go < 1.11 or 'go.mod' not found, create GOPATH at %v\n", tempdir)
|
||||
targetdir := filepath.Join(tempdir, "src", filepath.FromSlash(config.Namespace))
|
||||
if err = copy(targetdir, root); err != nil {
|
||||
die("copying files from %v to %v/src failed: %v\n", root, tempdir, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if !keepGopath {
|
||||
verbosePrintf("remove %v\n", tempdir)
|
||||
if err = os.RemoveAll(tempdir); err != nil {
|
||||
die("remove GOPATH at %s failed: %v\n", tempdir, err)
|
||||
}
|
||||
} else {
|
||||
verbosePrintf("leaving temporary GOPATH at %v\n", tempdir)
|
||||
}
|
||||
}()
|
||||
|
||||
buildCWD = targetdir
|
||||
|
||||
goEnv["GOPATH"] = tempdir
|
||||
buildEnv["GOPATH"] = tempdir
|
||||
buildCWD, err := os.Getwd()
|
||||
if err != nil {
|
||||
die("unable to determine current working directory: %v\n", err)
|
||||
}
|
||||
|
||||
verbosePrintf("environment:\n go: %v\n build: %v\n", goEnv, buildEnv)
|
||||
|
||||
buildArgs = append(buildArgs,
|
||||
"-tags", strings.Join(buildTags, " "),
|
||||
"-ldflags", ldflags,
|
||||
"-o", output, buildTarget,
|
||||
)
|
||||
|
||||
err = build(buildCWD, buildEnv, buildArgs...)
|
||||
err = build(buildCWD, env, buildArgs...)
|
||||
if err != nil {
|
||||
die("build failed: %v\n", err)
|
||||
}
|
||||
@@ -627,7 +466,7 @@ func main() {
|
||||
|
||||
testArgs = append(testArgs, config.Tests...)
|
||||
|
||||
err = test(buildCWD, goEnv, testArgs...)
|
||||
err = test(buildCWD, env, testArgs...)
|
||||
if err != nil {
|
||||
die("running tests failed: %v\n", err)
|
||||
}
|
||||
|
||||
0
changelog/0.10.0_2020-09-19/.gitkeep
Normal file
0
changelog/0.10.0_2020-09-19/.gitkeep
Normal file
9
changelog/0.10.0_2020-09-19/issue-1570
Normal file
9
changelog/0.10.0_2020-09-19/issue-1570
Normal file
@@ -0,0 +1,9 @@
|
||||
Enhancement: Support specifying multiple host flags for various commands
|
||||
|
||||
Previously commands didn't take more than one `--host` or `-H` argument into account, which could be limiting with e.g.
|
||||
the `forget` command.
|
||||
|
||||
The `dump`, `find`, `forget`, `ls`, `mount`, `restore`, `snapshots`, `stats` and `tag` commands will now take into account
|
||||
multiple `--host` and `-H` flags.
|
||||
|
||||
https://github.com/restic/restic/issues/1570
|
||||
8
changelog/0.10.0_2020-09-19/issue-1680
Normal file
8
changelog/0.10.0_2020-09-19/issue-1680
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Optimize `restic mount`
|
||||
|
||||
We've optimized the FUSE implementation used within restic.
|
||||
`restic mount` is now more responsive and uses less memory.
|
||||
|
||||
https://github.com/restic/restic/issues/1680
|
||||
https://github.com/restic/restic/pull/2587
|
||||
https://github.com/restic/restic/pull/2787
|
||||
6
changelog/0.10.0_2020-09-19/issue-1863
Normal file
6
changelog/0.10.0_2020-09-19/issue-1863
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Report correct number of directories processed by backup
|
||||
|
||||
The directory statistics calculation was fixed to report the actual number
|
||||
of processed directories instead of always zero.
|
||||
|
||||
https://github.com/restic/restic/issues/1863
|
||||
5
changelog/0.10.0_2020-09-19/issue-2072
Normal file
5
changelog/0.10.0_2020-09-19/issue-2072
Normal file
@@ -0,0 +1,5 @@
|
||||
Enhancement: Display snapshot date when using `restic find`
|
||||
|
||||
Added the respective snapshot date to the output of `restic find`.
|
||||
|
||||
https://github.com/restic/restic/issues/2072
|
||||
9
changelog/0.10.0_2020-09-19/issue-2175
Normal file
9
changelog/0.10.0_2020-09-19/issue-2175
Normal file
@@ -0,0 +1,9 @@
|
||||
Enhancement: Allow specifying user and host when creating keys
|
||||
|
||||
When adding a new key to the repository, the username and hostname for the new
|
||||
key can be specified on the command line. This allows overriding the defaults,
|
||||
for example if you would prefer to use the FQDN to identify the host or if you
|
||||
want to add keys for several different hosts without having to run the key add
|
||||
command on those hosts.
|
||||
|
||||
https://github.com/restic/restic/issues/2175
|
||||
9
changelog/0.10.0_2020-09-19/issue-2254
Normal file
9
changelog/0.10.0_2020-09-19/issue-2254
Normal file
@@ -0,0 +1,9 @@
|
||||
Bugfix: Fix tar issues when dumping `/`
|
||||
|
||||
We've fixed an issue with dumping either `/` or files on the first sublevel
|
||||
e.g. `/foo` to tar. This also fixes tar dumping issues on Windows where this
|
||||
issue could also happen.
|
||||
|
||||
https://github.com/restic/restic/issues/2254
|
||||
https://github.com/restic/restic/issues/2357
|
||||
https://github.com/restic/restic/pull/2255
|
||||
5
changelog/0.10.0_2020-09-19/issue-2277
Normal file
5
changelog/0.10.0_2020-09-19/issue-2277
Normal file
@@ -0,0 +1,5 @@
|
||||
Enhancement: Add support for ppc64le
|
||||
|
||||
Adds support for ppc64le, the processor architecture from IBM.
|
||||
|
||||
https://github.com/restic/restic/issues/2277
|
||||
7
changelog/0.10.0_2020-09-19/issue-2281
Normal file
7
changelog/0.10.0_2020-09-19/issue-2281
Normal file
@@ -0,0 +1,7 @@
|
||||
Bugfix: Handle format verbs like '%' properly in `find` output
|
||||
|
||||
The JSON or "normal" output of the `find` command can now deal with file names
|
||||
that contain substrings which the Golang `fmt` package considers "format verbs"
|
||||
like `%s`.
|
||||
|
||||
https://github.com/restic/restic/issues/2281
|
||||
8
changelog/0.10.0_2020-09-19/issue-2298
Normal file
8
changelog/0.10.0_2020-09-19/issue-2298
Normal file
@@ -0,0 +1,8 @@
|
||||
Bugfix: Do not hang when run as a background job
|
||||
|
||||
Restic did hang on exit while restoring the terminal configuration when it was
|
||||
started as a background job, for example using `restic ... &`. This has been
|
||||
fixed by only restoring the terminal configuration when restic is interrupted
|
||||
while reading a password from the terminal.
|
||||
|
||||
https://github.com/restic/restic/issues/2298
|
||||
8
changelog/0.10.0_2020-09-19/issue-2389
Normal file
8
changelog/0.10.0_2020-09-19/issue-2389
Normal file
@@ -0,0 +1,8 @@
|
||||
Bugfix: Fix mangled json output of backup command
|
||||
|
||||
We've fixed a race condition in the json output of the backup command
|
||||
that could cause multiple lines to get mixed up. We've also ensured that
|
||||
the backup summary is printed last.
|
||||
|
||||
https://github.com/restic/restic/issues/2389
|
||||
https://github.com/restic/restic/pull/2545
|
||||
5
changelog/0.10.0_2020-09-19/issue-2390
Normal file
5
changelog/0.10.0_2020-09-19/issue-2390
Normal file
@@ -0,0 +1,5 @@
|
||||
Bugfix: Refresh lock timestamp
|
||||
|
||||
Long-running operations did not refresh lock timestamp, resulting in locks becoming stale. This is now fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/2390
|
||||
12
changelog/0.10.0_2020-09-19/issue-2395
Normal file
12
changelog/0.10.0_2020-09-19/issue-2395
Normal file
@@ -0,0 +1,12 @@
|
||||
Enhancement: Ignore sync errors when operation not supported by local filesystem
|
||||
|
||||
The local backend has been modified to work with filesystems which doesn't support
|
||||
the `sync` operation. This operation is normally used by restic to ensure that data
|
||||
files are fully written to disk before continuing.
|
||||
|
||||
For these limited filesystems, saving a file in the backend would previously fail with
|
||||
an "operation not supported" error. This error is now ignored, which means that e.g.
|
||||
an SMB mount on macOS can now be used as storage location for a repository.
|
||||
|
||||
https://github.com/restic/restic/issues/2395
|
||||
https://forum.restic.net/t/sync-errors-on-mac-over-smb/1859
|
||||
7
changelog/0.10.0_2020-09-19/issue-2427
Normal file
7
changelog/0.10.0_2020-09-19/issue-2427
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Add flag `--iexclude-file` to backup command
|
||||
|
||||
The backup command now supports the flag `--iexclude-file` which is a
|
||||
case-insensitive version of `--exclude-file`.
|
||||
|
||||
https://github.com/restic/restic/issues/2427
|
||||
https://github.com/restic/restic/pull/2898
|
||||
6
changelog/0.10.0_2020-09-19/issue-2429
Normal file
6
changelog/0.10.0_2020-09-19/issue-2429
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: backup --json reports total_bytes_processed as 0
|
||||
|
||||
We've fixed the json output of total_bytes_processed. The non-json output
|
||||
was already fixed with pull request #2138 but left the json output untouched.
|
||||
|
||||
https://github.com/restic/restic/issues/2429
|
||||
5
changelog/0.10.0_2020-09-19/issue-2469
Normal file
5
changelog/0.10.0_2020-09-19/issue-2469
Normal file
@@ -0,0 +1,5 @@
|
||||
Bugfix: Fix incorrect bytes stats in `diff` command
|
||||
|
||||
In some cases, the wrong number of bytes (e.g. 16777215.998 TiB) were reported by the `diff` command. This is now fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/2469
|
||||
9
changelog/0.10.0_2020-09-19/issue-2482
Normal file
9
changelog/0.10.0_2020-09-19/issue-2482
Normal file
@@ -0,0 +1,9 @@
|
||||
Change: Remove vendored dependencies
|
||||
|
||||
We've removed the vendored dependencies (in the subdir `vendor/`). When
|
||||
building restic, the Go compiler automatically fetches the dependencies. It
|
||||
will also cryptographically verify that the correct code has been fetched by
|
||||
using the hashes in `go.sum` (see the link to the documentation below).
|
||||
|
||||
https://github.com/restic/restic/issues/2482
|
||||
https://golang.org/cmd/go/#hdr-Module_downloading_and_verification
|
||||
16
changelog/0.10.0_2020-09-19/issue-2518
Normal file
16
changelog/0.10.0_2020-09-19/issue-2518
Normal file
@@ -0,0 +1,16 @@
|
||||
Bugfix: Do not crash with Synology NAS sftp server
|
||||
|
||||
It was found that when restic is used to store data on an sftp server on a
|
||||
Synology NAS with a relative path (one which does not start with a slash), it
|
||||
may go into an endless loop trying to create directories on the server. We've
|
||||
fixed this bug by using a function in the sftp library instead of our own
|
||||
implementation.
|
||||
|
||||
The bug was discovered because the Synology sftp server behaves erratic with
|
||||
non-absolute path (e.g. `home/restic-repo`). This can be resolved by just using
|
||||
an absolute path instead (`/home/restic-repo`). We've also added a paragraph in
|
||||
the FAQ.
|
||||
|
||||
https://github.com/restic/restic/issues/2518
|
||||
https://github.com/restic/restic/issues/2363
|
||||
https://github.com/restic/restic/pull/2530
|
||||
5
changelog/0.10.0_2020-09-19/issue-2531
Normal file
5
changelog/0.10.0_2020-09-19/issue-2531
Normal file
@@ -0,0 +1,5 @@
|
||||
Bugfix: Fix incorrect size calculation in `stats --mode restore-size`
|
||||
|
||||
The restore-size mode of stats was counting hard-linked files as if they were independent.
|
||||
|
||||
https://github.com/restic/restic/issues/2531
|
||||
5
changelog/0.10.0_2020-09-19/issue-2537
Normal file
5
changelog/0.10.0_2020-09-19/issue-2537
Normal file
@@ -0,0 +1,5 @@
|
||||
Bugfix: Fix incorrect file counts in `stats --mode restore-size`
|
||||
|
||||
The restore-size mode of stats was failing to count empty directories and some files with hard links.
|
||||
|
||||
https://github.com/restic/restic/issues/2537
|
||||
8
changelog/0.10.0_2020-09-19/issue-2569
Normal file
8
changelog/0.10.0_2020-09-19/issue-2569
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Support excluding files by their size
|
||||
|
||||
The `backup` command now supports the `--exclude-larger-than` option to exclude files which are
|
||||
larger than the specified maximum size. This can for example be useful to exclude unimportant
|
||||
files with a large file size.
|
||||
|
||||
https://github.com/restic/restic/issues/2569
|
||||
https://github.com/restic/restic/pull/2914
|
||||
16
changelog/0.10.0_2020-09-19/issue-2571
Normal file
16
changelog/0.10.0_2020-09-19/issue-2571
Normal file
@@ -0,0 +1,16 @@
|
||||
Enhancement: Self-heal missing file parts during backup of unchanged files
|
||||
|
||||
We've improved the resilience of restic to certain types of repository corruption.
|
||||
|
||||
For files that are unchanged since the parent snapshot, the backup command now
|
||||
verifies that all parts of the files still exist in the repository. Parts that are
|
||||
missing, e.g. from a damaged repository, are backed up again. This verification
|
||||
was already run for files that were modified since the parent snapshot, but is
|
||||
now also done for unchanged files.
|
||||
|
||||
Note that restic will not backup file parts that are referenced in the index but
|
||||
where the actual data is not present on disk, as this situation can only be
|
||||
detected by restic check. Please ensure that you run `restic check` regularly.
|
||||
|
||||
https://github.com/restic/restic/issues/2571
|
||||
https://github.com/restic/restic/pull/2827
|
||||
9
changelog/0.10.0_2020-09-19/issue-2858
Normal file
9
changelog/0.10.0_2020-09-19/issue-2858
Normal file
@@ -0,0 +1,9 @@
|
||||
Enhancement: Support filtering snapshots by tag and path in the stats command
|
||||
|
||||
We've added filtering snapshots by `--tag tagList` and by `--path path` to
|
||||
the `stats` command. This includes filtering of only 'latest' snapshots or
|
||||
all snapshots in a repository.
|
||||
|
||||
https://github.com/restic/restic/issues/2858
|
||||
https://github.com/restic/restic/pull/2859
|
||||
https://forum.restic.net/t/stats-for-a-host-and-filtered-snapshots/3020
|
||||
20
changelog/0.10.0_2020-09-19/issue-323
Normal file
20
changelog/0.10.0_2020-09-19/issue-323
Normal file
@@ -0,0 +1,20 @@
|
||||
Enhancement: Add command for copying snapshots between repositories
|
||||
|
||||
We've added a copy command, allowing you to copy snapshots from one
|
||||
repository to another.
|
||||
|
||||
Note that this process will have to read (download) and write (upload) the
|
||||
entire snapshot(s) due to the different encryption keys used on the source
|
||||
and destination repository. Also, the transferred files are not re-chunked,
|
||||
which may break deduplication between files already stored in the
|
||||
destination repo and files copied there using this command.
|
||||
|
||||
To fully support deduplication between repositories when the copy command is
|
||||
used, the init command now supports the `--copy-chunker-params` option,
|
||||
which initializes the new repository with identical parameters for splitting
|
||||
files into chunks as an already existing repository. This allows copied
|
||||
snapshots to be equally deduplicated in both repositories.
|
||||
|
||||
https://github.com/restic/restic/issues/323
|
||||
https://github.com/restic/restic/pull/2606
|
||||
https://github.com/restic/restic/pull/2928
|
||||
10
changelog/0.10.0_2020-09-19/issue-551
Normal file
10
changelog/0.10.0_2020-09-19/issue-551
Normal file
@@ -0,0 +1,10 @@
|
||||
Enhancement: Use optimized library for hash calculation of file chunks
|
||||
|
||||
We've switched the library used to calculate the hashes of file chunks, which
|
||||
are used for deduplication, to the optimized Minio SHA-256 implementation.
|
||||
|
||||
Depending on the CPU it improves the hashing throughput by 10-30%. Modern x86
|
||||
CPUs with the SHA Extension should be about two to three times faster.
|
||||
|
||||
https://github.com/restic/restic/issues/551
|
||||
https://github.com/restic/restic/pull/2709
|
||||
23
changelog/0.10.0_2020-09-19/pull-2195
Normal file
23
changelog/0.10.0_2020-09-19/pull-2195
Normal file
@@ -0,0 +1,23 @@
|
||||
Enhancement: Simplify and improve restore performance
|
||||
|
||||
Significantly improves restore performance of large files (i.e. 50M+):
|
||||
https://github.com/restic/restic/issues/2074
|
||||
https://forum.restic.net/t/restore-using-rclone-gdrive-backend-is-slow/1112/8
|
||||
https://forum.restic.net/t/degraded-restore-performance-s3-backend/1400
|
||||
|
||||
Fixes "not enough cache capacity" error during restore:
|
||||
https://github.com/restic/restic/issues/2244
|
||||
|
||||
NOTE: This new implementation does not guarantee order in which blobs
|
||||
are written to the target files and, for example, the last blob of a
|
||||
file can be written to the file before any of the preceeding file blobs.
|
||||
It is therefore possible to have gaps in the data written to the target
|
||||
files if restore fails or interrupted by the user.
|
||||
|
||||
The implementation will try to preallocate space for the restored files
|
||||
on the filesystem to prevent file fragmentation. This ensures good read
|
||||
performance for large files, like for example VM images. If preallocating
|
||||
space is not supported by the filesystem, then this step is silently skipped.
|
||||
|
||||
https://github.com/restic/restic/pull/2195
|
||||
https://github.com/restic/restic/pull/2893
|
||||
8
changelog/0.10.0_2020-09-19/pull-2328
Normal file
8
changelog/0.10.0_2020-09-19/pull-2328
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Improve speed of check command
|
||||
|
||||
We've improved the check command to traverse trees only once independent of
|
||||
whether they are contained in multiple snapshots. The check command is now much
|
||||
faster for repositories with a large number of snapshots.
|
||||
|
||||
https://github.com/restic/restic/pull/2328
|
||||
https://github.com/restic/restic/issues/2284
|
||||
5
changelog/0.10.0_2020-09-19/pull-2423
Normal file
5
changelog/0.10.0_2020-09-19/pull-2423
Normal file
@@ -0,0 +1,5 @@
|
||||
Enhancement: support user@domain parsing as user
|
||||
|
||||
Added the ability for user@domain-like users to be authenticated over SFTP servers.
|
||||
|
||||
https://github.com/restic/restic/pull/2423
|
||||
19
changelog/0.10.0_2020-09-19/pull-2546
Normal file
19
changelog/0.10.0_2020-09-19/pull-2546
Normal file
@@ -0,0 +1,19 @@
|
||||
Change: Return exit code 3 when failing to backup all source data
|
||||
|
||||
The backup command used to return a zero exit code as long as a snapshot
|
||||
could be created successfully, even if some of the source files could not
|
||||
be read (in which case the snapshot would contain the rest of the files).
|
||||
|
||||
This made it hard for automation/scripts to detect failures/incomplete
|
||||
backups by looking at the exit code. Restic now returns the following exit
|
||||
codes for the backup command:
|
||||
|
||||
- 0 when the command was successful
|
||||
- 1 when there was a fatal error (no snapshot created)
|
||||
- 3 when some source data could not be read (incomplete snapshot created)
|
||||
|
||||
https://github.com/restic/restic/pull/2546
|
||||
https://github.com/restic/restic/issues/956
|
||||
https://github.com/restic/restic/issues/2064
|
||||
https://github.com/restic/restic/issues/2526
|
||||
https://github.com/restic/restic/issues/2364
|
||||
9
changelog/0.10.0_2020-09-19/pull-2576
Normal file
9
changelog/0.10.0_2020-09-19/pull-2576
Normal file
@@ -0,0 +1,9 @@
|
||||
Enhancement: Improve the chunking algorithm
|
||||
|
||||
We've updated the chunker library responsible for splitting files into smaller
|
||||
blocks. It should improve the chunking throughput by 5-15% depending on the
|
||||
CPU.
|
||||
|
||||
https://github.com/restic/restic/pull/2576
|
||||
https://github.com/restic/restic/pull/2845
|
||||
https://github.com/restic/restic/issues/2820
|
||||
6
changelog/0.10.0_2020-09-19/pull-2592
Normal file
6
changelog/0.10.0_2020-09-19/pull-2592
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: SFTP backend supports IPv6 addresses
|
||||
|
||||
The SFTP backend now supports IPv6 addresses natively, without relying on
|
||||
aliases in the external SSH configuration.
|
||||
|
||||
https://github.com/restic/restic/pull/2592
|
||||
6
changelog/0.10.0_2020-09-19/pull-2598
Normal file
6
changelog/0.10.0_2020-09-19/pull-2598
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Improve speed of diff command
|
||||
|
||||
We've improved the performance of the diff command when comparing snapshots
|
||||
with similar content. It should run up to twice as fast as before.
|
||||
|
||||
https://github.com/restic/restic/pull/2598
|
||||
6
changelog/0.10.0_2020-09-19/pull-2599
Normal file
6
changelog/0.10.0_2020-09-19/pull-2599
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Slightly reduce memory usage of prune and stats commands
|
||||
|
||||
The prune and the stats command kept directory identifiers in memory twice
|
||||
while searching for used blobs.
|
||||
|
||||
https://github.com/restic/restic/pull/2599
|
||||
14
changelog/0.10.0_2020-09-19/pull-2600
Normal file
14
changelog/0.10.0_2020-09-19/pull-2600
Normal file
@@ -0,0 +1,14 @@
|
||||
Change: Update dependencies, require Go >= 1.13
|
||||
|
||||
Restic now requires Go to be at least 1.13. This allows simplifications in the
|
||||
build process and removing workarounds.
|
||||
|
||||
This is also probably the last version of restic still supporting mounting
|
||||
repositories via fuse on macOS. The library we're using for fuse does not
|
||||
support macOS any more and osxfuse is not open source any more.
|
||||
|
||||
https://github.com/restic/restic/pull/2600
|
||||
https://github.com/restic/restic/pull/2852
|
||||
https://github.com/restic/restic/pull/2927
|
||||
https://github.com/bazil/fuse/issues/224
|
||||
https://github.com/osxfuse/osxfuse/issues/590
|
||||
7
changelog/0.10.0_2020-09-19/pull-2607
Normal file
7
changelog/0.10.0_2020-09-19/pull-2607
Normal file
@@ -0,0 +1,7 @@
|
||||
Bugfix: Honor RESTIC_CACHE_DIR environment variable on Mac and Windows
|
||||
|
||||
On Mac and Windows, the RESTIC_CACHE_DIR environment variable was ignored.
|
||||
This variable can now be used on all platforms to set the directory where
|
||||
restic stores caches.
|
||||
|
||||
https://github.com/restic/restic/pull/2607
|
||||
6
changelog/0.10.0_2020-09-19/pull-2668
Normal file
6
changelog/0.10.0_2020-09-19/pull-2668
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Don't abort the stats command when data blobs are missing
|
||||
|
||||
Runing the stats command in the blobs-per-file mode on a repository with
|
||||
missing data blobs previously resulted in a crash.
|
||||
|
||||
https://github.com/restic/restic/pull/2668
|
||||
8
changelog/0.10.0_2020-09-19/pull-2674
Normal file
8
changelog/0.10.0_2020-09-19/pull-2674
Normal file
@@ -0,0 +1,8 @@
|
||||
Bugfix: Add stricter prune error checks
|
||||
|
||||
Additional checks were added to the prune command in order to improve
|
||||
resiliency to backend, hardware and/or networking issues. The checks now
|
||||
detect a few more cases where such outside factors could potentially cause
|
||||
data loss.
|
||||
|
||||
https://github.com/restic/restic/pull/2674
|
||||
6
changelog/0.10.0_2020-09-19/pull-2733
Normal file
6
changelog/0.10.0_2020-09-19/pull-2733
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: S3 backend: Add support for WebIdentityTokenFile
|
||||
|
||||
We've added support for EKS IAM roles for service accounts feature to the S3 backend.
|
||||
|
||||
https://github.com/restic/restic/pull/2733
|
||||
https://github.com/restic/restic/issues/2703
|
||||
6
changelog/0.10.0_2020-09-19/pull-2773
Normal file
6
changelog/0.10.0_2020-09-19/pull-2773
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Optimize handling of new index entries
|
||||
|
||||
Restic now uses less memory for backups which add a lot of data, e.g. large initial backups.
|
||||
In addition, we've improved the stability in some edge cases.
|
||||
|
||||
https://github.com/restic/restic/pull/2773
|
||||
8
changelog/0.10.0_2020-09-19/pull-2781
Normal file
8
changelog/0.10.0_2020-09-19/pull-2781
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Reduce memory consumption of in-memory index
|
||||
|
||||
We've improved how the index is stored in memory.
|
||||
This change can reduce memory usage for large repositories by up to 50%
|
||||
(depending on the operation).
|
||||
|
||||
https://github.com/restic/restic/pull/2781
|
||||
https://github.com/restic/restic/pull/2812
|
||||
6
changelog/0.10.0_2020-09-19/pull-2786
Normal file
6
changelog/0.10.0_2020-09-19/pull-2786
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Optimize `list blobs` command
|
||||
|
||||
We've changed the implementation of `list blobs` which should be now a bit faster
|
||||
and consume almost no memory even for large repositories.
|
||||
|
||||
https://github.com/restic/restic/pull/2786
|
||||
6
changelog/0.10.0_2020-09-19/pull-2790
Normal file
6
changelog/0.10.0_2020-09-19/pull-2790
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Optimized file access in restic mount
|
||||
|
||||
Reading large (> 100GiB) files from restic mountpoints is now faster,
|
||||
and the speedup is greater for larger files.
|
||||
|
||||
https://github.com/restic/restic/pull/2790
|
||||
8
changelog/0.10.0_2020-09-19/pull-2821
Normal file
8
changelog/0.10.0_2020-09-19/pull-2821
Normal file
@@ -0,0 +1,8 @@
|
||||
Change: Honor the --no-lock flag in the mount command
|
||||
|
||||
The mount command now does not lock the repository if given the
|
||||
--no-lock flag. This allows to mount repositories which are archived
|
||||
on a read only backend/filesystem.
|
||||
|
||||
https://github.com/restic/restic/issues/1597
|
||||
https://github.com/restic/restic/pull/2821
|
||||
7
changelog/0.10.0_2020-09-19/pull-2840
Normal file
7
changelog/0.10.0_2020-09-19/pull-2840
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Speed-up file deletion in forget, prune and rebuild-index
|
||||
|
||||
We've sped up the file deletion for the commands forget, prune and
|
||||
rebuild-index, especially for remote repositories.
|
||||
Deletion was sequential before and is now run in parallel.
|
||||
|
||||
https://github.com/restic/restic/pull/2840
|
||||
9
changelog/0.10.0_2020-09-19/pull-2899
Normal file
9
changelog/0.10.0_2020-09-19/pull-2899
Normal file
@@ -0,0 +1,9 @@
|
||||
Bugfix: Fix possible crash in the progress bar of check --read-data
|
||||
|
||||
We've fixed a possible crash while displaying the progress bar for the
|
||||
check --read-data command. The crash occurred when the length of the
|
||||
progress bar status exceeded the terminal width, which only happened for
|
||||
very narrow terminal windows.
|
||||
|
||||
https://github.com/restic/restic/pull/2899
|
||||
https://forum.restic.net/t/restic-rclone-pcloud-connection-issues/2963/15
|
||||
@@ -7,7 +7,7 @@ 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
|
||||
addition, the attackers need to be able to 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
|
||||
|
||||
@@ -16,7 +16,7 @@ Details
|
||||
{{ range $entry := .Entries }}{{ with $entry }}
|
||||
* {{ .Type }} #{{ .PrimaryID }}: {{ .Title }}
|
||||
{{ range $par := .Paragraphs }}
|
||||
{{ wrap $par 80 3 }}
|
||||
{{ wrapIndent $par 80 3 }}
|
||||
{{ end -}}
|
||||
{{ range $url := .IssueURLs }}
|
||||
{{ $url -}}
|
||||
|
||||
@@ -9,4 +9,4 @@ in case there aren't any issue links) is used as the primary ID.
|
||||
|
||||
https://github.com/restic/restic/issues/1234
|
||||
https://github.com/restic/restic/pull/55555
|
||||
https://forum.restic/.net/foo/bar/baz
|
||||
https://forum.restic.net/foo/bar/baz
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
@@ -17,10 +16,8 @@ var cleanupHandlers struct {
|
||||
ch chan os.Signal
|
||||
}
|
||||
|
||||
var stderr = os.Stderr
|
||||
|
||||
func init() {
|
||||
cleanupHandlers.ch = make(chan os.Signal)
|
||||
cleanupHandlers.ch = make(chan os.Signal, 1)
|
||||
go CleanupHandler(cleanupHandlers.ch)
|
||||
signal.Notify(cleanupHandlers.ch, syscall.SIGINT)
|
||||
}
|
||||
@@ -51,7 +48,7 @@ func RunCleanupHandlers() {
|
||||
for _, f := range cleanupHandlers.list {
|
||||
err := f()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "error in cleanup handler: %v\n", err)
|
||||
Warnf("error in cleanup handler: %v\n", err)
|
||||
}
|
||||
}
|
||||
cleanupHandlers.list = nil
|
||||
@@ -61,7 +58,7 @@ func RunCleanupHandlers() {
|
||||
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)
|
||||
Warnf("%ssignal %v received, cleaning up\n", ClearLine(), s)
|
||||
|
||||
code := 0
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/textfile"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/jsonstatus"
|
||||
"github.com/restic/restic/internal/ui/json"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
@@ -35,6 +35,13 @@ var cmdBackup = &cobra.Command{
|
||||
Long: `
|
||||
The "backup" command creates a new snapshot and saves the files and directories
|
||||
given as the arguments.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful.
|
||||
Exit status is 1 if there was a fatal error (no snapshot created).
|
||||
Exit status is 3 if some source data could not be read (incomplete snapshot created).
|
||||
`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
if backupOptions.Host == "" {
|
||||
@@ -71,48 +78,55 @@ given as the arguments.
|
||||
|
||||
// BackupOptions bundles all options for the backup command.
|
||||
type BackupOptions struct {
|
||||
Parent string
|
||||
Force bool
|
||||
Excludes []string
|
||||
InsensitiveExcludes []string
|
||||
ExcludeFiles []string
|
||||
ExcludeOtherFS bool
|
||||
ExcludeIfPresent []string
|
||||
ExcludeCaches bool
|
||||
Stdin bool
|
||||
StdinFilename string
|
||||
Tags []string
|
||||
Host string
|
||||
FilesFrom []string
|
||||
TimeStamp string
|
||||
WithAtime bool
|
||||
IgnoreInode bool
|
||||
Parent string
|
||||
Force bool
|
||||
Excludes []string
|
||||
InsensitiveExcludes []string
|
||||
ExcludeFiles []string
|
||||
InsensitiveExcludeFiles []string
|
||||
ExcludeOtherFS bool
|
||||
ExcludeIfPresent []string
|
||||
ExcludeCaches bool
|
||||
ExcludeLargerThan string
|
||||
Stdin bool
|
||||
StdinFilename string
|
||||
Tags []string
|
||||
Host string
|
||||
FilesFrom []string
|
||||
TimeStamp string
|
||||
WithAtime bool
|
||||
IgnoreInode bool
|
||||
}
|
||||
|
||||
var backupOptions BackupOptions
|
||||
|
||||
// ErrInvalidSourceData is used to report an incomplete backup
|
||||
var ErrInvalidSourceData = errors.New("failed to read all source data during backup")
|
||||
|
||||
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.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.InsensitiveExcludes, "iexclude", nil, "same as `--exclude` but ignores the casing of filenames")
|
||||
f.StringArrayVar(&backupOptions.InsensitiveExcludes, "iexclude", nil, "same as --exclude `pattern` but ignores the casing of filenames")
|
||||
f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
|
||||
f.StringArrayVar(&backupOptions.InsensitiveExcludeFiles, "iexclude-file", nil, "same as --exclude-file but ignores casing of `file`names in patterns")
|
||||
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. See http://bford.info/cachedir/spec.html for the Cache Directory Tagging Standard`)
|
||||
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. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
|
||||
f.StringVar(&backupOptions.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
|
||||
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.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "`filename` 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.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
|
||||
f.StringVar(&backupOptions.Host, "hostname", "", "set the `hostname` for the snapshot manually")
|
||||
f.MarkDeprecated("hostname", "use --host")
|
||||
|
||||
f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from file (can be combined with file args/can be specified multiple times)")
|
||||
f.StringVar(&backupOptions.TimeStamp, "time", "", "time of the backup (ex. '2012-11-01 22:08:41') (default: now)")
|
||||
f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from `file` (can be combined with file args/can be specified multiple times)")
|
||||
f.StringVar(&backupOptions.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
|
||||
f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories")
|
||||
f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number changes when checking for modified files")
|
||||
}
|
||||
@@ -229,6 +243,14 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, t
|
||||
opts.Excludes = append(opts.Excludes, excludes...)
|
||||
}
|
||||
|
||||
if len(opts.InsensitiveExcludeFiles) > 0 {
|
||||
excludes, err := readExcludePatternsFromFiles(opts.InsensitiveExcludeFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.InsensitiveExcludes = append(opts.InsensitiveExcludes, excludes...)
|
||||
}
|
||||
|
||||
if len(opts.InsensitiveExcludes) > 0 {
|
||||
fs = append(fs, rejectByInsensitivePattern(opts.InsensitiveExcludes))
|
||||
}
|
||||
@@ -265,6 +287,14 @@ func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets
|
||||
fs = append(fs, f)
|
||||
}
|
||||
|
||||
if len(opts.ExcludeLargerThan) != 0 && !opts.Stdin {
|
||||
f, err := rejectBySize(opts.ExcludeLargerThan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fs = append(fs, f)
|
||||
}
|
||||
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
@@ -374,7 +404,7 @@ func findParentSnapshot(ctx context.Context, repo restic.Repository, opts Backup
|
||||
|
||||
// Find last snapshot to set it as parent, if not already set
|
||||
if !opts.Force && parentID == nil {
|
||||
id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, opts.Host)
|
||||
id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, []string{opts.Host})
|
||||
if err == nil {
|
||||
parentID = &id
|
||||
} else if err != restic.ErrNoSnapshotFound {
|
||||
@@ -407,7 +437,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
var t tomb.Tomb
|
||||
|
||||
if gopts.verbosity >= 2 && !gopts.JSON {
|
||||
term.Print("open repository\n")
|
||||
Verbosef("open repository\n")
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
@@ -439,7 +469,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
|
||||
var p ArchiveProgressReporter
|
||||
if gopts.JSON {
|
||||
p = jsonstatus.NewBackup(term, gopts.verbosity)
|
||||
p = json.NewBackup(term, gopts.verbosity)
|
||||
} else {
|
||||
p = ui.NewBackup(term, gopts.verbosity)
|
||||
}
|
||||
@@ -549,7 +579,11 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
arch.SelectByName = selectByNameFilter
|
||||
arch.Select = selectFilter
|
||||
arch.WithAtime = opts.WithAtime
|
||||
arch.Error = p.Error
|
||||
success := true
|
||||
arch.Error = func(item string, fi os.FileInfo, err error) error {
|
||||
success = false
|
||||
return p.Error(item, fi, err)
|
||||
}
|
||||
arch.CompleteItem = p.CompleteItem
|
||||
arch.StartFile = p.StartFile
|
||||
arch.CompleteBlob = p.CompleteBlob
|
||||
@@ -567,24 +601,6 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
ParentSnapshot: *parentSnapshotID,
|
||||
}
|
||||
|
||||
uploader := archiver.IndexUploader{
|
||||
Repository: repo,
|
||||
Start: func() {
|
||||
if !gopts.JSON {
|
||||
p.VV("uploading intermediate index")
|
||||
}
|
||||
},
|
||||
Complete: func(id restic.ID) {
|
||||
if !gopts.JSON {
|
||||
p.V("uploaded intermediate index %v", id.Str())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
t.Go(func() error {
|
||||
return uploader.Upload(gopts.ctx, t.Context(gopts.ctx), 30*time.Second)
|
||||
})
|
||||
|
||||
if !gopts.JSON {
|
||||
p.V("start backup on %v", targets)
|
||||
}
|
||||
@@ -593,19 +609,21 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||
}
|
||||
|
||||
p.Finish(id)
|
||||
if !gopts.JSON {
|
||||
p.P("snapshot %s saved\n", id.Str())
|
||||
}
|
||||
|
||||
// cleanly shutdown all running goroutines
|
||||
t.Kill(nil)
|
||||
|
||||
// let's see if one returned an error
|
||||
err = t.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
// Report finished execution
|
||||
p.Finish(id)
|
||||
if !gopts.JSON {
|
||||
p.P("snapshot %s saved\n", id.Str())
|
||||
}
|
||||
if !success {
|
||||
return ErrInvalidSourceData
|
||||
}
|
||||
|
||||
return nil
|
||||
// Return error if any
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ var cmdCache = &cobra.Command{
|
||||
Short: "Operate on local cache directories",
|
||||
Long: `
|
||||
The "cache" command allows listing and cleaning local cache directories.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -2,8 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -18,6 +16,11 @@ var cmdCat = &cobra.Command{
|
||||
Short: "Print internal objects to stdout",
|
||||
Long: `
|
||||
The "cat" command is used to print internal objects to stdout.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -71,7 +74,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(buf))
|
||||
Println(string(buf))
|
||||
return nil
|
||||
case "index":
|
||||
buf, err := repo.LoadAndDecrypt(gopts.ctx, nil, restic.IndexFile, id)
|
||||
@@ -79,9 +82,8 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = os.Stdout.Write(append(buf, '\n'))
|
||||
return err
|
||||
|
||||
Println(string(buf))
|
||||
return nil
|
||||
case "snapshot":
|
||||
sn := &restic.Snapshot{}
|
||||
err = repo.LoadJSONUnpacked(gopts.ctx, restic.SnapshotFile, id, sn)
|
||||
@@ -94,8 +96,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(buf))
|
||||
|
||||
Println(string(buf))
|
||||
return nil
|
||||
case "key":
|
||||
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
|
||||
@@ -115,7 +116,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(buf))
|
||||
Println(string(buf))
|
||||
return nil
|
||||
case "masterkey":
|
||||
buf, err := json.MarshalIndent(repo.Key(), "", " ")
|
||||
@@ -123,7 +124,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(buf))
|
||||
Println(string(buf))
|
||||
return nil
|
||||
case "lock":
|
||||
lock, err := restic.LoadLock(gopts.ctx, repo, id)
|
||||
@@ -136,8 +137,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(string(buf))
|
||||
|
||||
Println(string(buf))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
|
||||
switch tpe {
|
||||
case "pack":
|
||||
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
|
||||
h := restic.Handle{Type: restic.PackFile, Name: id.String()}
|
||||
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -157,28 +157,24 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
|
||||
hash := restic.Hash(buf)
|
||||
if !hash.Equal(id) {
|
||||
fmt.Fprintf(stderr, "Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String())
|
||||
Warnf("Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String())
|
||||
}
|
||||
|
||||
_, err = os.Stdout.Write(buf)
|
||||
_, err = globalOptions.stdout.Write(buf)
|
||||
return err
|
||||
|
||||
case "blob":
|
||||
for _, t := range []restic.BlobType{restic.DataBlob, restic.TreeBlob} {
|
||||
list, found := repo.Index().Lookup(id, t)
|
||||
if !found {
|
||||
if !repo.Index().Has(id, t) {
|
||||
continue
|
||||
}
|
||||
blob := list[0]
|
||||
|
||||
buf := make([]byte, blob.Length)
|
||||
n, err := repo.LoadBlob(gopts.ctx, t, id, buf)
|
||||
buf, err := repo.LoadBlob(gopts.ctx, t, id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
_, err = os.Stdout.Write(buf)
|
||||
_, err = globalOptions.stdout.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -25,6 +23,11 @@ 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.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -95,36 +98,6 @@ func stringToIntSlice(param string) (split []uint, err error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
|
||||
if gopts.Quiet {
|
||||
return nil
|
||||
}
|
||||
|
||||
readProgress := restic.NewProgress()
|
||||
|
||||
readProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||
status := fmt.Sprintf("[%s] %s %d / %d items",
|
||||
formatDuration(d),
|
||||
formatPercent(s.Blobs, todo.Blobs),
|
||||
s.Blobs, todo.Blobs)
|
||||
|
||||
if w := stdoutTerminalWidth(); w > 0 {
|
||||
if len(status) > w {
|
||||
max := w - len(status) - 4
|
||||
status = status[:max] + "... "
|
||||
}
|
||||
}
|
||||
|
||||
PrintProgress("%s", status)
|
||||
}
|
||||
|
||||
readProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||
fmt.Printf("\nduration: %s\n", formatDuration(d))
|
||||
}
|
||||
|
||||
return readProgress
|
||||
}
|
||||
|
||||
// prepareCheckCache configures a special cache directory for check.
|
||||
//
|
||||
// * if --with-cache is specified, the default cache is used
|
||||
@@ -230,7 +203,7 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
continue
|
||||
}
|
||||
errorsFound = true
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
Warnf("%v\n", err)
|
||||
}
|
||||
|
||||
if orphanedPacks > 0 {
|
||||
@@ -244,18 +217,18 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
for err := range errChan {
|
||||
errorsFound = true
|
||||
if e, ok := err.(checker.TreeError); ok {
|
||||
fmt.Fprintf(os.Stderr, "error for tree %v:\n", e.ID.Str())
|
||||
Warnf("error for tree %v:\n", e.ID.Str())
|
||||
for _, treeErr := range e.Errors {
|
||||
fmt.Fprintf(os.Stderr, " %v\n", treeErr)
|
||||
Warnf(" %v\n", treeErr)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
Warnf("error: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.CheckUnused {
|
||||
for _, id := range chkr.UnusedBlobs() {
|
||||
Verbosef("unused blob %v\n", id.Str())
|
||||
Verbosef("unused blob %v\n", id)
|
||||
errorsFound = true
|
||||
}
|
||||
}
|
||||
@@ -277,14 +250,14 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
Verbosef("read all data\n")
|
||||
}
|
||||
|
||||
p := newReadProgress(gopts, restic.Stat{Blobs: packCount})
|
||||
p := newProgressMax(!gopts.Quiet, packCount, "packs")
|
||||
errChan := make(chan error)
|
||||
|
||||
go chkr.ReadPacks(gopts.ctx, packs, p, errChan)
|
||||
|
||||
for err := range errChan {
|
||||
errorsFound = true
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
Warnf("%v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
233
cmd/restic/cmd_copy.go
Normal file
233
cmd/restic/cmd_copy.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdCopy = &cobra.Command{
|
||||
Use: "copy [flags] [snapshotID ...]",
|
||||
Short: "Copy snapshots from one repository to another",
|
||||
Long: `
|
||||
The "copy" command copies one or more snapshots from one repository to another
|
||||
repository. Note that this will have to read (download) and write (upload) the
|
||||
entire snapshot(s) due to the different encryption keys on the source and
|
||||
destination, and that transferred files are not re-chunked, which may break
|
||||
their deduplication. This can be mitigated by the "--copy-chunker-params"
|
||||
option when initializing a new destination repository using the "init" command.
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runCopy(copyOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// CopyOptions bundles all options for the copy command.
|
||||
type CopyOptions struct {
|
||||
secondaryRepoOptions
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
}
|
||||
|
||||
var copyOptions CopyOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdCopy)
|
||||
|
||||
f := cmdCopy.Flags()
|
||||
initSecondaryRepoOptions(f, ©Options.secondaryRepoOptions, "destination", "to copy snapshots to")
|
||||
f.StringArrayVarP(©Options.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
||||
f.Var(©Options.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given")
|
||||
f.StringArrayVar(©Options.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
|
||||
}
|
||||
|
||||
func runCopy(opts CopyOptions, gopts GlobalOptions, args []string) error {
|
||||
dstGopts, err := fillSecondaryGlobalOpts(opts.secondaryRepoOptions, gopts, "destination")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
|
||||
srcRepo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstRepo, err := OpenRepository(dstGopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcLock, err := lockRepo(srcRepo)
|
||||
defer unlockRepo(srcLock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstLock, err := lockRepo(dstRepo)
|
||||
defer unlockRepo(dstLock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
debug.Log("Loading source index")
|
||||
if err := srcRepo.LoadIndex(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
debug.Log("Loading destination index")
|
||||
if err := dstRepo.LoadIndex(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot)
|
||||
for sn := range FindFilteredSnapshots(ctx, dstRepo, opts.Hosts, opts.Tags, opts.Paths, nil) {
|
||||
if sn.Original != nil && !sn.Original.IsNull() {
|
||||
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
||||
}
|
||||
// also consider identical snapshot copies
|
||||
dstSnapshotByOriginal[*sn.ID()] = append(dstSnapshotByOriginal[*sn.ID()], sn)
|
||||
}
|
||||
|
||||
cloner := &treeCloner{
|
||||
srcRepo: srcRepo,
|
||||
dstRepo: dstRepo,
|
||||
visitedTrees: restic.NewIDSet(),
|
||||
buf: nil,
|
||||
}
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, srcRepo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||
Verbosef("\nsnapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time)
|
||||
|
||||
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
|
||||
srcOriginal := *sn.ID()
|
||||
if sn.Original != nil {
|
||||
srcOriginal = *sn.Original
|
||||
}
|
||||
if originalSns, ok := dstSnapshotByOriginal[srcOriginal]; ok {
|
||||
isCopy := false
|
||||
for _, originalSn := range originalSns {
|
||||
if similarSnapshots(originalSn, sn) {
|
||||
Verbosef("skipping source snapshot %s, was already copied to snapshot %s\n", sn.ID().Str(), originalSn.ID().Str())
|
||||
isCopy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isCopy {
|
||||
continue
|
||||
}
|
||||
}
|
||||
Verbosef(" copy started, this may take a while...\n")
|
||||
|
||||
if err := cloner.copyTree(ctx, *sn.Tree); err != nil {
|
||||
return err
|
||||
}
|
||||
debug.Log("tree copied")
|
||||
|
||||
if err = dstRepo.Flush(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
debug.Log("flushed packs and saved index")
|
||||
|
||||
// save snapshot
|
||||
sn.Parent = nil // Parent does not have relevance in the new repo.
|
||||
// Use Original as a persistent snapshot ID
|
||||
if sn.Original == nil {
|
||||
sn.Original = sn.ID()
|
||||
}
|
||||
newID, err := dstRepo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Verbosef("snapshot %s saved\n", newID.Str())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
|
||||
// everything except Parent and Original must match
|
||||
if !sna.Time.Equal(snb.Time) || !sna.Tree.Equal(*snb.Tree) || sna.Hostname != snb.Hostname ||
|
||||
sna.Username != snb.Username || sna.UID != snb.UID || sna.GID != snb.GID ||
|
||||
len(sna.Paths) != len(snb.Paths) || len(sna.Excludes) != len(snb.Excludes) ||
|
||||
len(sna.Tags) != len(snb.Tags) {
|
||||
return false
|
||||
}
|
||||
if !sna.HasPaths(snb.Paths) || !sna.HasTags(snb.Tags) {
|
||||
return false
|
||||
}
|
||||
for i, a := range sna.Excludes {
|
||||
if a != snb.Excludes[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type treeCloner struct {
|
||||
srcRepo restic.Repository
|
||||
dstRepo restic.Repository
|
||||
visitedTrees restic.IDSet
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (t *treeCloner) copyTree(ctx context.Context, treeID restic.ID) error {
|
||||
// We have already processed this tree
|
||||
if t.visitedTrees.Has(treeID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
tree, err := t.srcRepo.LoadTree(ctx, treeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("LoadTree(%v) returned error %v", treeID.Str(), err)
|
||||
}
|
||||
t.visitedTrees.Insert(treeID)
|
||||
|
||||
// Do we already have this tree blob?
|
||||
if !t.dstRepo.Index().Has(treeID, restic.TreeBlob) {
|
||||
newTreeID, err := t.dstRepo.SaveTree(ctx, tree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SaveTree(%v) returned error %v", treeID.Str(), err)
|
||||
}
|
||||
// Assurance only.
|
||||
if newTreeID != treeID {
|
||||
return fmt.Errorf("SaveTree(%v) returned unexpected id %s", treeID.Str(), newTreeID.Str())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: parellize this stuff, likely only needed inside a tree.
|
||||
|
||||
for _, entry := range tree.Nodes {
|
||||
// If it is a directory, recurse
|
||||
if entry.Type == "dir" && entry.Subtree != nil {
|
||||
if err := t.copyTree(ctx, *entry.Subtree); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Copy the blobs for this file.
|
||||
for _, blobID := range entry.Content {
|
||||
// Do we already have this data blob?
|
||||
if t.dstRepo.Index().Has(blobID, restic.DataBlob) {
|
||||
continue
|
||||
}
|
||||
debug.Log("Copying blob %s\n", blobID.Str())
|
||||
t.buf, err = t.srcRepo.LoadBlob(ctx, restic.DataBlob, blobID, t.buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("LoadBlob(%v) returned error %v", blobID, err)
|
||||
}
|
||||
|
||||
_, _, err = t.dstRepo.SaveBlob(ctx, restic.DataBlob, t.buf, blobID, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SaveBlob(%v) returned error %v", blobID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -27,7 +26,13 @@ var cmdDebugDump = &cobra.Command{
|
||||
Short: "Dump data structures",
|
||||
Long: `
|
||||
The "dump" command dumps data structures from the repository as JSON objects. It
|
||||
is used for debugging purposes only.`,
|
||||
is used for debugging purposes only.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDebugDump(globalOptions, args)
|
||||
@@ -79,12 +84,12 @@ type Blob struct {
|
||||
|
||||
func printPacks(repo *repository.Repository, wr io.Writer) error {
|
||||
|
||||
return repo.List(context.TODO(), restic.DataFile, func(id restic.ID, size int64) error {
|
||||
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
|
||||
return repo.List(context.TODO(), restic.PackFile, func(id restic.ID, size int64) error {
|
||||
h := restic.Handle{Type: restic.PackFile, Name: id.String()}
|
||||
|
||||
blobs, err := pack.List(repo.Key(), restic.ReaderAt(repo.Backend(), h), size)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error for pack %v: %v\n", id.Str(), err)
|
||||
Warnf("error for pack %v: %v\n", id.Str(), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,22 +106,20 @@ func printPacks(repo *repository.Repository, wr io.Writer) error {
|
||||
}
|
||||
}
|
||||
|
||||
return prettyPrintJSON(os.Stdout, p)
|
||||
return prettyPrintJSON(wr, p)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dumpIndexes(repo restic.Repository) error {
|
||||
func dumpIndexes(repo restic.Repository, wr io.Writer) error {
|
||||
return repo.List(context.TODO(), restic.IndexFile, func(id restic.ID, size int64) error {
|
||||
fmt.Printf("index_id: %v\n", id)
|
||||
Printf("index_id: %v\n", id)
|
||||
|
||||
idx, err := repository.LoadIndex(context.TODO(), repo, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return idx.Dump(os.Stdout)
|
||||
return idx.Dump(wr)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,29 +141,24 @@ func runDebugDump(gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(gopts.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpe := args[0]
|
||||
|
||||
switch tpe {
|
||||
case "indexes":
|
||||
return dumpIndexes(repo)
|
||||
return dumpIndexes(repo, gopts.stdout)
|
||||
case "snapshots":
|
||||
return debugPrintSnapshots(repo, os.Stdout)
|
||||
return debugPrintSnapshots(repo, gopts.stdout)
|
||||
case "packs":
|
||||
return printPacks(repo, os.Stdout)
|
||||
return printPacks(repo, gopts.stdout)
|
||||
case "all":
|
||||
fmt.Printf("snapshots:\n")
|
||||
err := debugPrintSnapshots(repo, os.Stdout)
|
||||
Printf("snapshots:\n")
|
||||
err := debugPrintSnapshots(repo, gopts.stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("\nindexes:\n")
|
||||
err = dumpIndexes(repo)
|
||||
Printf("\nindexes:\n")
|
||||
err = dumpIndexes(repo, gopts.stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
var cmdDiff = &cobra.Command{
|
||||
Use: "diff snapshot-ID snapshot-ID",
|
||||
Use: "diff [flags] snapshot-ID snapshot-ID",
|
||||
Short: "Show differences between two snapshots",
|
||||
Long: `
|
||||
The "diff" command shows differences from the first to the second snapshot. The
|
||||
@@ -26,6 +26,11 @@ directory:
|
||||
* U The metadata (access mode, timestamps, ...) for the item was updated
|
||||
* M The file's content was modified
|
||||
* T The type was changed, e.g. a file was made a symlink
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -66,7 +71,7 @@ type Comparer struct {
|
||||
type DiffStat struct {
|
||||
Files, Dirs, Others int
|
||||
DataBlobs, TreeBlobs int
|
||||
Bytes int
|
||||
Bytes uint64
|
||||
}
|
||||
|
||||
// Add adds stats information for node to s.
|
||||
@@ -111,10 +116,10 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
||||
|
||||
// DiffStats collects the differences between two snapshots.
|
||||
type DiffStats struct {
|
||||
ChangedFiles int
|
||||
Added DiffStat
|
||||
Removed DiffStat
|
||||
BlobsBefore, BlobsAfter restic.BlobSet
|
||||
ChangedFiles int
|
||||
Added DiffStat
|
||||
Removed DiffStat
|
||||
BlobsBefore, BlobsAfter, BlobsCommon restic.BlobSet
|
||||
}
|
||||
|
||||
// NewDiffStats creates new stats for a diff run.
|
||||
@@ -122,6 +127,7 @@ func NewDiffStats() *DiffStats {
|
||||
return &DiffStats{
|
||||
BlobsBefore: restic.NewBlobSet(),
|
||||
BlobsAfter: restic.NewBlobSet(),
|
||||
BlobsCommon: restic.NewBlobSet(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +147,7 @@ func updateBlobs(repo restic.Repository, blobs restic.BlobSet, stats *DiffStat)
|
||||
continue
|
||||
}
|
||||
|
||||
stats.Bytes += int(size)
|
||||
stats.Bytes += uint64(size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +178,27 @@ func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, b
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id restic.ID) error {
|
||||
debug.Log("print tree %v", id)
|
||||
tree, err := c.repo.LoadTree(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range tree.Nodes {
|
||||
addBlobs(blobs, node)
|
||||
|
||||
if node.Type == "dir" {
|
||||
err := c.collectDir(ctx, blobs, *node.Subtree)
|
||||
if err != nil {
|
||||
Warnf("error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[string]*restic.Node, uniqueNames []string) {
|
||||
names := make(map[string]struct{})
|
||||
tree1Nodes = make(map[string]*restic.Node)
|
||||
@@ -191,7 +218,7 @@ func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[stri
|
||||
uniqueNames = append(uniqueNames, name)
|
||||
}
|
||||
|
||||
sort.Sort(sort.StringSlice(uniqueNames))
|
||||
sort.Strings(uniqueNames)
|
||||
return tree1Nodes, tree2Nodes, uniqueNames
|
||||
}
|
||||
|
||||
@@ -243,7 +270,12 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStats, prefix string
|
||||
}
|
||||
|
||||
if node1.Type == "dir" && node2.Type == "dir" {
|
||||
err := c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
||||
var err error
|
||||
if (*node1.Subtree).Equal(*node2.Subtree) {
|
||||
err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree)
|
||||
} else {
|
||||
err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
||||
}
|
||||
if err != nil {
|
||||
Warnf("error: %v\n", err)
|
||||
}
|
||||
@@ -340,8 +372,8 @@ func runDiff(opts DiffOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
|
||||
updateBlobs(repo, stats.BlobsBefore.Sub(both), &stats.Removed)
|
||||
updateBlobs(repo, stats.BlobsAfter.Sub(both), &stats.Added)
|
||||
updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed)
|
||||
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added)
|
||||
|
||||
Printf("\n")
|
||||
Printf("Files: %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/dump"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -22,11 +19,18 @@ 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 "dump" command extracts files from a snapshot from the repository. If a
|
||||
single file is selected, it prints its contents to stdout. Folders are output
|
||||
as a tar file containing the contents of the specified folder. Pass "/" as
|
||||
file name to dump the whole snapshot as a tar file.
|
||||
|
||||
The special snapshot "latest" can be used to use the latest snapshot in the
|
||||
repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -36,7 +40,7 @@ repository.
|
||||
|
||||
// DumpOptions collects all options for the dump command.
|
||||
type DumpOptions struct {
|
||||
Host string
|
||||
Hosts []string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
}
|
||||
@@ -47,24 +51,21 @@ 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.StringArrayVarP(&dumpOptions.Hosts, "host", "H", nil, `only consider snapshots for this host when the snapshot ID is "latest" (can be specified multiple times)`)
|
||||
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(p string) []string {
|
||||
d, f := path.Split(p)
|
||||
if d == "" {
|
||||
if d == "" || d == "/" {
|
||||
return []string{f}
|
||||
}
|
||||
if d == "/" {
|
||||
return []string{d}
|
||||
}
|
||||
s := splitPath(path.Clean(d))
|
||||
s := splitPath(path.Join("/", d))
|
||||
return append(s, f)
|
||||
}
|
||||
|
||||
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string, pathToPrint string) error {
|
||||
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")
|
||||
@@ -76,24 +77,42 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repositor
|
||||
if l == 0 {
|
||||
return fmt.Errorf("empty path components")
|
||||
}
|
||||
|
||||
// If we print / we need to assume that there are multiple nodes at that
|
||||
// level in the tree.
|
||||
if pathComponents[0] == "" {
|
||||
if err := checkStdoutTar(); err != nil {
|
||||
return err
|
||||
}
|
||||
return dump.WriteTar(ctx, repo, tree, "/", os.Stdout)
|
||||
}
|
||||
|
||||
item := filepath.Join(prefix, pathComponents[0])
|
||||
for _, node := range tree.Nodes {
|
||||
if node.Name == pathComponents[0] || pathComponents[0] == "/" {
|
||||
// If dumping something in the highest level it will just take the
|
||||
// first item it finds and dump that according to the switch case below.
|
||||
if node.Name == pathComponents[0] {
|
||||
switch {
|
||||
case l == 1 && node.Type == "file":
|
||||
return getNodeData(ctx, os.Stdout, repo, node)
|
||||
case l > 1 && node.Type == "dir":
|
||||
case l == 1 && dump.IsFile(node):
|
||||
return dump.GetNodeData(ctx, os.Stdout, repo, node)
|
||||
case l > 1 && dump.IsDir(node):
|
||||
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:], pathToPrint)
|
||||
case node.Type == "dir":
|
||||
node.Path = pathToPrint
|
||||
return tarTree(ctx, repo, node, pathToPrint)
|
||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:])
|
||||
case dump.IsDir(node):
|
||||
if err := checkStdoutTar(); err != nil {
|
||||
return err
|
||||
}
|
||||
subtree, err := repo.LoadTree(ctx, *node.Subtree)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dump.WriteTar(ctx, repo, subtree, item, os.Stdout)
|
||||
case l > 1:
|
||||
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
||||
case node.Type != "file":
|
||||
case !dump.IsFile(node):
|
||||
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
||||
}
|
||||
}
|
||||
@@ -136,9 +155,9 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
||||
var id restic.ID
|
||||
|
||||
if snapshotIDString == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host)
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Hosts)
|
||||
if err != nil {
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Hosts:%v", err, opts.Paths, opts.Hosts)
|
||||
}
|
||||
} else {
|
||||
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||
@@ -157,7 +176,7 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
||||
Exitf(2, "loading tree for snapshot %q failed: %v", snapshotIDString, err)
|
||||
}
|
||||
|
||||
err = printFromTree(ctx, tree, repo, "", splittedPath, pathToPrint)
|
||||
err = printFromTree(ctx, tree, repo, "/", splittedPath)
|
||||
if err != nil {
|
||||
Exitf(2, "cannot dump file: %v", err)
|
||||
}
|
||||
@@ -165,135 +184,9 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getNodeData(ctx context.Context, output io.Writer, repo restic.Repository, node *restic.Node) error {
|
||||
var buf []byte
|
||||
for _, id := range node.Content {
|
||||
|
||||
size, found := repo.LookupBlobSize(id, restic.DataBlob)
|
||||
if !found {
|
||||
return errors.Errorf("id %v not found in repository", id)
|
||||
}
|
||||
|
||||
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 = output.Write(buf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Write")
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tarTree(ctx context.Context, repo restic.Repository, rootNode *restic.Node, rootPath string) error {
|
||||
|
||||
func checkStdoutTar() error {
|
||||
if stdoutIsTerminal() {
|
||||
return fmt.Errorf("stdout is the terminal, please redirect output")
|
||||
}
|
||||
|
||||
tw := tar.NewWriter(os.Stdout)
|
||||
defer tw.Close()
|
||||
|
||||
// If we want to dump "/" we'll need to add the name of the first node, too
|
||||
// as it would get lost otherwise.
|
||||
if rootNode.Path == "/" {
|
||||
rootNode.Path = path.Join(rootNode.Path, rootNode.Name)
|
||||
rootPath = rootNode.Path
|
||||
}
|
||||
|
||||
// we know that rootNode is a folder and walker.Walk will already process
|
||||
// the next node, so we have to tar this one first, too
|
||||
if err := tarNode(ctx, tw, rootNode, repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := walker.Walk(ctx, repo, *rootNode.Subtree, nil, func(_ restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if node == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
node.Path = path.Join(rootPath, nodepath)
|
||||
|
||||
if node.Type == "file" || node.Type == "symlink" || node.Type == "dir" {
|
||||
err := tarNode(ctx, tw, node, repo)
|
||||
if err != err {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func tarNode(ctx context.Context, tw *tar.Writer, node *restic.Node, repo restic.Repository) error {
|
||||
|
||||
header := &tar.Header{
|
||||
Name: node.Path,
|
||||
Size: int64(node.Size),
|
||||
Mode: int64(node.Mode),
|
||||
Uid: int(node.UID),
|
||||
Gid: int(node.GID),
|
||||
ModTime: node.ModTime,
|
||||
AccessTime: node.AccessTime,
|
||||
ChangeTime: node.ChangeTime,
|
||||
PAXRecords: parseXattrs(node.ExtendedAttributes),
|
||||
}
|
||||
|
||||
if node.Type == "symlink" {
|
||||
header.Typeflag = tar.TypeSymlink
|
||||
header.Linkname = node.LinkTarget
|
||||
}
|
||||
|
||||
if node.Type == "dir" {
|
||||
header.Typeflag = tar.TypeDir
|
||||
}
|
||||
|
||||
err := tw.WriteHeader(header)
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "TarHeader ")
|
||||
}
|
||||
|
||||
return getNodeData(ctx, tw, repo, node)
|
||||
|
||||
}
|
||||
|
||||
func parseXattrs(xattrs []restic.ExtendedAttribute) map[string]string {
|
||||
tmpMap := make(map[string]string)
|
||||
|
||||
for _, attr := range xattrs {
|
||||
attrString := string(attr.Value)
|
||||
|
||||
if strings.HasPrefix(attr.Name, "system.posix_acl_") {
|
||||
na := acl{}
|
||||
na.decode(attr.Value)
|
||||
|
||||
if na.String() != "" {
|
||||
if strings.Contains(attr.Name, "system.posix_acl_access") {
|
||||
tmpMap["SCHILY.acl.access"] = na.String()
|
||||
} else if strings.Contains(attr.Name, "system.posix_acl_default") {
|
||||
tmpMap["SCHILY.acl.default"] = na.String()
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
tmpMap["SCHILY.xattr."+attr.Name] = attrString
|
||||
}
|
||||
}
|
||||
|
||||
return tmpMap
|
||||
return nil
|
||||
}
|
||||
|
||||
27
cmd/restic/cmd_dump_test.go
Normal file
27
cmd/restic/cmd_dump_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func TestDumpSplitPath(t *testing.T) {
|
||||
testPaths := []struct {
|
||||
path string
|
||||
result []string
|
||||
}{
|
||||
{"", []string{""}},
|
||||
{"test", []string{"test"}},
|
||||
{"test/dir", []string{"test", "dir"}},
|
||||
{"test/dir/sub", []string{"test", "dir", "sub"}},
|
||||
{"/", []string{""}},
|
||||
{"/test", []string{"test"}},
|
||||
{"/test/dir", []string{"test", "dir"}},
|
||||
{"/test/dir/sub", []string{"test", "dir", "sub"}},
|
||||
}
|
||||
for _, path := range testPaths {
|
||||
parts := splitPath(path.path)
|
||||
rtest.Equals(t, path.result, parts)
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,13 @@ restic find --json "*.yml" "*.json"
|
||||
restic find --json --blob 420f620f b46ebe8a ddd38656
|
||||
restic find --show-pack-id --blob 420f620f
|
||||
restic find --tree 577c2bc9 f81f2e22 a62827a9
|
||||
restic find --pack 025c1d06`,
|
||||
restic find --pack 025c1d06
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runFind(findOptions, globalOptions, args)
|
||||
@@ -45,7 +51,7 @@ type FindOptions struct {
|
||||
PackID, ShowPackID bool
|
||||
CaseInsensitive bool
|
||||
ListLong bool
|
||||
Host string
|
||||
Hosts []string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
}
|
||||
@@ -66,7 +72,7 @@ func init() {
|
||||
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.StringArrayVarP(&findOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
||||
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")
|
||||
}
|
||||
@@ -150,7 +156,7 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
||||
if s.hits > 0 {
|
||||
Printf(",")
|
||||
}
|
||||
Printf(string(b))
|
||||
Print(string(b))
|
||||
s.hits++
|
||||
}
|
||||
|
||||
@@ -160,9 +166,9 @@ func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) {
|
||||
Verbosef("\n")
|
||||
}
|
||||
s.oldsn = s.newsn
|
||||
Verbosef("Found matching entries in snapshot %s\n", s.oldsn.ID().Str())
|
||||
Verbosef("Found matching entries in snapshot %s from %s\n", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(TimeFormat))
|
||||
}
|
||||
Printf(formatNode(path, node, s.ListLong) + "\n")
|
||||
Println(formatNode(path, node, s.ListLong))
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintPattern(path string, node *restic.Node) {
|
||||
@@ -201,7 +207,7 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *
|
||||
if s.hits > 0 {
|
||||
Printf(",")
|
||||
}
|
||||
Printf(string(b))
|
||||
Print(string(b))
|
||||
s.hits++
|
||||
}
|
||||
|
||||
@@ -264,7 +270,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
||||
|
||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
||||
|
||||
return false, walker.SkipNode
|
||||
return false, walker.ErrSkipNode
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
@@ -308,7 +314,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
||||
|
||||
if !childMayMatch {
|
||||
ignoreIfNoMatch = true
|
||||
errIfNoMatch = walker.SkipNode
|
||||
errIfNoMatch = walker.ErrSkipNode
|
||||
} else {
|
||||
ignoreIfNoMatch = false
|
||||
}
|
||||
@@ -348,7 +354,7 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
||||
|
||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
||||
|
||||
return false, walker.SkipNode
|
||||
return false, walker.ErrSkipNode
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
@@ -411,7 +417,7 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
||||
packsFound := 0
|
||||
|
||||
debug.Log("Looking for packs...")
|
||||
err := f.repo.List(ctx, restic.DataFile, func(id restic.ID, size int64) error {
|
||||
err := f.repo.List(ctx, restic.PackFile, func(id restic.ID, size int64) error {
|
||||
if allPacksFound {
|
||||
return nil
|
||||
}
|
||||
@@ -459,8 +465,8 @@ func (f *Finder) findObjectPack(ctx context.Context, id string, t restic.BlobTyp
|
||||
return
|
||||
}
|
||||
|
||||
blobs, found := idx.Lookup(rid, t)
|
||||
if !found {
|
||||
blobs := idx.Lookup(rid, t)
|
||||
if len(blobs) == 0 {
|
||||
Printf("Object %s not found in the index\n", rid.Str())
|
||||
return
|
||||
}
|
||||
@@ -561,7 +567,7 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
||||
f.packsToBlobs(ctx, []string{f.pat.pattern[0]}) // TODO: support multiple packs
|
||||
}
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, opts.Snapshots) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, opts.Snapshots) {
|
||||
if f.blobIDs != nil || f.treeIDs != nil {
|
||||
if err = f.findIDs(ctx, sn); err != nil && err.Error() != "OK" {
|
||||
return err
|
||||
|
||||
@@ -16,7 +16,13 @@ var cmdForget = &cobra.Command{
|
||||
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. `,
|
||||
data after 'forget' was run successfully, see the 'prune' command.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runForget(forgetOptions, globalOptions, args)
|
||||
@@ -34,7 +40,7 @@ type ForgetOptions struct {
|
||||
Within restic.Duration
|
||||
KeepTags restic.TagLists
|
||||
|
||||
Host string
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Compact bool
|
||||
@@ -60,8 +66,8 @@ func init() {
|
||||
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||
|
||||
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
|
||||
f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
|
||||
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname`")
|
||||
f.StringArrayVar(&forgetOptions.Hosts, "host", nil, "only consider snapshots with the given `host` (can be specified multiple times)")
|
||||
f.StringArrayVar(&forgetOptions.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)")
|
||||
f.MarkDeprecated("hostname", "use --host")
|
||||
|
||||
f.Var(&forgetOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")
|
||||
@@ -88,34 +94,22 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
removeSnapshots := 0
|
||||
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
|
||||
var snapshots restic.Snapshots
|
||||
removeSnIDs := restic.NewIDSet()
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
|
||||
var jsonGroups []*ForgetGroup
|
||||
|
||||
if len(args) > 0 {
|
||||
// When explicit snapshots args are given, remove them immediately.
|
||||
for _, sn := range snapshots {
|
||||
if !opts.DryRun {
|
||||
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
||||
if err = repo.Backend().Remove(gopts.ctx, h); err != nil {
|
||||
return err
|
||||
}
|
||||
if !gopts.JSON {
|
||||
Verbosef("removed snapshot %v\n", sn.ID().Str())
|
||||
}
|
||||
removeSnapshots++
|
||||
} else {
|
||||
if !gopts.JSON {
|
||||
Verbosef("would have removed snapshot %v\n", sn.ID().Str())
|
||||
}
|
||||
}
|
||||
removeSnIDs.Insert(*sn.ID())
|
||||
}
|
||||
} else {
|
||||
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
||||
@@ -145,8 +139,6 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||
Verbosef("Applying Policy: %v\n", policy)
|
||||
}
|
||||
|
||||
var jsonGroups []*ForgetGroup
|
||||
|
||||
for k, snapshotGroup := range snapshotGroups {
|
||||
if gopts.Verbose >= 1 && !gopts.JSON {
|
||||
err = PrintSnapshotGroupHeader(gopts.stdout, k)
|
||||
@@ -185,37 +177,37 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||
|
||||
jsonGroups = append(jsonGroups, &fg)
|
||||
|
||||
removeSnapshots += len(remove)
|
||||
|
||||
if !opts.DryRun {
|
||||
for _, sn := range remove {
|
||||
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
||||
err = repo.Backend().Remove(gopts.ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gopts.JSON {
|
||||
err = printJSONForget(gopts.stdout, jsonGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
for _, sn := range remove {
|
||||
removeSnIDs.Insert(*sn.ID())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if removeSnapshots > 0 && opts.Prune {
|
||||
if !gopts.JSON {
|
||||
Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
|
||||
}
|
||||
if len(removeSnIDs) > 0 {
|
||||
if !opts.DryRun {
|
||||
return pruneRepository(gopts, repo)
|
||||
err := DeleteFilesChecked(gopts, repo, removeSnIDs, restic.SnapshotFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if !gopts.JSON {
|
||||
Printf("Would have removed the following snapshots:\n%v\n\n", removeSnIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gopts.JSON && len(jsonGroups) > 0 {
|
||||
err = printJSONForget(gopts.stdout, jsonGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(removeSnIDs) > 0 && opts.Prune && !opts.DryRun {
|
||||
return pruneRepository(gopts, repo)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,16 @@ import (
|
||||
)
|
||||
|
||||
var cmdGenerate = &cobra.Command{
|
||||
Use: "generate [command]",
|
||||
Use: "generate [flags]",
|
||||
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).
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: runGenerate,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/restic/chunker"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
|
||||
@@ -12,22 +13,44 @@ var cmdInit = &cobra.Command{
|
||||
Short: "Initialize a new repository",
|
||||
Long: `
|
||||
The "init" command initializes a new repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInit(globalOptions, args)
|
||||
return runInit(initOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdInit)
|
||||
// InitOptions bundles all options for the init command.
|
||||
type InitOptions struct {
|
||||
secondaryRepoOptions
|
||||
CopyChunkerParameters bool
|
||||
}
|
||||
|
||||
func runInit(gopts GlobalOptions, args []string) error {
|
||||
var initOptions InitOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdInit)
|
||||
|
||||
f := cmdInit.Flags()
|
||||
initSecondaryRepoOptions(f, &initOptions.secondaryRepoOptions, "secondary", "to copy chunker parameters from")
|
||||
f.BoolVar(&initOptions.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
|
||||
}
|
||||
|
||||
func runInit(opts InitOptions, gopts GlobalOptions, args []string) error {
|
||||
if gopts.Repo == "" {
|
||||
return errors.Fatal("Please specify repository location (-r)")
|
||||
}
|
||||
|
||||
chunkerPolynomial, err := maybeReadChunkerPolynomial(opts, gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
be, err := create(gopts.Repo, gopts.extended)
|
||||
if err != nil {
|
||||
return errors.Fatalf("create repository at %s failed: %v\n", gopts.Repo, err)
|
||||
@@ -42,7 +65,7 @@ func runInit(gopts GlobalOptions, args []string) error {
|
||||
|
||||
s := repository.New(be)
|
||||
|
||||
err = s.Init(gopts.ctx, gopts.password)
|
||||
err = s.Init(gopts.ctx, gopts.password, chunkerPolynomial)
|
||||
if err != nil {
|
||||
return errors.Fatalf("create key in repository at %s failed: %v\n", gopts.Repo, err)
|
||||
}
|
||||
@@ -55,3 +78,25 @@ func runInit(gopts GlobalOptions, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeReadChunkerPolynomial(opts InitOptions, gopts GlobalOptions) (*chunker.Pol, error) {
|
||||
if opts.CopyChunkerParameters {
|
||||
otherGopts, err := fillSecondaryGlobalOpts(opts.secondaryRepoOptions, gopts, "secondary")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
otherRepo, err := OpenRepository(otherGopts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pol := otherRepo.Config().ChunkerPolynomial
|
||||
return &pol, nil
|
||||
}
|
||||
|
||||
if opts.Repo != "" {
|
||||
return nil, errors.Fatal("Secondary repository must only be specified when copying the chunker parameters")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -16,10 +16,15 @@ import (
|
||||
)
|
||||
|
||||
var cmdKey = &cobra.Command{
|
||||
Use: "key [list|add|remove|passwd] [ID]",
|
||||
Use: "key [flags] [list|add|remove|passwd] [ID]",
|
||||
Short: "Manage keys (passwords)",
|
||||
Long: `
|
||||
The "key" command manages keys (passwords) for accessing the repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -27,13 +32,19 @@ The "key" command manages keys (passwords) for accessing the repository.
|
||||
},
|
||||
}
|
||||
|
||||
var newPasswordFile string
|
||||
var (
|
||||
newPasswordFile string
|
||||
keyUsername string
|
||||
keyHostname string
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdKey)
|
||||
|
||||
flags := cmdKey.Flags()
|
||||
flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "the file from which to load a new password")
|
||||
flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "`file` from which to read the new password")
|
||||
flags.StringVarP(&keyUsername, "user", "", "", "the username for new keys")
|
||||
flags.StringVarP(&keyHostname, "host", "", "", "the hostname for new keys")
|
||||
}
|
||||
|
||||
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error {
|
||||
@@ -105,7 +116,7 @@ func getNewPassword(gopts GlobalOptions) (string, error) {
|
||||
newopts.password = ""
|
||||
|
||||
return ReadPasswordTwice(newopts,
|
||||
"enter password for new key: ",
|
||||
"enter new password: ",
|
||||
"enter password again: ")
|
||||
}
|
||||
|
||||
@@ -115,7 +126,7 @@ func addKey(gopts GlobalOptions, repo *repository.Repository) error {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := repository.AddKey(gopts.ctx, repo, pw, repo.Key())
|
||||
id, err := repository.AddKey(gopts.ctx, repo, pw, keyUsername, keyHostname, repo.Key())
|
||||
if err != nil {
|
||||
return errors.Fatalf("creating new key failed: %v\n", err)
|
||||
}
|
||||
@@ -146,7 +157,7 @@ func changePassword(gopts GlobalOptions, repo *repository.Repository) error {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := repository.AddKey(gopts.ctx, repo, pw, repo.Key())
|
||||
id, err := repository.AddKey(gopts.ctx, repo, pw, "", "", repo.Key())
|
||||
if err != nil {
|
||||
return errors.Fatalf("creating new key failed: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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 cmdList = &cobra.Command{
|
||||
Use: "list [blobs|packs|index|snapshots|keys|locks]",
|
||||
Use: "list [flags] [blobs|packs|index|snapshots|keys|locks]",
|
||||
Short: "List objects in the repository",
|
||||
Long: `
|
||||
The "list" command allows listing objects in the repository based on type.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -47,7 +50,7 @@ func runList(cmd *cobra.Command, opts GlobalOptions, args []string) error {
|
||||
var t restic.FileType
|
||||
switch args[0] {
|
||||
case "packs":
|
||||
t = restic.DataFile
|
||||
t = restic.PackFile
|
||||
case "index":
|
||||
t = restic.IndexFile
|
||||
case "snapshots":
|
||||
@@ -57,18 +60,17 @@ func runList(cmd *cobra.Command, opts GlobalOptions, args []string) error {
|
||||
case "locks":
|
||||
t = restic.LockFile
|
||||
case "blobs":
|
||||
idx, err := index.Load(opts.ctx, repo, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pack := range idx.Packs {
|
||||
for _, entry := range pack.Entries {
|
||||
fmt.Printf("%v %v\n", entry.Type, entry.ID)
|
||||
return repo.List(opts.ctx, restic.IndexFile, func(id restic.ID, size int64) error {
|
||||
idx, err := repository.LoadIndex(opts.ctx, repo, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for blobs := range idx.Each(opts.ctx) {
|
||||
Printf("%v %v\n", blobs.Type, blobs.ID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
default:
|
||||
return errors.Fatal("invalid type")
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
var cmdLs = &cobra.Command{
|
||||
Use: "ls [flags] [snapshotID] [dir...]",
|
||||
Use: "ls [flags] snapshotID [dir...]",
|
||||
Short: "List files in a snapshot",
|
||||
Long: `
|
||||
The "ls" command lists files and directories in a snapshot.
|
||||
@@ -33,6 +33,11 @@ will be listed. If the --recursive flag is used, then the filter
|
||||
will allow traversing into matching directories' subfolders.
|
||||
Any directory paths specified must be absolute (starting with
|
||||
a path separator); paths use the forward slash '/' as separator.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -43,7 +48,7 @@ a path separator); paths use the forward slash '/' as separator.
|
||||
// LsOptions collects all options for the ls command.
|
||||
type LsOptions struct {
|
||||
ListLong bool
|
||||
Host string
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Recursive bool
|
||||
@@ -56,7 +61,7 @@ func init() {
|
||||
|
||||
flags := cmdLs.Flags()
|
||||
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
|
||||
flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
|
||||
flags.StringArrayVarP(&lsOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
||||
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")
|
||||
flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories")
|
||||
@@ -84,8 +89,8 @@ type lsNode struct {
|
||||
}
|
||||
|
||||
func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
if len(args) == 0 && opts.Host == "" && len(opts.Tags) == 0 && len(opts.Paths) == 0 {
|
||||
return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
|
||||
if len(args) == 0 {
|
||||
return errors.Fatal("no snapshot ID specified")
|
||||
}
|
||||
|
||||
// extract any specific directories to walk
|
||||
@@ -186,7 +191,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args[:1]) {
|
||||
printSnapshot(sn)
|
||||
|
||||
err := walker.Walk(ctx, repo, *sn.Tree, nil, func(_ restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
|
||||
@@ -217,7 +222,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
// otherwise, signal the walker to not walk recursively into any
|
||||
// subdirs
|
||||
if node.Type == "dir" {
|
||||
return false, walker.SkipNode
|
||||
return false, walker.ErrSkipNode
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
|
||||
@@ -8,11 +8,16 @@ import (
|
||||
)
|
||||
|
||||
var cmdMigrate = &cobra.Command{
|
||||
Use: "migrate [name]",
|
||||
Use: "migrate [flags] [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.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// +build !netbsd
|
||||
// +build !openbsd
|
||||
// +build !solaris
|
||||
// +build !windows
|
||||
// +build darwin freebsd linux
|
||||
|
||||
package main
|
||||
|
||||
@@ -44,6 +41,11 @@ You need to specify a sample format for exactly the following timestamp:
|
||||
|
||||
For details please see the documentation for time.Format() at:
|
||||
https://godoc.org/time#Time.Format
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -54,10 +56,9 @@ For details please see the documentation for time.Format() at:
|
||||
// MountOptions collects all options for the mount command.
|
||||
type MountOptions struct {
|
||||
OwnerRoot bool
|
||||
AllowRoot bool
|
||||
AllowOther bool
|
||||
NoDefaultPermissions bool
|
||||
Host string
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
SnapshotTemplate string
|
||||
@@ -70,11 +71,10 @@ func init() {
|
||||
|
||||
mountFlags := cmdMount.Flags()
|
||||
mountFlags.BoolVar(&mountOptions.OwnerRoot, "owner-root", false, "use 'root' as the owner of files and dirs")
|
||||
mountFlags.BoolVar(&mountOptions.AllowRoot, "allow-root", false, "allow root user to access the data in the mounted directory")
|
||||
mountFlags.BoolVar(&mountOptions.AllowOther, "allow-other", false, "allow other users to access the data in the mounted directory")
|
||||
mountFlags.BoolVar(&mountOptions.NoDefaultPermissions, "no-default-permissions", false, "for 'allow-other', ignore Unix permissions and allow users to read all snapshot files")
|
||||
|
||||
mountFlags.StringVarP(&mountOptions.Host, "host", "H", "", `only consider snapshots for this host`)
|
||||
mountFlags.StringArrayVarP(&mountOptions.Hosts, "host", "H", nil, `only consider snapshots for this host (can be specified multiple times)`)
|
||||
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`")
|
||||
|
||||
@@ -90,10 +90,12 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
if !gopts.NoLock {
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = repo.LoadIndex(gopts.ctx)
|
||||
@@ -114,10 +116,6 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
systemFuse.FSName("restic"),
|
||||
}
|
||||
|
||||
if opts.AllowRoot {
|
||||
mountOptions = append(mountOptions, systemFuse.AllowRoot())
|
||||
}
|
||||
|
||||
if opts.AllowOther {
|
||||
mountOptions = append(mountOptions, systemFuse.AllowOther())
|
||||
|
||||
@@ -138,15 +136,12 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
|
||||
cfg := fuse.Config{
|
||||
OwnerIsRoot: opts.OwnerRoot,
|
||||
Host: opts.Host,
|
||||
Hosts: opts.Hosts,
|
||||
Tags: opts.Tags,
|
||||
Paths: opts.Paths,
|
||||
SnapshotTemplate: opts.SnapshotTemplate,
|
||||
}
|
||||
root, err := fuse.NewRoot(gopts.ctx, repo, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root := fuse.NewRoot(repo, cfg)
|
||||
|
||||
Printf("Now serving the repository at %s\n", mountpoint)
|
||||
Printf("When finished, quit with Ctrl-c or umount the mountpoint.\n")
|
||||
@@ -184,7 +179,7 @@ func runMount(opts MountOptions, gopts GlobalOptions, args []string) error {
|
||||
debug.Log("running umount cleanup handler for mount at %v", mountpoint)
|
||||
err := umount(mountpoint)
|
||||
if err != nil {
|
||||
Warnf("unable to umount (maybe already umounted?): %v\n", err)
|
||||
Warnf("unable to umount (maybe already umounted or still in use?): %v\n", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -13,6 +13,11 @@ var optionsCmd = &cobra.Command{
|
||||
Short: "Print list of extended options",
|
||||
Long: `
|
||||
The "options" command prints a list of extended options.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
Hidden: true,
|
||||
DisableAutoGenTag: true,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/index"
|
||||
@@ -19,6 +16,11 @@ var cmdPrune = &cobra.Command{
|
||||
Long: `
|
||||
The "prune" command checks the repository and removes data that is not
|
||||
referenced and therefore not needed any more.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -42,34 +44,6 @@ func shortenStatus(maxLength int, s string) string {
|
||||
return s[:maxLength-3] + "..."
|
||||
}
|
||||
|
||||
// newProgressMax returns a progress that counts blobs.
|
||||
func newProgressMax(show bool, max uint64, description string) *restic.Progress {
|
||||
if !show {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := restic.NewProgress()
|
||||
|
||||
p.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||
status := fmt.Sprintf("[%s] %s %d / %d %s",
|
||||
formatDuration(d),
|
||||
formatPercent(s.Blobs, max),
|
||||
s.Blobs, max, description)
|
||||
|
||||
if w := stdoutTerminalWidth(); w > 0 {
|
||||
status = shortenStatus(w, status)
|
||||
}
|
||||
|
||||
PrintProgress("%s", status)
|
||||
}
|
||||
|
||||
p.OnDone = func(s restic.Stat, d time.Duration, ticker bool) {
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func runPrune(gopts GlobalOptions) error {
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
@@ -82,6 +56,9 @@ func runPrune(gopts GlobalOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// we do not need index updates while pruning!
|
||||
repo.DisableAutoIndexUpdate()
|
||||
|
||||
return pruneRepository(gopts, repo)
|
||||
}
|
||||
|
||||
@@ -120,7 +97,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
}
|
||||
|
||||
Verbosef("counting files in repo\n")
|
||||
err = repo.List(ctx, restic.DataFile, func(restic.ID, int64) error {
|
||||
err = repo.List(ctx, restic.PackFile, func(restic.ID, int64) error {
|
||||
stats.packs++
|
||||
return nil
|
||||
})
|
||||
@@ -178,34 +155,22 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
|
||||
stats.snapshots = len(snapshots)
|
||||
|
||||
Verbosef("find data that is still in use for %d snapshots\n", stats.snapshots)
|
||||
|
||||
usedBlobs := restic.NewBlobSet()
|
||||
seenBlobs := restic.NewBlobSet()
|
||||
|
||||
bar = newProgressMax(!gopts.Quiet, uint64(len(snapshots)), "snapshots")
|
||||
bar.Start()
|
||||
for _, sn := range snapshots {
|
||||
debug.Log("process snapshot %v", sn.ID())
|
||||
|
||||
err = restic.FindUsedBlobs(ctx, repo, *sn.Tree, usedBlobs, seenBlobs)
|
||||
if err != nil {
|
||||
if repo.Backend().IsNotExist(err) {
|
||||
return errors.Fatal("unable to load a tree from the repo: " + err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
debug.Log("processed snapshot %v", sn.ID())
|
||||
bar.Report(restic.Stat{Blobs: 1})
|
||||
usedBlobs, err := getUsedBlobs(gopts, repo, snapshots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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")
|
||||
var missingBlobs []restic.BlobHandle
|
||||
for h := range usedBlobs {
|
||||
if _, ok := blobCount[h]; !ok {
|
||||
missingBlobs = append(missingBlobs, h)
|
||||
}
|
||||
}
|
||||
if len(missingBlobs) > 0 {
|
||||
return errors.Fatalf("%v not found in the new index\n"+
|
||||
"Data blobs seem to be missing, aborting prune to prevent further data loss!\n"+
|
||||
"Please report this error (along with the output of the 'prune' run) at\n"+
|
||||
"https://github.com/restic/restic/issues/new/choose", missingBlobs)
|
||||
}
|
||||
|
||||
Verbosef("found %d of %d data blobs still in use, removing %d blobs\n",
|
||||
@@ -273,13 +238,11 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
|
||||
var obsoletePacks restic.IDSet
|
||||
if len(rewritePacks) != 0 {
|
||||
bar = newProgressMax(!gopts.Quiet, uint64(len(rewritePacks)), "packs rewritten")
|
||||
bar.Start()
|
||||
bar := newProgressMax(!gopts.Quiet, uint64(len(rewritePacks)), "packs rewritten")
|
||||
obsoletePacks, err = repository.Repack(ctx, repo, rewritePacks, usedBlobs, bar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bar.Done()
|
||||
}
|
||||
|
||||
removePacks.Merge(obsoletePacks)
|
||||
@@ -289,19 +252,38 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
}
|
||||
|
||||
if len(removePacks) != 0 {
|
||||
bar = newProgressMax(!gopts.Quiet, uint64(len(removePacks)), "packs deleted")
|
||||
bar.Start()
|
||||
for packID := range removePacks {
|
||||
h := restic.Handle{Type: restic.DataFile, Name: packID.String()}
|
||||
err = repo.Backend().Remove(ctx, h)
|
||||
if err != nil {
|
||||
Warnf("unable to remove file %v from the repository\n", packID.Str())
|
||||
}
|
||||
bar.Report(restic.Stat{Blobs: 1})
|
||||
}
|
||||
bar.Done()
|
||||
Verbosef("remove %d old packs\n", len(removePacks))
|
||||
DeleteFiles(gopts, repo, removePacks, restic.PackFile)
|
||||
}
|
||||
|
||||
Verbosef("done\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUsedBlobs(gopts GlobalOptions, repo restic.Repository, snapshots []*restic.Snapshot) (usedBlobs restic.BlobSet, err error) {
|
||||
ctx := gopts.ctx
|
||||
|
||||
Verbosef("find data that is still in use for %d snapshots\n", len(snapshots))
|
||||
|
||||
usedBlobs = restic.NewBlobSet()
|
||||
|
||||
bar := newProgressMax(!gopts.Quiet, uint64(len(snapshots)), "snapshots")
|
||||
bar.Start()
|
||||
defer bar.Done()
|
||||
for _, sn := range snapshots {
|
||||
debug.Log("process snapshot %v", sn.ID())
|
||||
|
||||
err = restic.FindUsedBlobs(ctx, repo, *sn.Tree, usedBlobs)
|
||||
if err != nil {
|
||||
if repo.Backend().IsNotExist(err) {
|
||||
return nil, errors.Fatal("unable to load a tree from the repo: " + err.Error())
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
debug.Log("processed snapshot %v", sn.ID())
|
||||
bar.Report(restic.Stat{Blobs: 1})
|
||||
}
|
||||
return usedBlobs, nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ var cmdRebuildIndex = &cobra.Command{
|
||||
Long: `
|
||||
The "rebuild-index" command creates a new index based on the pack files in the
|
||||
repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -48,7 +53,7 @@ func rebuildIndex(ctx context.Context, repo restic.Repository, ignorePacks resti
|
||||
Verbosef("counting files in repo\n")
|
||||
|
||||
var packs uint64
|
||||
err := repo.List(ctx, restic.DataFile, func(restic.ID, int64) error {
|
||||
err := repo.List(ctx, restic.PackFile, func(restic.ID, int64) error {
|
||||
packs++
|
||||
return nil
|
||||
})
|
||||
@@ -57,11 +62,17 @@ func rebuildIndex(ctx context.Context, repo restic.Repository, ignorePacks resti
|
||||
}
|
||||
|
||||
bar := newProgressMax(!globalOptions.Quiet, packs-uint64(len(ignorePacks)), "packs")
|
||||
idx, _, err := index.New(ctx, repo, ignorePacks, bar)
|
||||
idx, invalidFiles, err := index.New(ctx, repo, ignorePacks, bar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if globalOptions.verbosity >= 2 {
|
||||
for _, id := range invalidFiles {
|
||||
Printf("skipped incomplete pack file: %v\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
Verbosef("finding old index files\n")
|
||||
|
||||
var supersedes restic.IDs
|
||||
@@ -81,14 +92,9 @@ func rebuildIndex(ctx context.Context, repo restic.Repository, ignorePacks resti
|
||||
Verbosef("saved new indexes as %v\n", ids)
|
||||
|
||||
Verbosef("remove %d old index files\n", len(supersedes))
|
||||
|
||||
for _, id := range supersedes {
|
||||
if err := repo.Backend().Remove(ctx, restic.Handle{
|
||||
Type: restic.IndexFile,
|
||||
Name: id.String(),
|
||||
}); err != nil {
|
||||
Warnf("error removing old index %v: %v\n", id.Str(), err)
|
||||
}
|
||||
err = DeleteFilesChecked(globalOptions, repo, restic.NewIDSet(supersedes...), restic.IndexFile)
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to remove an old index: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -16,6 +16,11 @@ var cmdRecover = &cobra.Command{
|
||||
The "recover" command build a new snapshot from all directories it can find in
|
||||
the raw data of the repository. It can be used if, for example, a snapshot has
|
||||
been removed by accident with "forget".
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -125,11 +130,6 @@ func runRecover(gopts GlobalOptions) error {
|
||||
return errors.Fatalf("unable to save blobs to the repo: %v", err)
|
||||
}
|
||||
|
||||
err = repo.SaveIndex(gopts.ctx)
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to save new index to the repo: %v", err)
|
||||
}
|
||||
|
||||
sn, err := restic.NewSnapshot([]string{"/recover"}, []string{}, hostname, time.Now())
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"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/restic/restic/internal/restorer"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -20,6 +21,11 @@ a directory.
|
||||
|
||||
The special snapshot "latest" can be used to restore the latest snapshot in the
|
||||
repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -34,7 +40,7 @@ type RestoreOptions struct {
|
||||
Include []string
|
||||
InsensitiveInclude []string
|
||||
Target string
|
||||
Host string
|
||||
Hosts []string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
Verify bool
|
||||
@@ -52,7 +58,7 @@ func init() {
|
||||
flags.StringArrayVar(&restoreOptions.InsensitiveInclude, "iinclude", nil, "same as `--include` but ignores the casing of filenames")
|
||||
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.StringArrayVarP(&restoreOptions.Hosts, "host", "H", nil, `only consider snapshots for this host when the snapshot ID is "latest" (can be specified multiple times)`)
|
||||
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\"")
|
||||
flags.BoolVar(&restoreOptions.Verify, "verify", false, "verify restored files content")
|
||||
@@ -111,9 +117,9 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||
var id restic.ID
|
||||
|
||||
if snapshotIDString == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Host)
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, opts.Paths, opts.Tags, opts.Hosts)
|
||||
if err != nil {
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Host:%v", err, opts.Paths, opts.Host)
|
||||
Exitf(1, "latest snapshot for criteria not found: %v Paths:%v Hosts:%v", err, opts.Paths, opts.Hosts)
|
||||
}
|
||||
} else {
|
||||
id, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||
|
||||
@@ -18,6 +18,11 @@ The command "self-update" downloads the latest stable release of restic from
|
||||
GitHub and replaces the currently running binary. After download, the
|
||||
authenticity of the binary is verified using the GPG signature on the release
|
||||
files.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -14,10 +14,15 @@ import (
|
||||
)
|
||||
|
||||
var cmdSnapshots = &cobra.Command{
|
||||
Use: "snapshots [snapshotID ...]",
|
||||
Use: "snapshots [flags] [snapshotID ...]",
|
||||
Short: "List all snapshots",
|
||||
Long: `
|
||||
The "snapshots" command lists all snapshots stored in the repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -27,7 +32,7 @@ The "snapshots" command lists all snapshots stored in the repository.
|
||||
|
||||
// SnapshotOptions bundles all options for the snapshots command.
|
||||
type SnapshotOptions struct {
|
||||
Host string
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Compact bool
|
||||
@@ -41,7 +46,7 @@ func init() {
|
||||
cmdRoot.AddCommand(cmdSnapshots)
|
||||
|
||||
f := cmdSnapshots.Flags()
|
||||
f.StringVarP(&snapshotOptions.Host, "host", "H", "", "only consider snapshots for this `host`")
|
||||
f.StringArrayVarP(&snapshotOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host` (can be specified multiple times)")
|
||||
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")
|
||||
@@ -67,7 +72,7 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
|
||||
defer cancel()
|
||||
|
||||
var snapshots restic.Snapshots
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
snapshotGroups, grouped, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
||||
@@ -246,9 +251,8 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
|
||||
// Prints nothing, if we did not group at all.
|
||||
func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
|
||||
var key restic.SnapshotGroupKey
|
||||
var err error
|
||||
|
||||
err = json.Unmarshal([]byte(groupKeyJSON), &key)
|
||||
err := json.Unmarshal([]byte(groupKeyJSON), &key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,31 +2,31 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdStats = &cobra.Command{
|
||||
Use: "stats [flags] [snapshot-ID]",
|
||||
Use: "stats [flags] [snapshot ID] [...]",
|
||||
Short: "Scan the repository and show basic statistics",
|
||||
Long: `
|
||||
The "stats" command walks one or all snapshots in a repository and
|
||||
accumulates statistics about the data stored therein. It reports on
|
||||
the number of unique files and their sizes, according to one of
|
||||
The "stats" command walks one or multiple snapshots in a repository
|
||||
and accumulates statistics about the data stored therein. It reports
|
||||
on the number of unique files and their sizes, according to one of
|
||||
the counting modes as given by the --mode flag.
|
||||
|
||||
If no snapshot is specified, all snapshots will be considered. Some
|
||||
modes make more sense over just a single snapshot, while others
|
||||
are useful across all snapshots, depending on what you are trying
|
||||
to calculate.
|
||||
It operates on all snapshots matching the selection criteria or all
|
||||
snapshots if nothing is specified. The special snapshot ID "latest"
|
||||
is also supported. Some modes make more sense over
|
||||
just a single snapshot, while others are useful across all snapshots,
|
||||
depending on what you are trying to calculate.
|
||||
|
||||
The modes are:
|
||||
|
||||
@@ -38,6 +38,11 @@ The modes are:
|
||||
* blobs-per-file: A combination of files-by-contents and raw-data.
|
||||
|
||||
Refer to the online manual for more details about each mode.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -45,11 +50,26 @@ Refer to the online manual for more details about each mode.
|
||||
},
|
||||
}
|
||||
|
||||
// StatsOptions collects all options for the stats command.
|
||||
type StatsOptions struct {
|
||||
// the mode of counting to perform (see consts for available modes)
|
||||
countMode string
|
||||
|
||||
// filter snapshots by, if given by user
|
||||
Hosts []string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
}
|
||||
|
||||
var statsOptions StatsOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdStats)
|
||||
f := cmdStats.Flags()
|
||||
f.StringVar(&countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file, or raw-data")
|
||||
f.StringVarP(&snapshotByHost, "host", "H", "", "filter latest snapshot by this hostname")
|
||||
f.StringVar(&statsOptions.countMode, "mode", countModeRestoreSize, "counting mode: restore-size (default), files-by-contents, blobs-per-file or raw-data")
|
||||
f.StringArrayVarP(&statsOptions.Hosts, "host", "H", nil, "only consider snapshots with the given `host` (can be specified multiple times)")
|
||||
f.Var(&statsOptions.Tags, "tag", "only consider snapshots which include this `taglist` in the format `tag[,tag,...]` (can be specified multiple times)")
|
||||
f.StringArrayVar(&statsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
|
||||
}
|
||||
|
||||
func runStats(gopts GlobalOptions, args []string) error {
|
||||
@@ -84,49 +104,25 @@ func runStats(gopts GlobalOptions, args []string) error {
|
||||
|
||||
// create a container for the stats (and other needed state)
|
||||
stats := &statsContainer{
|
||||
uniqueFiles: make(map[fileID]struct{}),
|
||||
fileBlobs: make(map[string]restic.IDSet),
|
||||
blobs: restic.NewBlobSet(),
|
||||
blobsSeen: restic.NewBlobSet(),
|
||||
uniqueFiles: make(map[fileID]struct{}),
|
||||
uniqueInodes: make(map[uint64]struct{}),
|
||||
fileBlobs: make(map[string]restic.IDSet),
|
||||
blobs: restic.NewBlobSet(),
|
||||
snapshotsCount: 0,
|
||||
}
|
||||
|
||||
if snapshotIDString != "" {
|
||||
// scan just a single snapshot
|
||||
|
||||
var sID restic.ID
|
||||
if snapshotIDString == "latest" {
|
||||
sID, err = restic.FindLatestSnapshot(ctx, repo, []string{}, []restic.TagList{}, snapshotByHost)
|
||||
if err != nil {
|
||||
return errors.Fatalf("latest snapshot for criteria not found: %v", err)
|
||||
}
|
||||
} else {
|
||||
sID, err = restic.FindSnapshot(repo, snapshotIDString)
|
||||
if err != nil {
|
||||
return errors.Fatalf("error loading snapshot: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
snapshot, err := restic.LoadSnapshot(ctx, repo, sID)
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, statsOptions.Hosts, statsOptions.Tags, statsOptions.Paths, args) {
|
||||
err = statsWalkSnapshot(ctx, sn, repo, stats)
|
||||
if err != nil {
|
||||
return errors.Fatalf("error loading snapshot from repo: %v", err)
|
||||
return fmt.Errorf("error walking snapshot: %v", err)
|
||||
}
|
||||
|
||||
err = statsWalkSnapshot(ctx, snapshot, repo, stats)
|
||||
} else {
|
||||
// iterate every snapshot in the repo
|
||||
err = repo.List(ctx, restic.SnapshotFile, func(snapshotID restic.ID, size int64) error {
|
||||
snapshot, err := restic.LoadSnapshot(ctx, repo, snapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error loading snapshot %s: %v", snapshotID.Str(), err)
|
||||
}
|
||||
return statsWalkSnapshot(ctx, snapshot, repo, stats)
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if countMode == countModeRawData {
|
||||
if statsOptions.countMode == countModeRawData {
|
||||
// the blob handles have been collected, but not yet counted
|
||||
for blobHandle := range stats.blobs {
|
||||
blobSize, found := repo.LookupBlobSize(blobHandle.ID, blobHandle.Type)
|
||||
@@ -139,29 +135,23 @@ func runStats(gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
if gopts.JSON {
|
||||
err = json.NewEncoder(os.Stdout).Encode(stats)
|
||||
err = json.NewEncoder(globalOptions.stdout).Encode(stats)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding output: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// inform the user what was scanned and how it was scanned
|
||||
snapshotsScanned := snapshotIDString
|
||||
if snapshotsScanned == "latest" {
|
||||
snapshotsScanned = "the latest snapshot"
|
||||
} else if snapshotsScanned == "" {
|
||||
snapshotsScanned = "all snapshots"
|
||||
}
|
||||
Printf("Stats for %s in %s mode:\n", snapshotsScanned, countMode)
|
||||
Printf("Stats in %s mode:\n", statsOptions.countMode)
|
||||
Printf("Snapshots processed: %d\n", stats.snapshotsCount)
|
||||
|
||||
if stats.TotalBlobCount > 0 {
|
||||
Printf(" Total Blob Count: %d\n", stats.TotalBlobCount)
|
||||
Printf(" Total Blob Count: %d\n", stats.TotalBlobCount)
|
||||
}
|
||||
if stats.TotalFileCount > 0 {
|
||||
Printf(" Total File Count: %d\n", stats.TotalFileCount)
|
||||
Printf(" Total File Count: %d\n", stats.TotalFileCount)
|
||||
}
|
||||
Printf(" Total Size: %-5s\n", formatBytes(stats.TotalSize))
|
||||
Printf(" Total Size: %-5s\n", formatBytes(stats.TotalSize))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -171,21 +161,24 @@ func statsWalkSnapshot(ctx context.Context, snapshot *restic.Snapshot, repo rest
|
||||
return fmt.Errorf("snapshot %s has nil tree", snapshot.ID().Str())
|
||||
}
|
||||
|
||||
if countMode == countModeRawData {
|
||||
stats.snapshotsCount++
|
||||
|
||||
if statsOptions.countMode == countModeRawData {
|
||||
// count just the sizes of unique blobs; we don't need to walk the tree
|
||||
// ourselves in this case, since a nifty function does it for us
|
||||
return restic.FindUsedBlobs(ctx, repo, *snapshot.Tree, stats.blobs, stats.blobsSeen)
|
||||
return restic.FindUsedBlobs(ctx, repo, *snapshot.Tree, stats.blobs)
|
||||
}
|
||||
|
||||
err := walker.Walk(ctx, repo, *snapshot.Tree, restic.NewIDSet(), statsWalkTree(repo, stats))
|
||||
if err != nil {
|
||||
return fmt.Errorf("walking tree %s: %v", *snapshot.Tree, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFunc {
|
||||
return func(_ restic.ID, npath string, node *restic.Node, nodeErr error) (bool, error) {
|
||||
return func(parentTreeID restic.ID, npath string, node *restic.Node, nodeErr error) (bool, error) {
|
||||
if nodeErr != nil {
|
||||
return true, nodeErr
|
||||
}
|
||||
@@ -193,19 +186,19 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if countMode == countModeUniqueFilesByContents || countMode == countModeBlobsPerFile {
|
||||
if statsOptions.countMode == countModeUniqueFilesByContents || statsOptions.countMode == countModeBlobsPerFile {
|
||||
// only count this file if we haven't visited it before
|
||||
fid := makeFileIDByContents(node)
|
||||
if _, ok := stats.uniqueFiles[fid]; !ok {
|
||||
// mark the file as visited
|
||||
stats.uniqueFiles[fid] = struct{}{}
|
||||
|
||||
if countMode == countModeUniqueFilesByContents {
|
||||
if statsOptions.countMode == countModeUniqueFilesByContents {
|
||||
// simply count the size of each unique file (unique by contents only)
|
||||
stats.TotalSize += node.Size
|
||||
stats.TotalFileCount++
|
||||
}
|
||||
if countMode == countModeBlobsPerFile {
|
||||
if statsOptions.countMode == countModeBlobsPerFile {
|
||||
// count the size of each unique blob reference, which is
|
||||
// by unique file (unique by contents and file path)
|
||||
for _, blobID := range node.Content {
|
||||
@@ -220,7 +213,7 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun
|
||||
// is always a data blob since we're accessing it via a file's Content array
|
||||
blobSize, found := repo.LookupBlobSize(blobID, restic.DataBlob)
|
||||
if !found {
|
||||
return true, fmt.Errorf("blob %s not found for tree %s", blobID, *node.Subtree)
|
||||
return true, fmt.Errorf("blob %s not found for tree %s", blobID, parentTreeID)
|
||||
}
|
||||
|
||||
// count the blob's size, then add this blob by this
|
||||
@@ -235,12 +228,20 @@ func statsWalkTree(repo restic.Repository, stats *statsContainer) walker.WalkFun
|
||||
}
|
||||
}
|
||||
|
||||
if countMode == countModeRestoreSize {
|
||||
if statsOptions.countMode == countModeRestoreSize {
|
||||
// as this is a file in the snapshot, we can simply count its
|
||||
// size without worrying about uniqueness, since duplicate files
|
||||
// will still be restored
|
||||
stats.TotalSize += node.Size
|
||||
stats.TotalFileCount++
|
||||
|
||||
// if inodes are present, only count each inode once
|
||||
// (hard links do not increase restore size)
|
||||
if _, ok := stats.uniqueInodes[node.Inode]; !ok || node.Inode == 0 {
|
||||
stats.uniqueInodes[node.Inode] = struct{}{}
|
||||
stats.TotalSize += node.Size
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -259,23 +260,13 @@ func makeFileIDByContents(node *restic.Node) fileID {
|
||||
|
||||
func verifyStatsInput(gopts GlobalOptions, args []string) error {
|
||||
// require a recognized counting mode
|
||||
switch countMode {
|
||||
switch statsOptions.countMode {
|
||||
case countModeRestoreSize:
|
||||
case countModeUniqueFilesByContents:
|
||||
case countModeBlobsPerFile:
|
||||
case countModeRawData:
|
||||
default:
|
||||
return fmt.Errorf("unknown counting mode: %s (use the -h flag to get a list of supported modes)", countMode)
|
||||
}
|
||||
|
||||
// ensure at most one snapshot was specified
|
||||
if len(args) > 1 {
|
||||
return fmt.Errorf("only one snapshot may be specified")
|
||||
}
|
||||
|
||||
// if a snapshot was specified, mark it as the one to scan
|
||||
if len(args) == 1 {
|
||||
snapshotIDString = args[0]
|
||||
return fmt.Errorf("unknown counting mode: %s (use the -h flag to get a list of supported modes)", statsOptions.countMode)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -293,30 +284,25 @@ type statsContainer struct {
|
||||
// contents (hashed sequence of content blob IDs)
|
||||
uniqueFiles map[fileID]struct{}
|
||||
|
||||
// uniqueInodes marks visited files according to their
|
||||
// inode # (hashed sequence of inode numbers)
|
||||
uniqueInodes map[uint64]struct{}
|
||||
|
||||
// fileBlobs maps a file name (path) to the set of
|
||||
// blobs that have been seen as a part of the file
|
||||
fileBlobs map[string]restic.IDSet
|
||||
|
||||
// blobs and blobsSeen are used to count individual
|
||||
// unique blobs, independent of references to files
|
||||
blobs, blobsSeen restic.BlobSet
|
||||
// blobs is used to count individual unique blobs,
|
||||
// independent of references to files
|
||||
blobs restic.BlobSet
|
||||
|
||||
// holds count of all considered snapshots
|
||||
snapshotsCount int
|
||||
}
|
||||
|
||||
// fileID is a 256-bit hash that distinguishes unique files.
|
||||
type fileID [32]byte
|
||||
|
||||
var (
|
||||
// the mode of counting to perform
|
||||
countMode string
|
||||
|
||||
// the snapshot to scan, as given by the user
|
||||
snapshotIDString string
|
||||
|
||||
// snapshotByHost is the host to filter latest
|
||||
// snapshot by, if given by user
|
||||
snapshotByHost string
|
||||
)
|
||||
|
||||
const (
|
||||
countModeRestoreSize = "restore-size"
|
||||
countModeUniqueFilesByContents = "files-by-contents"
|
||||
|
||||
@@ -21,6 +21,11 @@ You can either set/replace the entire set of tags on a snapshot, or
|
||||
add tags to/remove tags from the existing set.
|
||||
|
||||
When no snapshot-ID is given, all snapshots matching the host, tag and path filter criteria are modified.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -30,7 +35,7 @@ When no snapshot-ID is given, all snapshots matching the host, tag and path filt
|
||||
|
||||
// TagOptions bundles all options for the 'tag' command.
|
||||
type TagOptions struct {
|
||||
Host string
|
||||
Hosts []string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
SetTags []string
|
||||
@@ -48,7 +53,7 @@ func init() {
|
||||
tagFlags.StringSliceVar(&tagOptions.AddTags, "add", nil, "`tag` which will be added to the existing tags (can be given multiple times)")
|
||||
tagFlags.StringSliceVar(&tagOptions.RemoveTags, "remove", nil, "`tag` which will be removed from the existing tags (can be given multiple times)")
|
||||
|
||||
tagFlags.StringVarP(&tagOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
|
||||
tagFlags.StringArrayVarP(&tagOptions.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when no snapshot ID is given (can be specified multiple times)")
|
||||
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")
|
||||
}
|
||||
@@ -124,7 +129,7 @@ func runTag(opts TagOptions, gopts GlobalOptions, args []string) error {
|
||||
changeCnt := 0
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||
changed, err := changeTags(ctx, repo, sn, opts.SetTags, opts.AddTags, opts.RemoveTags)
|
||||
if err != nil {
|
||||
Warnf("unable to modify the tags for snapshot ID %q, ignoring: %v\n", sn.ID(), err)
|
||||
|
||||
@@ -10,6 +10,11 @@ var unlockCmd = &cobra.Command{
|
||||
Short: "Remove locks other processes created",
|
||||
Long: `
|
||||
The "unlock" command removes stale locks that have been created by other restic processes.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -13,6 +13,11 @@ var versionCmd = &cobra.Command{
|
||||
Long: `
|
||||
The "version" command prints detailed information about the build environment
|
||||
and the version of this software.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
|
||||
Exit status is 0 if the command was successful, and non-zero if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
62
cmd/restic/delete.go
Normal file
62
cmd/restic/delete.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
)
|
||||
|
||||
// DeleteFiles deletes the given fileList of fileType in parallel
|
||||
// it will print a warning if there is an error, but continue deleting the remaining files
|
||||
func DeleteFiles(gopts GlobalOptions, repo restic.Repository, fileList restic.IDSet, fileType restic.FileType) {
|
||||
deleteFiles(gopts, true, repo, fileList, fileType)
|
||||
}
|
||||
|
||||
// DeleteFilesChecked deletes the given fileList of fileType in parallel
|
||||
// if an error occurs, it will cancel and return this error
|
||||
func DeleteFilesChecked(gopts GlobalOptions, repo restic.Repository, fileList restic.IDSet, fileType restic.FileType) error {
|
||||
return deleteFiles(gopts, false, repo, fileList, fileType)
|
||||
}
|
||||
|
||||
const numDeleteWorkers = 8
|
||||
|
||||
// deleteFiles deletes the given fileList of fileType in parallel
|
||||
// if ignoreError=true, it will print a warning if there was an error, else it will abort.
|
||||
func deleteFiles(gopts GlobalOptions, ignoreError bool, repo restic.Repository, fileList restic.IDSet, fileType restic.FileType) error {
|
||||
totalCount := len(fileList)
|
||||
fileChan := make(chan restic.ID)
|
||||
go func() {
|
||||
for id := range fileList {
|
||||
fileChan <- id
|
||||
}
|
||||
close(fileChan)
|
||||
}()
|
||||
|
||||
bar := newProgressMax(!gopts.JSON && !gopts.Quiet, uint64(totalCount), "files deleted")
|
||||
wg, ctx := errgroup.WithContext(gopts.ctx)
|
||||
bar.Start()
|
||||
for i := 0; i < numDeleteWorkers; i++ {
|
||||
wg.Go(func() error {
|
||||
for id := range fileChan {
|
||||
h := restic.Handle{Type: fileType, Name: id.String()}
|
||||
err := repo.Backend().Remove(ctx, h)
|
||||
if err != nil {
|
||||
if !gopts.JSON {
|
||||
Warnf("unable to remove %v from the repository\n", h)
|
||||
}
|
||||
if !ignoreError {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !gopts.JSON && gopts.verbosity >= 2 {
|
||||
Verbosef("removed %v\n", h)
|
||||
}
|
||||
bar.Report(restic.Stat{Blobs: 1})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
err := wg.Wait()
|
||||
bar.Done()
|
||||
return err
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -131,7 +132,7 @@ func rejectIfPresent(excludeFileSpec string) (RejectByNameFunc, error) {
|
||||
}
|
||||
|
||||
// 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
|
||||
// is in an 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.
|
||||
@@ -190,7 +191,7 @@ func isDirExcludedByFile(dir, tagFilename, header string) bool {
|
||||
Warnf("could not read signature from exclusion tagfile %q: %v\n", tf, err)
|
||||
return false
|
||||
}
|
||||
if bytes.Compare(buf, []byte(header)) != 0 {
|
||||
if !bytes.Equal(buf, []byte(header)) {
|
||||
Warnf("invalid signature in exclusion tagfile %q\n", tf)
|
||||
return false
|
||||
}
|
||||
@@ -292,3 +293,50 @@ func rejectResticCache(repo *repository.Repository) (RejectByNameFunc, error) {
|
||||
return false
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rejectBySize(maxSizeStr string) (RejectFunc, error) {
|
||||
maxSize, err := parseSizeStr(maxSizeStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(item string, fi os.FileInfo) bool {
|
||||
// directory will be ignored
|
||||
if fi.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
filesize := fi.Size()
|
||||
if filesize > maxSize {
|
||||
debug.Log("file %s is oversize: %d", item, filesize)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseSizeStr(sizeStr string) (int64, error) {
|
||||
numStr := sizeStr[:len(sizeStr)-1]
|
||||
var unit int64 = 1
|
||||
|
||||
switch sizeStr[len(sizeStr)-1] {
|
||||
case 'b', 'B':
|
||||
// use initialized values, do nothing here
|
||||
case 'k', 'K':
|
||||
unit = 1024
|
||||
case 'm', 'M':
|
||||
unit = 1024 * 1024
|
||||
case 'g', 'G':
|
||||
unit = 1024 * 1024 * 1024
|
||||
case 't', 'T':
|
||||
unit = 1024 * 1024 * 1024 * 1024
|
||||
default:
|
||||
numStr = sizeStr
|
||||
}
|
||||
value, err := strconv.ParseInt(numStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return value * unit, nil
|
||||
}
|
||||
|
||||
@@ -189,3 +189,113 @@ func TestMultipleIsExcludedByFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSizeStr(t *testing.T) {
|
||||
sizeStrTests := []struct {
|
||||
in string
|
||||
expected int64
|
||||
}{
|
||||
{"1024", 1024},
|
||||
{"1024b", 1024},
|
||||
{"1024B", 1024},
|
||||
{"1k", 1024},
|
||||
{"100k", 102400},
|
||||
{"100K", 102400},
|
||||
{"10M", 10485760},
|
||||
{"100m", 104857600},
|
||||
{"20G", 21474836480},
|
||||
{"10g", 10737418240},
|
||||
{"2T", 2199023255552},
|
||||
{"2t", 2199023255552},
|
||||
}
|
||||
|
||||
for _, tt := range sizeStrTests {
|
||||
actual, err := parseSizeStr(tt.in)
|
||||
test.OK(t, err)
|
||||
|
||||
if actual != tt.expected {
|
||||
t.Errorf("parseSizeStr(%s) = %d; expected %d", tt.in, actual, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsExcludedByFileSize is for testing the instance of
|
||||
// --exclude-larger-than parameters
|
||||
func TestIsExcludedByFileSize(t *testing.T) {
|
||||
tempDir, cleanup := test.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
// Max size of file is set to be 1k
|
||||
maxSizeStr := "1k"
|
||||
|
||||
// Create some files in a temporary directory.
|
||||
// Files in UPPERCASE will be used as exclusion triggers later on.
|
||||
// We will test the inclusion later, so we add the expected value as
|
||||
// a bool.
|
||||
files := []struct {
|
||||
path string
|
||||
size int64
|
||||
incl bool
|
||||
}{
|
||||
{"42", 100, true},
|
||||
|
||||
// everything in foodir except the FOOLARGE tagfile
|
||||
// should not be included.
|
||||
{"foodir/FOOLARGE", 2048, false},
|
||||
{"foodir/foo", 1002, true},
|
||||
{"foodir/foosub/underfoo", 100, true},
|
||||
|
||||
// everything in bardir except the BARLARGE tagfile
|
||||
// should not be included.
|
||||
{"bardir/BARLARGE", 1030, false},
|
||||
{"bardir/bar", 1000, true},
|
||||
{"bardir/barsub/underbar", 500, true},
|
||||
|
||||
// everything in bazdir should be included.
|
||||
{"bazdir/baz", 100, true},
|
||||
{"bazdir/bazsub/underbaz", 200, true},
|
||||
}
|
||||
var errs []error
|
||||
for _, f := range files {
|
||||
// create directories first, then the file
|
||||
p := filepath.Join(tempDir, filepath.FromSlash(f.path))
|
||||
errs = append(errs, os.MkdirAll(filepath.Dir(p), 0700))
|
||||
file, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
errs = append(errs, err)
|
||||
if err == nil {
|
||||
// create a file with given size
|
||||
errs = append(errs, file.Truncate(f.size))
|
||||
}
|
||||
errs = append(errs, file.Close())
|
||||
}
|
||||
test.OKs(t, errs) // see if anything went wrong during the creation
|
||||
|
||||
// create rejection function
|
||||
sizeExclude, _ := rejectBySize(maxSizeStr)
|
||||
|
||||
// To mock the archiver scanning walk, we create filepath.WalkFn
|
||||
// that tests against the two rejection functions and stores
|
||||
// the result in a map against we can test later.
|
||||
m := make(map[string]bool)
|
||||
walk := func(p string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
excluded := sizeExclude(p, fi)
|
||||
// the log message helps debugging in case the test fails
|
||||
t.Logf("%q: dir:%t; size:%d; excluded:%v", p, fi.IsDir(), fi.Size(), excluded)
|
||||
m[p] = !excluded
|
||||
return nil
|
||||
}
|
||||
// walk through the temporary file and check the error
|
||||
test.OK(t, filepath.Walk(tempDir, walk))
|
||||
|
||||
// compare whether the walk gave the expected values for the test cases
|
||||
for _, f := range files {
|
||||
p := filepath.Join(tempDir, filepath.FromSlash(f.path))
|
||||
if m[p] != f.incl {
|
||||
t.Errorf("inclusion status of %s is wrong: want %v, got %v", f.path, f.incl, m[p])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// 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 []restic.TagList, paths []string, snapshotIDs []string) <-chan *restic.Snapshot {
|
||||
func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hosts []string, tags []restic.TagList, paths []string, snapshotIDs []string) <-chan *restic.Snapshot {
|
||||
out := make(chan *restic.Snapshot)
|
||||
go func() {
|
||||
defer close(out)
|
||||
@@ -22,10 +22,10 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
|
||||
// Process all snapshot IDs given as arguments.
|
||||
for _, s := range snapshotIDs {
|
||||
if s == "latest" {
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, paths, tags, host)
|
||||
usedFilter = true
|
||||
id, err = restic.FindLatestSnapshot(ctx, repo, paths, tags, hosts)
|
||||
if err != nil {
|
||||
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Host:%v)\n", s, paths, tags, host)
|
||||
usedFilter = true
|
||||
Warnf("Ignoring %q, no snapshot matched given filter (Paths:%v Tags:%v Hosts:%v)\n", s, paths, tags, hosts)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
@@ -39,7 +39,7 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
|
||||
}
|
||||
|
||||
// Give the user some indication their filters are not used.
|
||||
if !usedFilter && (host != "" || len(tags) != 0 || len(paths) != 0) {
|
||||
if !usedFilter && (len(hosts) != 0 || len(tags) != 0 || len(paths) != 0) {
|
||||
Warnf("Ignoring filters as there are explicit snapshot ids given\n")
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func FindFilteredSnapshots(ctx context.Context, repo *repository.Repository, hos
|
||||
return
|
||||
}
|
||||
|
||||
snapshots, err := restic.FindFilteredSnapshots(ctx, repo, host, tags, paths)
|
||||
snapshots, err := restic.FindFilteredSnapshots(ctx, repo, hosts, tags, paths)
|
||||
if err != nil {
|
||||
Warnf("could not load snapshots: %v\n", err)
|
||||
return
|
||||
|
||||
@@ -51,12 +51,6 @@ func formatPercent(numerator uint64, denominator uint64) string {
|
||||
return fmt.Sprintf("%3.2f%%", percent)
|
||||
}
|
||||
|
||||
func formatRate(bytes uint64, duration time.Duration) string {
|
||||
sec := float64(duration) / float64(time.Second)
|
||||
rate := float64(bytes) / sec / (1 << 20)
|
||||
return fmt.Sprintf("%.2fMiB/s", rate)
|
||||
}
|
||||
|
||||
func formatDuration(d time.Duration) string {
|
||||
sec := uint64(d / time.Second)
|
||||
return formatSeconds(sec)
|
||||
|
||||
@@ -39,11 +39,13 @@ import (
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
var version = "0.9.6"
|
||||
var version = "0.10.0"
|
||||
|
||||
// TimeFormat is the format used for all timestamps printed by restic.
|
||||
const TimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
type backendWrapper func(r restic.Backend) (restic.Backend, error)
|
||||
|
||||
// GlobalOptions hold all global options for restic.
|
||||
type GlobalOptions struct {
|
||||
Repo string
|
||||
@@ -68,11 +70,13 @@ type GlobalOptions struct {
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
|
||||
backendTestHook backendWrapper
|
||||
|
||||
// verbosity is set as follows:
|
||||
// 0 means: don't print any messages except errors, this is used when --quiet is specified
|
||||
// 1 is the default: print essential messages
|
||||
// 2 means: print more messages, report minor things, this is used when --verbose is specified
|
||||
// 3 means: print very detailed debug messages, this is used when --verbose 2 is specified
|
||||
// 3 means: print very detailed debug messages, this is used when --verbose=2 is specified
|
||||
verbosity uint
|
||||
|
||||
Options []string
|
||||
@@ -85,6 +89,8 @@ var globalOptions = GlobalOptions{
|
||||
stderr: os.Stderr,
|
||||
}
|
||||
|
||||
var isReadingPassword bool
|
||||
|
||||
func init() {
|
||||
var cancel context.CancelFunc
|
||||
globalOptions.ctx, cancel = context.WithCancel(context.Background())
|
||||
@@ -94,18 +100,18 @@ func init() {
|
||||
})
|
||||
|
||||
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.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "key ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
|
||||
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "specify a shell command to obtain a password (default: $RESTIC_PASSWORD_COMMAND)")
|
||||
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"), "`file` to read the repository password from (default: $RESTIC_PASSWORD_FILE)")
|
||||
f.StringVarP(&globalOptions.KeyHint, "key-hint", "", os.Getenv("RESTIC_KEY_HINT"), "`key` ID of key to try decrypting first (default: $RESTIC_KEY_HINT)")
|
||||
f.StringVarP(&globalOptions.PasswordCommand, "password-command", "", os.Getenv("RESTIC_PASSWORD_COMMAND"), "shell `command` to obtain the repository password from (default: $RESTIC_PASSWORD_COMMAND)")
|
||||
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
|
||||
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
|
||||
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level --verbose=`n`)")
|
||||
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. (default: use system default cache directory)")
|
||||
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache `directory`. (default: use system default cache directory)")
|
||||
f.BoolVar(&globalOptions.NoCache, "no-cache", false, "do not use a local cache")
|
||||
f.StringSliceVar(&globalOptions.CACerts, "cacert", nil, "`file` to load root certificates from (default: use system certificates)")
|
||||
f.StringVar(&globalOptions.TLSClientCert, "tls-client-cert", "", "path to a file containing PEM encoded TLS client certificate and private key")
|
||||
f.StringVar(&globalOptions.TLSClientCert, "tls-client-cert", "", "path to a `file` containing PEM encoded TLS client certificate and private key")
|
||||
f.BoolVar(&globalOptions.CleanupCache, "cleanup-cache", false, "auto remove old cache directories")
|
||||
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)")
|
||||
@@ -146,7 +152,10 @@ func stdoutTerminalWidth() int {
|
||||
}
|
||||
|
||||
// restoreTerminal installs a cleanup handler that restores the previous
|
||||
// terminal state on exit.
|
||||
// terminal state on exit. This handler is only intended to restore the
|
||||
// terminal configuration if restic exits after receiving a signal. A regular
|
||||
// program execution must revert changes to the terminal configuration itself.
|
||||
// The terminal configuration is only restored while reading a password.
|
||||
func restoreTerminal() {
|
||||
if !stdoutIsTerminal() {
|
||||
return
|
||||
@@ -160,9 +169,17 @@ func restoreTerminal() {
|
||||
}
|
||||
|
||||
AddCleanupHandler(func() error {
|
||||
// Restoring the terminal configuration while restic runs in the
|
||||
// background, causes restic to get stopped on unix systems with
|
||||
// a SIGTTOU signal. Thus only restore the terminal settings if
|
||||
// they might have been modified, which is the case while reading
|
||||
// a password.
|
||||
if !isReadingPassword {
|
||||
return nil
|
||||
}
|
||||
err := checkErrno(terminal.Restore(fd, state))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to get restore terminal state: %#+v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
@@ -189,6 +206,22 @@ func Printf(format string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Print writes the message to the configured stdout stream.
|
||||
func Print(args ...interface{}) {
|
||||
_, err := fmt.Fprint(globalOptions.stdout, args...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Println writes the message to the configured stdout stream.
|
||||
func Println(args ...interface{}) {
|
||||
_, err := fmt.Fprintln(globalOptions.stdout, args...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verbosef calls Printf to write the message when the verbose flag is set.
|
||||
func Verbosef(format string, args ...interface{}) {
|
||||
if globalOptions.verbosity >= 1 {
|
||||
@@ -232,7 +265,7 @@ func Warnf(format string, args ...interface{}) {
|
||||
// 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' {
|
||||
if !(strings.HasSuffix(format, "\n")) {
|
||||
format += "\n"
|
||||
}
|
||||
|
||||
@@ -241,7 +274,7 @@ func Exitf(exitcode int, format string, args ...interface{}) {
|
||||
}
|
||||
|
||||
// resolvePassword determines the password to be used for opening the repository.
|
||||
func resolvePassword(opts GlobalOptions) (string, error) {
|
||||
func resolvePassword(opts GlobalOptions, envStr string) (string, error) {
|
||||
if opts.PasswordFile != "" && opts.PasswordCommand != "" {
|
||||
return "", errors.Fatalf("Password file and command are mutually exclusive options")
|
||||
}
|
||||
@@ -266,7 +299,7 @@ func resolvePassword(opts GlobalOptions) (string, error) {
|
||||
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
||||
}
|
||||
|
||||
if pwd := os.Getenv("RESTIC_PASSWORD"); pwd != "" {
|
||||
if pwd := os.Getenv(envStr); pwd != "" {
|
||||
return pwd, nil
|
||||
}
|
||||
|
||||
@@ -286,7 +319,9 @@ func readPassword(in io.Reader) (password string, err error) {
|
||||
// password.
|
||||
func readPasswordTerminal(in *os.File, out io.Writer, prompt string) (password string, err error) {
|
||||
fmt.Fprint(out, prompt)
|
||||
isReadingPassword = true
|
||||
buf, err := terminal.ReadPassword(int(in.Fd()))
|
||||
isReadingPassword = false
|
||||
fmt.Fprintln(out)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "ReadPassword")
|
||||
@@ -364,6 +399,14 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
||||
Warnf("%v returned error, retrying after %v: %v\n", msg, d, err)
|
||||
})
|
||||
|
||||
// wrap backend if a test specified a hook
|
||||
if opts.backendTestHook != nil {
|
||||
be, err = opts.backendTestHook(be)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s := repository.New(be)
|
||||
|
||||
passwordTriesLeft := 1
|
||||
|
||||
28
cmd/restic/global_test.go
Normal file
28
cmd/restic/global_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func Test_PrintFunctionsRespectsGlobalStdout(t *testing.T) {
|
||||
gopts := globalOptions
|
||||
defer func() {
|
||||
globalOptions = gopts
|
||||
}()
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
globalOptions.stdout = buf
|
||||
|
||||
for _, p := range []func(){
|
||||
func() { Println("message") },
|
||||
func() { Print("message\n") },
|
||||
func() { Printf("mes%s\n", "sage") },
|
||||
} {
|
||||
p()
|
||||
rtest.Equals(t, "message\n", buf.String())
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -54,7 +55,7 @@ func walkDir(dir string) <-chan *dirEntry {
|
||||
}()
|
||||
|
||||
// first element is root
|
||||
_ = <-ch
|
||||
<-ch
|
||||
|
||||
return ch
|
||||
}
|
||||
@@ -72,27 +73,16 @@ func sameModTime(fi1, fi2 os.FileInfo) bool {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return fi1.ModTime().Equal(fi2.ModTime())
|
||||
}
|
||||
|
||||
// directoriesEqualContents checks if both directories contain exactly the same
|
||||
// contents.
|
||||
func directoriesEqualContents(dir1, dir2 string) bool {
|
||||
// directoriesContentsDiff returns a diff between both directories. If these
|
||||
// contain exactly the same contents, then the diff is an empty string.
|
||||
func directoriesContentsDiff(dir1, dir2 string) string {
|
||||
var out bytes.Buffer
|
||||
ch1 := walkDir(dir1)
|
||||
ch2 := walkDir(dir2)
|
||||
|
||||
changes := false
|
||||
|
||||
var a, b *dirEntry
|
||||
for {
|
||||
var ok bool
|
||||
@@ -116,36 +106,27 @@ func directoriesEqualContents(dir1, dir2 string) bool {
|
||||
}
|
||||
|
||||
if ch1 == nil {
|
||||
fmt.Printf("+%v\n", b.path)
|
||||
changes = true
|
||||
fmt.Fprintf(&out, "+%v\n", b.path)
|
||||
} else if ch2 == nil {
|
||||
fmt.Printf("-%v\n", a.path)
|
||||
changes = true
|
||||
} else if !a.equals(b) {
|
||||
fmt.Fprintf(&out, "-%v\n", a.path)
|
||||
} else if !a.equals(&out, b) {
|
||||
if a.path < b.path {
|
||||
fmt.Printf("-%v\n", a.path)
|
||||
changes = true
|
||||
fmt.Fprintf(&out, "-%v\n", a.path)
|
||||
a = nil
|
||||
continue
|
||||
} else if a.path > b.path {
|
||||
fmt.Printf("+%v\n", b.path)
|
||||
changes = true
|
||||
fmt.Fprintf(&out, "+%v\n", b.path)
|
||||
b = nil
|
||||
continue
|
||||
} else {
|
||||
fmt.Printf("%%%v\n", a.path)
|
||||
changes = true
|
||||
fmt.Fprintf(&out, "%%%v\n", a.path)
|
||||
}
|
||||
}
|
||||
|
||||
a, b = nil, nil
|
||||
}
|
||||
|
||||
if changes {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return out.String()
|
||||
}
|
||||
|
||||
type dirStat struct {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user