mirror of
https://github.com/restic/restic.git
synced 2026-02-22 16:56:24 +00:00
Compare commits
581 Commits
add-config
...
v0.9.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b723ca3de5 | ||
|
|
f5084d70d7 | ||
|
|
29b7b17491 | ||
|
|
e14c4b1737 | ||
|
|
745d79fe5f | ||
|
|
fb95426f64 | ||
|
|
4cadc89ad3 | ||
|
|
409909a7f5 | ||
|
|
df500a372d | ||
|
|
a444731dc0 | ||
|
|
a6e8af7e0f | ||
|
|
aa5af8af0e | ||
|
|
4e3353109d | ||
|
|
02c8d38095 | ||
|
|
fd6211653c | ||
|
|
3d4f2dd6b4 | ||
|
|
c1ddc0c18b | ||
|
|
c95f032a9c | ||
|
|
3087776135 | ||
|
|
b6f01ffbe6 | ||
|
|
41fe9318b1 | ||
|
|
8387d18d4d | ||
|
|
929d2b8df3 | ||
|
|
4f0682d730 | ||
|
|
967d1bbf0c | ||
|
|
2f80b37b93 | ||
|
|
4d2aa18273 | ||
|
|
6b1e5d4e18 | ||
|
|
26d1f9f4ba | ||
|
|
6a89c0f0ef | ||
|
|
b87230b93d | ||
|
|
6f2b8d622a | ||
|
|
90440212f2 | ||
|
|
3a5c9aadad | ||
|
|
a78142c1bb | ||
|
|
07045c7e23 | ||
|
|
0a5d42db3f | ||
|
|
67d99b8cfb | ||
|
|
1a0c0dc277 | ||
|
|
e86d9307d0 | ||
|
|
923e681af3 | ||
|
|
37770b1d82 | ||
|
|
02fea4f76a | ||
|
|
e6db3596f1 | ||
|
|
3acc7af310 | ||
|
|
5c4653f427 | ||
|
|
f7317a9287 | ||
|
|
30db8057e4 | ||
|
|
0e897ef7b8 | ||
|
|
b3e727f40d | ||
|
|
17feccd998 | ||
|
|
604b18aa74 | ||
|
|
01c51b3449 | ||
|
|
de8cf5e345 | ||
|
|
cfa2ac69e0 | ||
|
|
1e9eefa066 | ||
|
|
e9af012229 | ||
|
|
8066e93f47 | ||
|
|
e19622e4b1 | ||
|
|
38ea7ed4f6 | ||
|
|
76d1866444 | ||
|
|
8b22fe29cf | ||
|
|
02014be76c | ||
|
|
16eeed2ad5 | ||
|
|
3f94f63967 | ||
|
|
88716794e3 | ||
|
|
3ca424050f | ||
|
|
fea2464d4d | ||
|
|
5bd5db4294 | ||
|
|
4429a66b5f | ||
|
|
8066195e6e | ||
|
|
f7f14cf8c9 | ||
|
|
5096f3b491 | ||
|
|
cf3fc2a5b1 | ||
|
|
920d458a4a | ||
|
|
b016dc2ff0 | ||
|
|
355db0bc29 | ||
|
|
6e2fe73189 | ||
|
|
303a5dab6a | ||
|
|
7dcd2968b6 | ||
|
|
298f490195 | ||
|
|
37cb82b28b | ||
|
|
bce6438d22 | ||
|
|
919dd2ac84 | ||
|
|
870bc5108e | ||
|
|
418296c5c9 | ||
|
|
a6481b3707 | ||
|
|
00b527fb09 | ||
|
|
0ebfc55ee3 | ||
|
|
35b7607802 | ||
|
|
fad9f65c65 | ||
|
|
939f3e972c | ||
|
|
ca8c3b4fd5 | ||
|
|
4f45b14f25 | ||
|
|
389067fb8b | ||
|
|
4b0ca9ddab | ||
|
|
b8c2544dcb | ||
|
|
c7762453cf | ||
|
|
303210aa08 | ||
|
|
c029881379 | ||
|
|
6e89963c21 | ||
|
|
1ac560181b | ||
|
|
18ec27a0da | ||
|
|
b40dea29ad | ||
|
|
0561155963 | ||
|
|
1aafc17212 | ||
|
|
f11789c437 | ||
|
|
8cab0c121d | ||
|
|
5979414bcd | ||
|
|
cc8b690b52 | ||
|
|
a164dc9391 | ||
|
|
9a26be4e5b | ||
|
|
733519d895 | ||
|
|
3d5a0c799b | ||
|
|
c4475ac58f | ||
|
|
c9fd9b5275 | ||
|
|
cadcab5a19 | ||
|
|
5ac9c1157a | ||
|
|
5715517e29 | ||
|
|
ecc2458de8 | ||
|
|
2c6ba5d9ac | ||
|
|
0cc3647e51 | ||
|
|
6b700d02f5 | ||
|
|
2b09a10234 | ||
|
|
1c87d01bad | ||
|
|
78a3ffcfb9 | ||
|
|
4d77c0c21c | ||
|
|
fb064afa34 | ||
|
|
7304738872 | ||
|
|
66efa425bf | ||
|
|
d51e9d1b98 | ||
|
|
e046428c94 | ||
|
|
75906edef5 | ||
|
|
203d775190 | ||
|
|
ecd7ee85e8 | ||
|
|
2022355800 | ||
|
|
36f22a0feb | ||
|
|
f58a44b911 | ||
|
|
fe886a6439 | ||
|
|
be23313072 | ||
|
|
3c112d9cae | ||
|
|
2970e38d92 | ||
|
|
870e7583a1 | ||
|
|
db1c835c37 | ||
|
|
190bed9908 | ||
|
|
85f4c826db | ||
|
|
5da4b0fc7d | ||
|
|
c1058005c3 | ||
|
|
ca73808649 | ||
|
|
f2ea91df38 | ||
|
|
15cc4d74b2 | ||
|
|
bf9a507148 | ||
|
|
65b476ead9 | ||
|
|
aaa1cc2c26 | ||
|
|
95434cff16 | ||
|
|
1b94ae1c00 | ||
|
|
d138b38f28 | ||
|
|
db8f5864fc | ||
|
|
1d8b21cdad | ||
|
|
3865b59716 | ||
|
|
7b8d1dc040 | ||
|
|
d19a29f79e | ||
|
|
449c049ce9 | ||
|
|
9f436d80e1 | ||
|
|
e277a92a2f | ||
|
|
d9e22c2df1 | ||
|
|
4b0fb5af36 | ||
|
|
7519c73987 | ||
|
|
45a48eb4a8 | ||
|
|
a2f30cde4c | ||
|
|
6ebcfe7c18 | ||
|
|
0022926eba | ||
|
|
3e3a0220ec | ||
|
|
c125fb763d | ||
|
|
b9f0f031b6 | ||
|
|
aa7043151a | ||
|
|
ebf22a35f4 | ||
|
|
3f069ac404 | ||
|
|
56e5467096 | ||
|
|
5ee932a124 | ||
|
|
fed25714a4 | ||
|
|
8906d85ab8 | ||
|
|
97aafc1eec | ||
|
|
6a5c9f57c2 | ||
|
|
6cf13483b5 | ||
|
|
f645306a18 | ||
|
|
186e10e0cb | ||
|
|
29a5bd5b30 | ||
|
|
06a01bc016 | ||
|
|
cdc287a7f6 | ||
|
|
deedc38129 | ||
|
|
1107eef215 | ||
|
|
60c7020bcb | ||
|
|
b96ef48562 | ||
|
|
cd9b2295f1 | ||
|
|
a439cdeb05 | ||
|
|
827f6d7b24 | ||
|
|
77ab10d401 | ||
|
|
3b0ad2e368 | ||
|
|
2996c110f1 | ||
|
|
4609b5c24d | ||
|
|
830511460a | ||
|
|
0dc3648416 | ||
|
|
d71dba3788 | ||
|
|
e482633943 | ||
|
|
900621051a | ||
|
|
1f246c5309 | ||
|
|
e40805b002 | ||
|
|
6f69ae1b8d | ||
|
|
c4fbf2c779 | ||
|
|
7c084014fa | ||
|
|
d65bea1b2a | ||
|
|
3b68acf853 | ||
|
|
82a70643a2 | ||
|
|
0dd805421e | ||
|
|
16b82f4b1d | ||
|
|
7a6bfcd58c | ||
|
|
de54618852 | ||
|
|
98526b8dbe | ||
|
|
0083680d33 | ||
|
|
05222b7343 | ||
|
|
d4ff5b6bf4 | ||
|
|
cf0883e16c | ||
|
|
a35a24b8b4 | ||
|
|
df7f72cdde | ||
|
|
3edc723bf0 | ||
|
|
71891b340c | ||
|
|
6f5c3e57f6 | ||
|
|
56af0ce370 | ||
|
|
c9745cd47e | ||
|
|
2434ab2106 | ||
|
|
1688713400 | ||
|
|
00597284de | ||
|
|
879f6e0c81 | ||
|
|
8a97bb8661 | ||
|
|
5fe6de219d | ||
|
|
c13f79da02 | ||
|
|
db82e6b80c | ||
|
|
6dc7cca597 | ||
|
|
d32c7c2aba | ||
|
|
09e9b74cbd | ||
|
|
d53595e43c | ||
|
|
0de19cc87f | ||
|
|
2c9ec07d0b | ||
|
|
a7971a3ece | ||
|
|
4ab0022da8 | ||
|
|
4b3c054257 | ||
|
|
7486bfea5b | ||
|
|
c8fc72364a | ||
|
|
987ef2f4a9 | ||
|
|
5b95bb7059 | ||
|
|
8471a359ee | ||
|
|
f9422ff4c7 | ||
|
|
c0572ca15f | ||
|
|
a630d69e0c | ||
|
|
20bcd281a3 | ||
|
|
c012fccd22 | ||
|
|
920727dd34 | ||
|
|
157d365894 | ||
|
|
bfa18ee8ec | ||
|
|
890eebf151 | ||
|
|
9310cd0cd6 | ||
|
|
9f7ce7ce5a | ||
|
|
0b600d6cef | ||
|
|
3ae2a79bdf | ||
|
|
f7c0893f76 | ||
|
|
c3de301fc8 | ||
|
|
944b446ac0 | ||
|
|
b096fc7abf | ||
|
|
d10754e2b4 | ||
|
|
7ac683c360 | ||
|
|
6caa9d38ac | ||
|
|
19fd0f101f | ||
|
|
8c91c51d1b | ||
|
|
7e28bf7e97 | ||
|
|
43d6e426c8 | ||
|
|
26fc60e7cb | ||
|
|
e5d7879622 | ||
|
|
d2ee58f2e9 | ||
|
|
3f25537a06 | ||
|
|
d203ae37f4 | ||
|
|
6eedd66c1a | ||
|
|
e4b39ae553 | ||
|
|
7cbcb6d318 | ||
|
|
c0fca3f50a | ||
|
|
4c2072d875 | ||
|
|
92ecca1808 | ||
|
|
7236635cc1 | ||
|
|
21a3486ebb | ||
|
|
bda8d7722e | ||
|
|
c2bcb764cd | ||
|
|
9e24154ec9 | ||
|
|
9f3ca97ee8 | ||
|
|
32d5ceba87 | ||
|
|
e010f3b884 | ||
|
|
941202c119 | ||
|
|
c021ad2334 | ||
|
|
2b3420820b | ||
|
|
da57302fca | ||
|
|
1869930d95 | ||
|
|
1213d8fef4 | ||
|
|
a432b42c81 | ||
|
|
7d0f2eaf24 | ||
|
|
41a4d67d93 | ||
|
|
afde60e433 | ||
|
|
d7baa67acb | ||
|
|
167397c18c | ||
|
|
be36c5f150 | ||
|
|
9484a14ab2 | ||
|
|
0f5fc8fb3d | ||
|
|
a5b40e9372 | ||
|
|
c5ec4efe91 | ||
|
|
e64a0e0454 | ||
|
|
8b5b031f90 | ||
|
|
4a2134bbc5 | ||
|
|
484844aa1a | ||
|
|
4ed10239ad | ||
|
|
c4896ed642 | ||
|
|
29aaec383c | ||
|
|
0cb241b7d3 | ||
|
|
de4750b8e0 | ||
|
|
7b91c40e21 | ||
|
|
cc9bf02da1 | ||
|
|
b7959c44d2 | ||
|
|
277cba4b32 | ||
|
|
ed651df19b | ||
|
|
641dc65e6e | ||
|
|
de9136b29f | ||
|
|
b36345fd84 | ||
|
|
03402c8a04 | ||
|
|
966e5a5575 | ||
|
|
5aa0deeff9 | ||
|
|
af4d822380 | ||
|
|
fd95b86894 | ||
|
|
5dbef3712e | ||
|
|
63647e93e4 | ||
|
|
9b8deb51ba | ||
|
|
2c4b0d975e | ||
|
|
8ceda538ef | ||
|
|
233596f4bc | ||
|
|
6712ee8f92 | ||
|
|
0916ff71bd | ||
|
|
5971650f77 | ||
|
|
19725954ee | ||
|
|
b1e1b71bab | ||
|
|
f1799de309 | ||
|
|
585a5e3416 | ||
|
|
b7eeeedc3f | ||
|
|
a20d4bc6b0 | ||
|
|
fb31d66951 | ||
|
|
33dfbf5c38 | ||
|
|
d1df3718b5 | ||
|
|
e2da0a416c | ||
|
|
0c0a8e3d2b | ||
|
|
0882aca3a8 | ||
|
|
cd41915e10 | ||
|
|
2effacd444 | ||
|
|
c6901ff908 | ||
|
|
2f774acce3 | ||
|
|
5f8658238c | ||
|
|
2bb1be4d4e | ||
|
|
40e0016403 | ||
|
|
541d232f1c | ||
|
|
6bc99ce451 | ||
|
|
e42d2d1da8 | ||
|
|
bd9022962e | ||
|
|
91f1b40206 | ||
|
|
d9b89eead0 | ||
|
|
5399297de6 | ||
|
|
96f7be5d9b | ||
|
|
0922367308 | ||
|
|
e2d9900d82 | ||
|
|
1140950d7b | ||
|
|
6d9c008900 | ||
|
|
b617444158 | ||
|
|
e588c42646 | ||
|
|
14bb2a9005 | ||
|
|
f04d347e7a | ||
|
|
746182c526 | ||
|
|
08beb7d84c | ||
|
|
9795b00f51 | ||
|
|
bfc1bc6ee6 | ||
|
|
e9cdcf131c | ||
|
|
35e9885e8b | ||
|
|
16885529f7 | ||
|
|
3c02eeb5a8 | ||
|
|
9e9bb62ad4 | ||
|
|
175e630717 | ||
|
|
44f38ad049 | ||
|
|
ca928aeae4 | ||
|
|
27b60a05b4 | ||
|
|
8af4b331ef | ||
|
|
a5a46e4989 | ||
|
|
e4cdb0eab3 | ||
|
|
e9a764129f | ||
|
|
65129bde5e | ||
|
|
b4beaf807b | ||
|
|
4734056583 | ||
|
|
71e0408390 | ||
|
|
1352a9d848 | ||
|
|
e0f68ec2c0 | ||
|
|
9c6e0c6eb9 | ||
|
|
4cbc7c4467 | ||
|
|
aaff8803ef | ||
|
|
16e20676b6 | ||
|
|
6cd5f8b7f5 | ||
|
|
10c0b8080e | ||
|
|
d31666d332 | ||
|
|
6d53e767d5 | ||
|
|
f1b0bb33dd | ||
|
|
99ae913414 | ||
|
|
df78896e59 | ||
|
|
c896751ce2 | ||
|
|
501189625e | ||
|
|
a065ada46a | ||
|
|
17d6d537e2 | ||
|
|
5cc224e44a | ||
|
|
896089976a | ||
|
|
a563f87818 | ||
|
|
de307ea2ab | ||
|
|
4bc904a527 | ||
|
|
5937b5b355 | ||
|
|
76387b6cd0 | ||
|
|
9aa36a37c7 | ||
|
|
9fd3796d93 | ||
|
|
93fa17b53f | ||
|
|
15ad0e5bc7 | ||
|
|
1f27d17c0d | ||
|
|
8af918a1e4 | ||
|
|
bb5425a1d8 | ||
|
|
12246969db | ||
|
|
9151eec24e | ||
|
|
22475729ce | ||
|
|
04c67d700d | ||
|
|
d708d607fa | ||
|
|
46f71f4c22 | ||
|
|
48cc2f2188 | ||
|
|
bd6e7c934c | ||
|
|
7925217e25 | ||
|
|
401a564486 | ||
|
|
31176d212b | ||
|
|
2d89311d49 | ||
|
|
5a25ad1972 | ||
|
|
79d3a18b31 | ||
|
|
89f17847ad | ||
|
|
1ab5703404 | ||
|
|
49d95e9a50 | ||
|
|
7dff1a08d0 | ||
|
|
5fee36fa84 | ||
|
|
b0211dff49 | ||
|
|
0f6d21cf84 | ||
|
|
10b5cf8f32 | ||
|
|
ad5aec3f3b | ||
|
|
6e1a3987b7 | ||
|
|
9630398e3b | ||
|
|
7e34de4c29 | ||
|
|
ace5cc4ed3 | ||
|
|
7f617cfd7f | ||
|
|
0deb4e5994 | ||
|
|
6b9dde3ce8 | ||
|
|
c145b618d4 | ||
|
|
b07bb3d8c3 | ||
|
|
9b513312e2 | ||
|
|
bf26a3ed57 | ||
|
|
77a8d931b8 | ||
|
|
11ce572894 | ||
|
|
7a468d1226 | ||
|
|
00e2fd8b5f | ||
|
|
0f83fea007 | ||
|
|
04f7c054cd | ||
|
|
5dd0df0162 | ||
|
|
abc923f693 | ||
|
|
ac3bd6b2eb | ||
|
|
156d85a29b | ||
|
|
8c146eac4b | ||
|
|
6f5b0f3622 | ||
|
|
beb208e159 | ||
|
|
c221d662d0 | ||
|
|
143597d445 | ||
|
|
16ca837763 | ||
|
|
ce7fb166b3 | ||
|
|
9de51d04ec | ||
|
|
dc39773cd2 | ||
|
|
30fa305c07 | ||
|
|
686f24b578 | ||
|
|
247d2b7215 | ||
|
|
017cd113d3 | ||
|
|
f744c2553e | ||
|
|
56cd6bd495 | ||
|
|
bff635bc5f | ||
|
|
3422c1ca83 | ||
|
|
f6b2731aa5 | ||
|
|
3eb5b45b41 | ||
|
|
01aacf41b5 | ||
|
|
2caf8edc55 | ||
|
|
3151978f58 | ||
|
|
ab4ef432ff | ||
|
|
be4f54b603 | ||
|
|
7260110c27 | ||
|
|
2437f11af7 | ||
|
|
57873502f8 | ||
|
|
3678ec9ad8 | ||
|
|
a717e9e6f7 | ||
|
|
12c797700e | ||
|
|
daca9d6815 | ||
|
|
930602a444 | ||
|
|
acb05e7855 | ||
|
|
a7b95d716a | ||
|
|
925b542eb0 | ||
|
|
f7659bd8b0 | ||
|
|
8c124a2b75 | ||
|
|
d3ad63a4ec | ||
|
|
271c50cf5c | ||
|
|
1aeb193fd9 | ||
|
|
f715bef82f | ||
|
|
4fc00d4120 | ||
|
|
7603ab7ac1 | ||
|
|
36fa1f8c20 | ||
|
|
445fb23b6d | ||
|
|
5f79b4cb6c | ||
|
|
8e15b59347 | ||
|
|
6e2e957332 | ||
|
|
7ffc03ff8f | ||
|
|
44924ba043 | ||
|
|
ce19f26948 | ||
|
|
74016d5981 | ||
|
|
57636a4573 | ||
|
|
4f6d2502f7 | ||
|
|
f1f69bc648 | ||
|
|
d7551d7b0c | ||
|
|
fb74de6360 | ||
|
|
67535e00a8 | ||
|
|
19592285eb | ||
|
|
f64862722a | ||
|
|
254239c2a9 | ||
|
|
cce1a1f768 | ||
|
|
754482fe6c | ||
|
|
73153dbd3f | ||
|
|
92421ec47f | ||
|
|
9acc9243ba | ||
|
|
df64998649 | ||
|
|
64d27eed86 | ||
|
|
abb18a830c | ||
|
|
1e42f4f300 | ||
|
|
bd742ddb69 | ||
|
|
b511f4dce2 | ||
|
|
7961740dcc | ||
|
|
dc3032c360 | ||
|
|
44fb2a860f | ||
|
|
fbf8073dfc | ||
|
|
7ddf91b65c | ||
|
|
8dae2de2ce | ||
|
|
03a0377410 | ||
|
|
025ec9dff5 | ||
|
|
2384c1cee7 | ||
|
|
bb2ad76833 | ||
|
|
30cfd13328 | ||
|
|
9ffc26883a | ||
|
|
83c51db903 | ||
|
|
d30d5d4473 | ||
|
|
5088905502 | ||
|
|
ae72b438b0 | ||
|
|
ddf2065ce2 | ||
|
|
228a970540 | ||
|
|
c7a8086c19 | ||
|
|
c2c06ae2c9 | ||
|
|
1824168aa3 | ||
|
|
350761f1ba | ||
|
|
3231945a85 | ||
|
|
f080142137 | ||
|
|
ff785924de | ||
|
|
393a7266c9 | ||
|
|
cb8d2d3df5 | ||
|
|
a884ce1566 | ||
|
|
5ae8316c24 | ||
|
|
85eca1b5e9 | ||
|
|
a1536f38fa | ||
|
|
888f52afd1 | ||
|
|
e206680947 | ||
|
|
5fa6dc53cb | ||
|
|
26be094f28 |
72
.github/ISSUE_TEMPLATE.md
vendored
72
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,17 +1,9 @@
|
||||
<!--
|
||||
|
||||
Welcome! - We kindly ask that you:
|
||||
Welcome! If you have a question or are unsure if you should open an issue,
|
||||
please use the forum instead!
|
||||
|
||||
1. Fill out the issue template below - not doing so needs a good reason.
|
||||
2. Use the forum if you have a question rather than a bug or feature request.
|
||||
|
||||
The forum is at: https://forum.restic.net
|
||||
|
||||
NOTE: Not filling out the issue template needs a good reason, as otherwise it
|
||||
may take a lot longer to find the problem, not to mention it can take up a lot
|
||||
more time which can otherwise be spent on development. Please also take the
|
||||
time to help us debug the issue by collecting relevant information, even if
|
||||
it doesn't seem to be relevant to you. Thanks!
|
||||
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
|
||||
@@ -19,61 +11,17 @@ 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`
|
||||
|
||||
|
||||
## How did you run restic exactly?
|
||||
Output of `restic version`
|
||||
--------------------------
|
||||
|
||||
<!--
|
||||
This section should include at least:
|
||||
|
||||
* The complete command line and any environment variables you used to
|
||||
configure restic's backend access. Make sure to replace sensitive values!
|
||||
|
||||
* The output of the commands, what restic prints gives may give us much
|
||||
information to diagnose the problem!
|
||||
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.
|
||||
-->
|
||||
|
||||
## What backend/server/service did you use to store the repository?
|
||||
|
||||
|
||||
|
||||
## Expected behavior
|
||||
|
||||
<!--
|
||||
Describe what you'd like restic to do differently.
|
||||
-->
|
||||
|
||||
## Actual behavior
|
||||
|
||||
<!--
|
||||
In this section, please try to concentrate on observations, so only describe
|
||||
what you observed directly.
|
||||
-->
|
||||
|
||||
## Steps to reproduce the behavior
|
||||
|
||||
<!--
|
||||
The more time you spend describing an easy way to reproduce the behavior (if
|
||||
this is possible), the easier it is for the project developers to fix it!
|
||||
-->
|
||||
|
||||
## Do you have any idea what may have caused this?
|
||||
|
||||
|
||||
|
||||
## Do you have an idea how to solve the issue?
|
||||
|
||||
|
||||
|
||||
## Did restic help you or made you happy in any way?
|
||||
|
||||
<!--
|
||||
Answering this question is not required, but if you have anything positive to share, please do so here!
|
||||
Sometimes we get tired of reading bug reports all day and a little positive end note does wonders.
|
||||
Idea by Joey Hess, https://joeyh.name/blog/entry/two_holiday_stories/
|
||||
-->
|
||||
Describe the issue
|
||||
------------------
|
||||
|
||||
93
.github/ISSUE_TEMPLATE/Bug.md
vendored
Normal file
93
.github/ISSUE_TEMPLATE/Bug.md
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a problem with restic to help us resolve it and improve
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Welcome! - We kindly ask that you:
|
||||
|
||||
1. Fill out the issue template below - not doing so needs a good reason.
|
||||
2. Use the forum if you have a question rather than a bug or feature request.
|
||||
|
||||
The forum is at: https://forum.restic.net
|
||||
|
||||
NOTE: Not filling out the issue template needs a good reason, as otherwise it
|
||||
may take a lot longer to find the problem, not to mention it can take up a lot
|
||||
more time which can otherwise be spent on development. Please also take the
|
||||
time to help us debug the issue by collecting relevant information, even if
|
||||
it doesn't seem to be relevant to you. Thanks!
|
||||
|
||||
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`
|
||||
--------------------------
|
||||
|
||||
|
||||
How did you run restic exactly?
|
||||
-------------------------------
|
||||
|
||||
<!--
|
||||
This section should include at least:
|
||||
|
||||
* The complete command line and any environment variables you used to
|
||||
configure restic's backend access. Make sure to replace sensitive values!
|
||||
|
||||
* The output of the commands, what restic prints gives may give us much
|
||||
information to diagnose the problem!
|
||||
-->
|
||||
|
||||
What backend/server/service did you use to store the repository?
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Expected behavior
|
||||
-----------------
|
||||
|
||||
<!--
|
||||
Describe what you'd like restic to do differently.
|
||||
-->
|
||||
|
||||
Actual behavior
|
||||
---------------
|
||||
|
||||
<!--
|
||||
In this section, please try to concentrate on observations, so only describe
|
||||
what you observed directly.
|
||||
-->
|
||||
|
||||
Steps to reproduce the behavior
|
||||
-------------------------------
|
||||
|
||||
<!--
|
||||
The more time you spend describing an easy way to reproduce the behavior (if
|
||||
this is possible), the easier it is for the project developers to fix it!
|
||||
-->
|
||||
|
||||
Do you have any idea what may have caused this?
|
||||
-----------------------------------------------
|
||||
|
||||
|
||||
|
||||
Do you have an idea how to solve the issue?
|
||||
-------------------------------------------
|
||||
|
||||
|
||||
|
||||
Did restic help you or made you happy in any way?
|
||||
-------------------------------------------------
|
||||
|
||||
<!--
|
||||
Answering this question is not required, but if you have anything positive to share, please do so here!
|
||||
Sometimes we get tired of reading bug reports all day and a little positive end note does wonders.
|
||||
Idea by Joey Hess, https://joeyh.name/blog/entry/two_holiday_stories/
|
||||
-->
|
||||
57
.github/ISSUE_TEMPLATE/Feature.md
vendored
Normal file
57
.github/ISSUE_TEMPLATE/Feature.md
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature or enhancement for restic
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Welcome! - We kindly ask that you:
|
||||
|
||||
1. Fill out the issue template below - not doing so needs a good reason.
|
||||
2. Use the forum if you have a question rather than a bug or feature request.
|
||||
|
||||
The forum is at: 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.
|
||||
-->
|
||||
|
||||
What should restic do differently? Which functionality do you think we should add?
|
||||
----------------------------------------------------------------------------------
|
||||
|
||||
<!--
|
||||
Please describe the feature you'd like us to add here.
|
||||
-->
|
||||
|
||||
|
||||
What are you trying to do?
|
||||
--------------------------
|
||||
|
||||
<!--
|
||||
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?
|
||||
-------------------------------------------------
|
||||
|
||||
<!--
|
||||
Answering this question is not required, but if you have anything positive to share, please do so here!
|
||||
Sometimes we get tired of reading bug reports all day and a little positive end note does wonders.
|
||||
Idea by Joey Hess, https://joeyh.name/blog/entry/two_holiday_stories/
|
||||
-->
|
||||
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
<!--
|
||||
Thank you very much for contributing code or documentation to restic! Please
|
||||
fill out the following questions to make it easier for us to review your
|
||||
@@ -8,19 +10,25 @@ 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 is the purpose of this change? What does it change?
|
||||
--------------------------------------------------------
|
||||
|
||||
<!--
|
||||
Describe the changes here, as detailed as needed.
|
||||
-->
|
||||
|
||||
### Was the change discussed in an issue or in the forum before?
|
||||
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.
|
||||
-->
|
||||
|
||||
### Checklist
|
||||
Checklist
|
||||
---------
|
||||
|
||||
- [ ] I have read the [Contribution Guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches)
|
||||
- [ ] I have added tests for all changes in this PR
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
/restic
|
||||
/.vagrant
|
||||
/doc/_build
|
||||
|
||||
39
.travis.yml
39
.travis.yml
@@ -4,17 +4,45 @@ sudo: false
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
go: "1.9.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0 RESTIC_BUILD_SOLARIS=0
|
||||
go: "1.10.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.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"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
# only run fuse and cloud backends tests on Travis for the latest Go on Linux
|
||||
- os: linux
|
||||
go: "1.10.x"
|
||||
go: "1.13.x"
|
||||
sudo: true
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
- os: osx
|
||||
go: "1.10.x"
|
||||
go: "1.13.x"
|
||||
env: RESTIC_TEST_FUSE=0 RESTIC_TEST_CLOUD_BACKENDS=0
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/Library/Caches/go-build
|
||||
- $HOME/gopath/pkg/mod
|
||||
|
||||
branches:
|
||||
only:
|
||||
@@ -36,6 +64,3 @@ install:
|
||||
|
||||
script:
|
||||
- go run run_integration_tests.go
|
||||
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash) -f all.cov
|
||||
|
||||
666
CHANGELOG.md
666
CHANGELOG.md
@@ -1,3 +1,669 @@
|
||||
Changelog for restic 0.9.6 (2019-11-22)
|
||||
=======================================
|
||||
|
||||
The following sections list the changes in restic 0.9.6 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
* Fix #2063: Allow absolute path for filename when backing up from stdin
|
||||
* Fix #2174: Save files with invalid timestamps
|
||||
* 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 #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
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
* Bugfix #2063: Allow absolute path for filename when backing up from stdin
|
||||
|
||||
When backing up from stdin, handle directory path for `--stdin-filename`. This can be used to
|
||||
specify the full path for the backed-up file.
|
||||
|
||||
https://github.com/restic/restic/issues/2063
|
||||
|
||||
* Bugfix #2174: Save files with invalid timestamps
|
||||
|
||||
When restic reads invalid timestamps (year is before 0000 or after 9999) it refused to read and
|
||||
archive the file. We've changed the behavior and will now save modified timestamps with the
|
||||
year set to either 0000 or 9999, the rest of the timestamp stays the same, so the file will be saved
|
||||
(albeit with a bogus timestamp).
|
||||
|
||||
https://github.com/restic/restic/issues/2174
|
||||
https://github.com/restic/restic/issues/1173
|
||||
|
||||
* Bugfix #2249: Read fresh metadata for unmodified files
|
||||
|
||||
Restic took all metadata for files which were detected as unmodified, not taking into account
|
||||
changed metadata (ownership, mode). This is now corrected.
|
||||
|
||||
https://github.com/restic/restic/issues/2249
|
||||
https://github.com/restic/restic/pull/2252
|
||||
|
||||
* Bugfix #2301: Add upper bound for t in --read-data-subset=n/t
|
||||
|
||||
256 is the effective maximum for t, but restic would allow larger values, leading to strange
|
||||
behavior.
|
||||
|
||||
https://github.com/restic/restic/issues/2301
|
||||
https://github.com/restic/restic/pull/2304
|
||||
|
||||
* Bugfix #2321: Check errors when loading index files
|
||||
|
||||
Restic now checks and handles errors which occur when loading index files, the missing check
|
||||
leads to odd errors (and a stack trace printed to users) later. This was reported in the forum.
|
||||
|
||||
https://github.com/restic/restic/pull/2321
|
||||
https://forum.restic.net/t/check-rebuild-index-prune/1848/13
|
||||
|
||||
* 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
|
||||
the user again for the repository password if typed incorrectly. The user will now get three
|
||||
tries to input the correct password before restic quits.
|
||||
|
||||
https://github.com/restic/restic/issues/2306
|
||||
|
||||
* Enhancement #2330: Make `--group-by` accept both singular and plural
|
||||
|
||||
One can now use the values `host`/`hosts`, `path`/`paths` and `tag` / `tags` interchangeably
|
||||
in the `--group-by` argument.
|
||||
|
||||
https://github.com/restic/restic/issues/2330
|
||||
|
||||
* Enhancement #2350: Add option to configure S3 region
|
||||
|
||||
We've added a new option for setting the region when accessing an S3-compatible service. For
|
||||
some providers, it is required to set this to a valid value. You can do that either by setting the
|
||||
environment variable `AWS_DEFAULT_REGION` or using the option `s3.region`, e.g. like this:
|
||||
`-o s3.region="us-east-1"`.
|
||||
|
||||
https://github.com/restic/restic/pull/2350
|
||||
|
||||
|
||||
Changelog for restic 0.9.5 (2019-04-23)
|
||||
=======================================
|
||||
|
||||
The following sections list the changes in restic 0.9.5 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
* Fix #2135: Return error when no bytes could be read from stdin
|
||||
* Fix #2181: Don't cancel timeout after 30 seconds for self-update
|
||||
* Fix #2203: Fix reading passwords from stdin
|
||||
* Fix #2224: Don't abort the find command when a tree can't be loaded
|
||||
* 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
|
||||
* Enh #2139: Return error if no bytes could be read for `backup --stdin`
|
||||
* Enh #2205: Add --ignore-inode option to backup cmd
|
||||
* Enh #2220: Add config option to set S3 storage class
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
* Bugfix #2135: Return error when no bytes could be read from stdin
|
||||
|
||||
We assume that users reading backup data from stdin want to know when no data could be read, so now
|
||||
restic returns an error when `backup --stdin` is called but no bytes could be read. Usually,
|
||||
this means that an earlier command in a pipe has failed. The documentation was amended and now
|
||||
recommends setting the `pipefail` option (`set -o pipefail`).
|
||||
|
||||
https://github.com/restic/restic/pull/2135
|
||||
https://github.com/restic/restic/pull/2139
|
||||
|
||||
* Bugfix #2181: Don't cancel timeout after 30 seconds for self-update
|
||||
|
||||
https://github.com/restic/restic/issues/2181
|
||||
|
||||
* Bugfix #2203: Fix reading passwords from stdin
|
||||
|
||||
Passwords for the `init`, `key add`, and `key passwd` commands can now be read from
|
||||
non-terminal stdin.
|
||||
|
||||
https://github.com/restic/restic/issues/2203
|
||||
|
||||
* Bugfix #2224: Don't abort the find command when a tree can't be loaded
|
||||
|
||||
Change the find command so that missing trees don't result in a crash. Instead, the error is
|
||||
logged to the debug log, and the tree ID is displayed along with the snapshot it belongs to. This
|
||||
makes it possible to recover repositories that are missing trees by forgetting the snapshots
|
||||
they are used in.
|
||||
|
||||
https://github.com/restic/restic/issues/2224
|
||||
|
||||
* Enhancement #1895: Add case insensitive include & exclude options
|
||||
|
||||
The backup and restore commands now have --iexclude and --iinclude flags as case insensitive
|
||||
variants of --exclude and --include.
|
||||
|
||||
https://github.com/restic/restic/issues/1895
|
||||
https://github.com/restic/restic/pull/2032
|
||||
|
||||
* Enhancement #1937: Support streaming JSON output for backup
|
||||
|
||||
We've added support for getting machine-readable status output during backup, just pass the
|
||||
flag `--json` for `restic backup` and restic will output a stream of JSON objects which contain
|
||||
the current progress.
|
||||
|
||||
https://github.com/restic/restic/issues/1937
|
||||
https://github.com/restic/restic/pull/1944
|
||||
|
||||
* Enhancement #2155: Add Openstack application credential auth for Swift
|
||||
|
||||
Since Openstack Queens Identity (auth V3) service supports an application credential auth
|
||||
method. It allows to create a technical account with the limited roles. This commit adds an
|
||||
application credential authentication method for the Swift backend.
|
||||
|
||||
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
|
||||
(or would-be) kept and removed from the repository.
|
||||
|
||||
https://github.com/restic/restic/issues/2184
|
||||
https://github.com/restic/restic/pull/2185
|
||||
|
||||
* Enhancement #2037: Add group-by option to snapshots command
|
||||
|
||||
We have added an option to group the output of the snapshots command, similar to the output of the
|
||||
forget command. The option has been called "--group-by" and accepts any combination of the
|
||||
values "host", "paths" and "tags", separated by commas. Default behavior (not specifying
|
||||
--group-by) has not been changed. We have added support of the grouping to the JSON output.
|
||||
|
||||
https://github.com/restic/restic/issues/2037
|
||||
https://github.com/restic/restic/pull/2087
|
||||
|
||||
* Enhancement #2124: Ability to dump folders to tar via stdout
|
||||
|
||||
We've added the ability to dump whole folders to stdout via the `dump` command. Restic now
|
||||
requires at least Go 1.10 due to a limitation of the standard library for Go <= 1.9.
|
||||
|
||||
https://github.com/restic/restic/issues/2123
|
||||
https://github.com/restic/restic/pull/2124
|
||||
|
||||
* Enhancement #2139: Return error if no bytes could be read for `backup --stdin`
|
||||
|
||||
When restic is used to backup the output of a program, like `mysqldump | restic backup --stdin`,
|
||||
it now returns an error if no bytes could be read at all. This catches the failure case when
|
||||
`mysqldump` failed for some reason and did not output any data to stdout.
|
||||
|
||||
https://github.com/restic/restic/pull/2139
|
||||
|
||||
* Enhancement #2205: Add --ignore-inode option to backup cmd
|
||||
|
||||
This option handles backup of virtual filesystems that do not keep fixed inodes for files, like
|
||||
Fuse-based, pCloud, etc. Ignoring inode changes allows to consider the file as unchanged if
|
||||
last modification date and size are unchanged.
|
||||
|
||||
https://github.com/restic/restic/issues/1631
|
||||
https://github.com/restic/restic/pull/2205
|
||||
https://github.com/restic/restic/pull/2047
|
||||
|
||||
* Enhancement #2220: Add config option to set S3 storage class
|
||||
|
||||
The `s3.storage-class` option can be passed to restic (using `-o`) to specify the storage
|
||||
class to be used for S3 objects created by restic.
|
||||
|
||||
The storage class is passed as-is to S3, so it needs to be understood by the API. On AWS, it can be
|
||||
one of `STANDARD`, `STANDARD_IA`, `ONEZONE_IA`, `INTELLIGENT_TIERING` and
|
||||
`REDUCED_REDUNDANCY`. If unspecified, the default storage class is used (`STANDARD` on
|
||||
AWS).
|
||||
|
||||
You can mix storage classes in the same bucket, and the setting isn't stored in the restic
|
||||
repository, so be sure to specify it with each command that writes to S3.
|
||||
|
||||
https://github.com/restic/restic/issues/706
|
||||
https://github.com/restic/restic/pull/2220
|
||||
|
||||
|
||||
Changelog for restic 0.9.4 (2019-01-06)
|
||||
=======================================
|
||||
|
||||
The following sections list the changes in restic 0.9.4 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
* Fix #1989: Google Cloud Storage: Respect bandwidth limit
|
||||
* Fix #2040: Add host name filter shorthand flag for `stats` command
|
||||
* Fix #2068: Correctly return error loading data
|
||||
* Fix #2095: Consistently use local time for snapshots times
|
||||
* Enh #1605: Concurrent restore
|
||||
* Enh #2089: Increase granularity of the "keep within" retention policy
|
||||
* Enh #2097: Add key hinting
|
||||
* Enh #2017: Mount: Enforce FUSE Unix permissions with allow-other
|
||||
* Enh #2070: Make all commands display timestamps in local time
|
||||
* Enh #2085: Allow --files-from to be specified multiple times
|
||||
* Enh #2094: Run command to get password
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
* Bugfix #1989: Google Cloud Storage: Respect bandwidth limit
|
||||
|
||||
The GCS backend did not respect the bandwidth limit configured, a previous commit
|
||||
accidentally removed support for it.
|
||||
|
||||
https://github.com/restic/restic/issues/1989
|
||||
https://github.com/restic/restic/pull/2100
|
||||
|
||||
* Bugfix #2040: Add host name filter shorthand flag for `stats` command
|
||||
|
||||
The default value for `--host` flag was set to 'H' (the shorthand version of the flag), this
|
||||
caused the lookup for the latest snapshot to fail.
|
||||
|
||||
Add shorthand flag `-H` for `--host` (with empty default so if these flags are not specified the
|
||||
latest snapshot will not filter by host name).
|
||||
|
||||
Also add shorthand `-H` for `backup` command.
|
||||
|
||||
https://github.com/restic/restic/issues/2040
|
||||
|
||||
* Bugfix #2068: Correctly return error loading data
|
||||
|
||||
In one case during `prune` and `check`, an error loading data from the backend is not returned
|
||||
properly. This is now corrected.
|
||||
|
||||
https://github.com/restic/restic/issues/1999#issuecomment-433737921
|
||||
https://github.com/restic/restic/pull/2068
|
||||
|
||||
* Bugfix #2095: Consistently use local time for snapshots times
|
||||
|
||||
By default snapshots created with restic backup were set to local time, but when the --time flag
|
||||
was used the provided timestamp was parsed as UTC. With this change all snapshots times are set
|
||||
to local time.
|
||||
|
||||
https://github.com/restic/restic/pull/2095
|
||||
|
||||
* Enhancement #1605: Concurrent restore
|
||||
|
||||
This change significantly improves restore performance, especially when using
|
||||
high-latency remote repositories like B2.
|
||||
|
||||
The implementation now uses several concurrent threads to download and process multiple
|
||||
remote files concurrently. To further reduce restore time, each remote file is downloaded
|
||||
using a single repository request.
|
||||
|
||||
https://github.com/restic/restic/issues/1605
|
||||
https://github.com/restic/restic/pull/1719
|
||||
|
||||
* Enhancement #2089: Increase granularity of the "keep within" retention policy
|
||||
|
||||
The `keep-within` option of the `forget` command now accepts time ranges with an hourly
|
||||
granularity. For example, running `restic forget --keep-within 3d12h` will keep all the
|
||||
snapshots made within three days and twelve hours from the time of the latest snapshot.
|
||||
|
||||
https://github.com/restic/restic/issues/2089
|
||||
https://github.com/restic/restic/pull/2090
|
||||
|
||||
* Enhancement #2097: Add key hinting
|
||||
|
||||
Added a new option `--key-hint` and corresponding environment variable `RESTIC_KEY_HINT`.
|
||||
The key hint is a key ID to try decrypting first, before other keys in the repository.
|
||||
|
||||
This change will benefit repositories with many keys; if the correct key hint is supplied then
|
||||
restic only needs to check one key. If the key hint is incorrect (the key does not exist, or the
|
||||
password is incorrect) then restic will check all keys, as usual.
|
||||
|
||||
https://github.com/restic/restic/issues/2097
|
||||
|
||||
* Enhancement #2017: Mount: Enforce FUSE Unix permissions with allow-other
|
||||
|
||||
The fuse mount (`restic mount`) now lets the kernel check the permissions of the files within
|
||||
snapshots (this is done through the `DefaultPermissions` FUSE option) when the option
|
||||
`--allow-other` is specified.
|
||||
|
||||
To restore the old behavior, we've added the `--no-default-permissions` option. This allows
|
||||
all users that have access to the mount point to access all files within the snapshots.
|
||||
|
||||
https://github.com/restic/restic/pull/2017
|
||||
|
||||
* Enhancement #2070: Make all commands display timestamps in local time
|
||||
|
||||
Restic used to drop the timezone information from displayed timestamps, it now converts
|
||||
timestamps to local time before printing them so the times can be easily compared to.
|
||||
|
||||
https://github.com/restic/restic/pull/2070
|
||||
|
||||
* Enhancement #2085: Allow --files-from to be specified multiple times
|
||||
|
||||
Before, restic took only the last file specified with `--files-from` into account, this is now
|
||||
corrected.
|
||||
|
||||
https://github.com/restic/restic/issues/2085
|
||||
https://github.com/restic/restic/pull/2086
|
||||
|
||||
* Enhancement #2094: Run command to get password
|
||||
|
||||
We've added the `--password-command` option which allows specifying a command that restic
|
||||
runs every time the password for the repository is needed, so it can be integrated with a
|
||||
password manager or keyring. The option can also be set via the environment variable
|
||||
`$RESTIC_PASSWORD_COMMAND`.
|
||||
|
||||
https://github.com/restic/restic/pull/2094
|
||||
|
||||
|
||||
Changelog for restic 0.9.3 (2018-10-13)
|
||||
=======================================
|
||||
|
||||
The following sections list the changes in restic 0.9.3 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
* Fix #1935: Remove truncated files from cache
|
||||
* Fix #1978: Do not return an error when the scanner is slower than backup
|
||||
* Enh #1766: Restore: suppress lchown errors when not running as root
|
||||
* Enh #1909: Reject files/dirs by name first
|
||||
* Enh #1940: Add directory filter to ls command
|
||||
* Enh #1967: Use `--host` everywhere
|
||||
* Enh #2028: Display size of cache directories
|
||||
* Enh #1777: Improve the `find` command
|
||||
* Enh #1876: Display reason why forget keeps snapshots
|
||||
* Enh #1891: Accept glob in paths loaded via --files-from
|
||||
* Enh #1920: Vendor dependencies with Go 1.11 Modules
|
||||
* Enh #1949: Add new command `self-update`
|
||||
* Enh #1953: Ls: Add JSON output support for restic ls cmd
|
||||
* Enh #1962: Stream JSON output for ls command
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
* Bugfix #1935: Remove truncated files from cache
|
||||
|
||||
When a file in the local cache is truncated, and restic tries to access data beyond the end of the
|
||||
(cached) file, it used to return an error "EOF". This is now fixed, such truncated files are
|
||||
removed and the data is fetched directly from the backend.
|
||||
|
||||
https://github.com/restic/restic/issues/1935
|
||||
|
||||
* Bugfix #1978: Do not return an error when the scanner is slower than backup
|
||||
|
||||
When restic makes a backup, there's a background task called "scanner" which collects
|
||||
information on how many files and directories are to be saved, in order to display progress
|
||||
information to the user. When the backup finishes faster than the scanner, it is aborted
|
||||
because the result is not needed any more. This logic contained a bug, where quitting the
|
||||
scanner process was treated as an error, and caused restic to print an unhelpful error message
|
||||
("context canceled").
|
||||
|
||||
https://github.com/restic/restic/issues/1978
|
||||
https://github.com/restic/restic/pull/1991
|
||||
|
||||
* Enhancement #1766: Restore: suppress lchown errors when not running as root
|
||||
|
||||
Like "cp" and "rsync" do, restic now only reports errors for changing the ownership of files
|
||||
during restore if it is run as root, on non-Windows operating systems. On Windows, the error
|
||||
is reported as usual.
|
||||
|
||||
https://github.com/restic/restic/issues/1766
|
||||
|
||||
* Enhancement #1909: Reject files/dirs by name first
|
||||
|
||||
The current scanner/archiver code had an architectural limitation: it always ran the
|
||||
`lstat()` system call on all files and directories before a decision to include/exclude the
|
||||
file/dir was made. This lead to a lot of unnecessary system calls for items that could have been
|
||||
rejected by their name or path only.
|
||||
|
||||
We've changed the archiver/scanner implementation so that it now first rejects by name/path,
|
||||
and only runs the system call on the remaining items. This reduces the number of `lstat()`
|
||||
system calls a lot (depending on the exclude settings).
|
||||
|
||||
https://github.com/restic/restic/issues/1909
|
||||
https://github.com/restic/restic/pull/1912
|
||||
|
||||
* Enhancement #1940: Add directory filter to ls command
|
||||
|
||||
The ls command can now be filtered by directories, so that only files in the given directories
|
||||
will be shown. If the --recursive flag is specified, then ls will traverse subfolders and list
|
||||
their files as well.
|
||||
|
||||
It used to be possible to specify multiple snapshots, but that has been replaced by only one
|
||||
snapshot and the possibility of specifying multiple directories.
|
||||
|
||||
Specifying directories constrains the walk, which can significantly speed up the listing.
|
||||
|
||||
https://github.com/restic/restic/issues/1940
|
||||
https://github.com/restic/restic/pull/1941
|
||||
|
||||
* Enhancement #1967: Use `--host` everywhere
|
||||
|
||||
We now use the flag `--host` for all commands which need a host name, using `--hostname` (e.g.
|
||||
for `restic backup`) still works, but will print a deprecation warning. Also, add the short
|
||||
option `-H` where possible.
|
||||
|
||||
https://github.com/restic/restic/issues/1967
|
||||
|
||||
* Enhancement #2028: Display size of cache directories
|
||||
|
||||
The `cache` command now by default shows the size of the individual cache directories. It can be
|
||||
disabled with `--no-size`.
|
||||
|
||||
https://github.com/restic/restic/issues/2028
|
||||
https://github.com/restic/restic/pull/2033
|
||||
|
||||
* Enhancement #1777: Improve the `find` command
|
||||
|
||||
We've updated the `find` command to support multiple patterns.
|
||||
|
||||
`restic find` is now able to list the snapshots containing a specific tree or blob, or even the
|
||||
snapshots that contain blobs belonging to a given pack. A list of IDs can be given, as long as they
|
||||
all have the same type.
|
||||
|
||||
The command `find` can also display the pack IDs the blobs belong to, if the `--show-pack-id`
|
||||
flag is provided.
|
||||
|
||||
https://github.com/restic/restic/issues/1777
|
||||
https://github.com/restic/restic/pull/1780
|
||||
|
||||
* Enhancement #1876: Display reason why forget keeps snapshots
|
||||
|
||||
We've added a column to the list of snapshots `forget` keeps which details the reasons to keep a
|
||||
particuliar snapshot. This makes debugging policies for forget much easier. Please remember
|
||||
to always try things out with `--dry-run`!
|
||||
|
||||
https://github.com/restic/restic/pull/1876
|
||||
|
||||
* Enhancement #1891: Accept glob in paths loaded via --files-from
|
||||
|
||||
Before that, behaviour was different if paths were appended to command line or from a file,
|
||||
because wild card characters were expanded by shell if appended to command line, but not
|
||||
expanded if loaded from file.
|
||||
|
||||
https://github.com/restic/restic/issues/1891
|
||||
|
||||
* Enhancement #1920: Vendor dependencies with Go 1.11 Modules
|
||||
|
||||
Until now, we've used `dep` for managing dependencies, we've now switch to using Go modules.
|
||||
For users this does not change much, only if you want to compile restic without downloading
|
||||
anything with Go 1.11, then you need to run: `go build -mod=vendor build.go`
|
||||
|
||||
https://github.com/restic/restic/pull/1920
|
||||
|
||||
* Enhancement #1949: Add new command `self-update`
|
||||
|
||||
We have added a new command called `self-update` which downloads the latest released version
|
||||
of restic from GitHub and replaces the current binary with it. It does not rely on any external
|
||||
program (so it'll work everywhere), but still verifies the GPG signature using the embedded
|
||||
GPG public key.
|
||||
|
||||
By default, the `self-update` command is hidden behind the `selfupdate` built tag, which is
|
||||
only set when restic is built using `build.go` (including official releases). The reason for
|
||||
this is that downstream distributions will then not include the command by default, so users
|
||||
are encouraged to use the platform-specific distribution mechanism.
|
||||
|
||||
https://github.com/restic/restic/pull/1949
|
||||
|
||||
* Enhancement #1953: Ls: Add JSON output support for restic ls cmd
|
||||
|
||||
We've implemented listing files in the repository with JSON as output, just pass `--json` as an
|
||||
option to `restic ls`. This makes the output of the command machine readable.
|
||||
|
||||
https://github.com/restic/restic/pull/1953
|
||||
|
||||
* Enhancement #1962: Stream JSON output for ls command
|
||||
|
||||
The `ls` command now supports JSON output with the global `--json` flag, and this change
|
||||
streams out JSON messages one object at a time rather than en entire array buffered in memory
|
||||
before encoding. The advantage is it allows large listings to be handled efficiently.
|
||||
|
||||
Two message types are printed: snapshots and nodes. A snapshot object will precede node
|
||||
objects which belong to that snapshot. The `struct_type` field can be used to determine which
|
||||
kind of message an object is.
|
||||
|
||||
https://github.com/restic/restic/pull/1962
|
||||
|
||||
|
||||
Changelog for restic 0.9.2 (2018-08-06)
|
||||
=======================================
|
||||
|
||||
The following sections list the changes in restic 0.9.2 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
* Fix #1854: Allow saving files/dirs on different fs with `--one-file-system`
|
||||
* Fix #1870: Fix restore with --include
|
||||
* Fix #1880: Use `--cache-dir` argument for `check` command
|
||||
* Fix #1893: Return error when exclude file cannot be read
|
||||
* Fix #1861: Fix case-insensitive search with restic find
|
||||
* Enh #1906: Add support for B2 application keys
|
||||
* Enh #874: Add stats command to get information about a repository
|
||||
* Enh #1772: Add restore --verify to verify restored file content
|
||||
* Enh #1853: Add JSON output support to `restic key list`
|
||||
* Enh #1477: S3 backend: accept AWS_SESSION_TOKEN
|
||||
* Enh #1901: Update the Backblaze B2 library
|
||||
|
||||
Details
|
||||
-------
|
||||
|
||||
* Bugfix #1854: Allow saving files/dirs on different fs with `--one-file-system`
|
||||
|
||||
Restic now allows saving files/dirs on a different file system in a subdir correctly even when
|
||||
`--one-file-system` is specified.
|
||||
|
||||
The first thing the restic archiver code does is to build a tree of the target
|
||||
files/directories. If it detects that a parent directory is already included (e.g. `restic
|
||||
backup /foo /foo/bar/baz`), it'll ignore the latter argument.
|
||||
|
||||
Without `--one-file-system`, that's perfectly valid: If `/foo` is to be archived, it will
|
||||
include `/foo/bar/baz`. But with `--one-file-system`, `/foo/bar/baz` may reside on a
|
||||
different file system, so it won't be included with `/foo`.
|
||||
|
||||
https://github.com/restic/restic/issues/1854
|
||||
https://github.com/restic/restic/pull/1855
|
||||
|
||||
* Bugfix #1870: Fix restore with --include
|
||||
|
||||
We fixed a bug which prevented restic to restore files with an include filter.
|
||||
|
||||
https://github.com/restic/restic/issues/1870
|
||||
https://github.com/restic/restic/pull/1900
|
||||
|
||||
* Bugfix #1880: Use `--cache-dir` argument for `check` command
|
||||
|
||||
`check` command now uses a temporary sub-directory of the specified directory if set using the
|
||||
`--cache-dir` argument. If not set, the cache directory is created in the default temporary
|
||||
directory as before. In either case a temporary cache is used to ensure the actual repository is
|
||||
checked (rather than a local copy).
|
||||
|
||||
The `--cache-dir` argument was not used by the `check` command, instead a cache directory was
|
||||
created in the temporary directory.
|
||||
|
||||
https://github.com/restic/restic/issues/1880
|
||||
|
||||
* Bugfix #1893: Return error when exclude file cannot be read
|
||||
|
||||
A bug was found: when multiple exclude files were passed to restic and one of them could not be
|
||||
read, an error was printed and restic continued, ignoring even the existing exclude files.
|
||||
Now, an error message is printed and restic aborts when an exclude file cannot be read.
|
||||
|
||||
https://github.com/restic/restic/issues/1893
|
||||
|
||||
* Bugfix #1861: Fix case-insensitive search with restic find
|
||||
|
||||
We've fixed the behavior for `restic find -i PATTERN`, which was broken in v0.9.1.
|
||||
|
||||
https://github.com/restic/restic/pull/1861
|
||||
|
||||
* Enhancement #1906: Add support for B2 application keys
|
||||
|
||||
Restic can now use so-called "application keys" which can be created in the B2 dashboard and
|
||||
were only introduced recently. In contrast to the "master key", such keys can be restricted to a
|
||||
specific bucket and/or path.
|
||||
|
||||
https://github.com/restic/restic/issues/1906
|
||||
https://github.com/restic/restic/pull/1914
|
||||
|
||||
* Enhancement #874: Add stats command to get information about a repository
|
||||
|
||||
https://github.com/restic/restic/issues/874
|
||||
https://github.com/restic/restic/pull/1729
|
||||
|
||||
* Enhancement #1772: Add restore --verify to verify restored file content
|
||||
|
||||
Restore will print error message if restored file content does not match expected SHA256
|
||||
checksum
|
||||
|
||||
https://github.com/restic/restic/pull/1772
|
||||
|
||||
* Enhancement #1853: Add JSON output support to `restic key list`
|
||||
|
||||
This PR enables users to get the output of `restic key list` in JSON in addition to the existing
|
||||
table format.
|
||||
|
||||
https://github.com/restic/restic/pull/1853
|
||||
|
||||
* Enhancement #1477: S3 backend: accept AWS_SESSION_TOKEN
|
||||
|
||||
Before, it was not possible to use s3 backend with AWS temporary security credentials(with
|
||||
AWS_SESSION_TOKEN). This change gives higher priority to credentials.EnvAWS credentials
|
||||
provider.
|
||||
|
||||
https://github.com/restic/restic/issues/1477
|
||||
https://github.com/restic/restic/pull/1479
|
||||
https://github.com/restic/restic/pull/1647
|
||||
|
||||
* Enhancement #1901: Update the Backblaze B2 library
|
||||
|
||||
We've updated the library we're using for accessing the Backblaze B2 service to 0.5.0 to
|
||||
include support for upcoming so-called "application keys". With this feature, you can create
|
||||
access credentials for B2 which are restricted to e.g. a single bucket or even a sub-directory
|
||||
of a bucket.
|
||||
|
||||
https://github.com/restic/restic/pull/1901
|
||||
https://github.com/kurin/blazer
|
||||
|
||||
|
||||
Changelog for restic 0.9.1 (2018-06-10)
|
||||
=======================================
|
||||
|
||||
|
||||
@@ -46,12 +46,15 @@ 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 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:
|
||||
`go run -mod=vendor 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.
|
||||
@@ -60,9 +63,37 @@ uploading it somewhere or post only the parts that are really relevant.
|
||||
Development Environment
|
||||
=======================
|
||||
|
||||
In order to compile restic with the `go` tool directly, it needs to be checked
|
||||
out at the right path within a `GOPATH`. The concept of a `GOPATH` is explained
|
||||
in ["How to write Go code"](https://golang.org/doc/code.html).
|
||||
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`.
|
||||
|
||||
Go >= 1.11
|
||||
----------
|
||||
|
||||
For Go version 1.11 or later, you should clone the repo (without having
|
||||
`$GOPATH` set) and `cd` into the directory:
|
||||
|
||||
$ unset GOPATH
|
||||
$ git clone https://github.com/restic/restic
|
||||
$ cd restic
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -83,12 +114,7 @@ You can then build restic as follows:
|
||||
|
||||
The following commands can be used to run all the tests:
|
||||
|
||||
$ go test ./cmd/... ./internal/...
|
||||
|
||||
The repository contains two sets of directories with code: `cmd/` and
|
||||
`internal/` contain the code written for restic, whereas `vendor/` contains
|
||||
copies of libraries restic depends on. The libraries are managed with the
|
||||
[`dep`](https://github.com/golang/dep) tool.
|
||||
$ go test ./...
|
||||
|
||||
Providing Patches
|
||||
=================
|
||||
@@ -107,8 +133,7 @@ down to the following steps:
|
||||
|
||||
2. Clone the repository locally and create a new branch. If you are working on
|
||||
the code itself, please set up the development environment as described in
|
||||
the previous section. Especially take care to place your forked repository
|
||||
at the correct path (`src/github.com/restic/restic`) within your `GOPATH`.
|
||||
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.
|
||||
@@ -124,11 +149,14 @@ 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 to the file
|
||||
['CHANGELOG.md'](CHANGELOG.md). It will be used in the announcement of the
|
||||
next stable release. While writing, ask yourself: If I were the user, what
|
||||
would I need to be aware of with this change.
|
||||
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!
|
||||
@@ -141,13 +169,14 @@ run
|
||||
|
||||
gofmt -w **/*.go
|
||||
|
||||
in the project root directory before committing. Installing the script
|
||||
`fmt-check` from https://github.com/edsrzf/gofmt-git-hook locally as a
|
||||
pre-commit hook checks formatting before committing automatically, just copy
|
||||
this script to `.git/hooks/pre-commit`.
|
||||
in the project root directory before committing. For each Pull Request, the
|
||||
formatting is tested with `gofmt` for the latest stable version of Go.
|
||||
Installing the script `fmt-check` from https://github.com/edsrzf/gofmt-git-hook
|
||||
locally as a pre-commit hook checks formatting before committing automatically,
|
||||
just copy this script to `.git/hooks/pre-commit`.
|
||||
|
||||
For each pull request, several different systems run the integration tests on
|
||||
Linux, OS X and Windows. We won't merge any code that does not pass all tests
|
||||
Linux, macOS and Windows. We won't merge any code that does not pass all tests
|
||||
for all systems, so when a tests fails, try to find out what's wrong and fix
|
||||
it. If you need help on this, please leave a comment in the pull request, and
|
||||
we'll be glad to assist. Having a PR with failing integration tests is nothing
|
||||
@@ -164,7 +193,7 @@ history and triaging bugs much easier.
|
||||
Git commit messages have a very terse summary in the first line of the commit
|
||||
message, followed by an empty line, followed by a more verbose description or a
|
||||
List of changed things. For examples, please refer to the excellent [How to
|
||||
Write a Git Commit Message](http://chris.beams.io/posts/git-commit/).
|
||||
Write a Git Commit Message](https://chris.beams.io/posts/git-commit/).
|
||||
|
||||
If you change/add multiple different things that aren't related at all, try to
|
||||
make several smaller commits. This is much easier to review. Using `git add -p`
|
||||
|
||||
255
Gopkg.lock
generated
255
Gopkg.lock
generated
@@ -1,255 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "bazil.org/fuse"
|
||||
packages = [".","fs","fuseutil"]
|
||||
revision = "371fbbdaa8987b715bdd21d6adc4c9b20155f748"
|
||||
|
||||
[[projects]]
|
||||
name = "cloud.google.com/go"
|
||||
packages = ["compute/metadata"]
|
||||
revision = "4b98a6370e36d7a85192e7bad08a4ebd82eac2a8"
|
||||
version = "v0.20.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Azure/azure-sdk-for-go"
|
||||
packages = ["storage","version"]
|
||||
revision = "56332fec5b308fbb6615fa1af6117394cdba186d"
|
||||
version = "v15.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Azure/go-autorest"
|
||||
packages = ["autorest","autorest/adal","autorest/azure","autorest/date"]
|
||||
revision = "ed4b7f5bf1ec0c9ede1fda2681d96771282f2862"
|
||||
version = "v10.4.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cenkalti/backoff"
|
||||
packages = ["."]
|
||||
revision = "2ea60e5f094469f9e65adb9cd103795b73ae743e"
|
||||
version = "v2.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/cpuguy83/go-md2man"
|
||||
packages = ["md2man"]
|
||||
revision = "20f5889cbdc3c73dbd2862796665e7c465ade7d1"
|
||||
version = "v1.0.8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/elithrar/simple-scrypt"
|
||||
packages = ["."]
|
||||
revision = "d150773194090feb6c897805a7bcea8d49544e2c"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-ini/ini"
|
||||
packages = ["."]
|
||||
revision = "6333e38ac20b8949a8dd68baa3650f4dee8f39f0"
|
||||
version = "v1.33.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-cmp"
|
||||
packages = ["cmp","cmp/internal/diff","cmp/internal/function","cmp/internal/value"]
|
||||
revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/juju/ratelimit"
|
||||
packages = ["."]
|
||||
revision = "59fac5042749a5afb9af70e813da1dd5474f0167"
|
||||
version = "1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/kr/fs"
|
||||
packages = ["."]
|
||||
revision = "2788f0dbd16903de03cb8186e5c7d97b69ad387b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/kurin/blazer"
|
||||
packages = ["b2","base","internal/b2assets","internal/b2types","internal/blog","x/window"]
|
||||
revision = "318e9768bf9a0fe52a64b9f8fe74f4f5caef6452"
|
||||
version = "v0.4.4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/marstr/guid"
|
||||
packages = ["."]
|
||||
revision = "8bd9a64bf37eb297b492a4101fb28e80ac0b290f"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/minio/minio-go"
|
||||
packages = [".","pkg/credentials","pkg/encrypt","pkg/policy","pkg/s3signer","pkg/s3utils","pkg/set"]
|
||||
revision = "66252c2a3c15f7b90cc8493d497a04ac3b6e3606"
|
||||
version = "5.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ncw/swift"
|
||||
packages = ["."]
|
||||
revision = "b2a7479cf26fa841ff90dd932d0221cb5c50782d"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/profile"
|
||||
packages = ["."]
|
||||
revision = "5b67d428864e92711fcbd2f8629456121a56d91f"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/sftp"
|
||||
packages = ["."]
|
||||
revision = "49488377fa2f14143ba3067cf7555f60f6c7b550"
|
||||
version = "1.5.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/xattr"
|
||||
packages = ["."]
|
||||
revision = "1d7b7ffe7c46974a836eb583b7452f22de1c18cf"
|
||||
version = "v0.2.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/restic/chunker"
|
||||
packages = ["."]
|
||||
revision = "db83917be3b88cc307464b7d8a221c173e34a0db"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/russross/blackfriday"
|
||||
packages = ["."]
|
||||
revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5"
|
||||
version = "v1.5.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = [".","doc"]
|
||||
revision = "a1f051bc3eba734da4772d60e2d677f47cf93ef4"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["argon2","blake2b","curve25519","ed25519","ed25519/internal/edwards25519","internal/chacha20","pbkdf2","poly1305","scrypt","ssh","ssh/terminal"]
|
||||
revision = "4ec37c66abab2c7e02ae775328b2ff001c3f025a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","context/ctxhttp","http2","http2/hpack","idna","lex/httplex"]
|
||||
revision = "6078986fec03a1dcc236c34816c71b0e05018fda"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [".","google","internal","jws","jwt"]
|
||||
revision = "fdc9e635145ae97e6c2cb777c48305600cf515cb"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["cpu","unix","windows"]
|
||||
revision = "7db1c3b1a98089d0071c84f646ff5c96aad43682"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = ["collate","collate/build","encoding","encoding/internal","encoding/internal/identifier","encoding/unicode","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","internal/utf8internal","language","runes","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"]
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "google.golang.org/api"
|
||||
packages = ["gensupport","googleapi","googleapi/internal/uritemplates","storage/v1"]
|
||||
revision = "dbbc13f71100fa6ece308335445fca6bb0dd5c2f"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/tomb.v2"
|
||||
packages = ["."]
|
||||
revision = "d5d1b5820637886def9eef33e03a27a9f166942c"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "a5de339cba7570216b212439b90e1e6c384c94be8342fe7755b7cb66aa0a3440"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
21
Gopkg.toml
21
Gopkg.toml
@@ -1,21 +0,0 @@
|
||||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@
|
||||
all: restic
|
||||
|
||||
restic:
|
||||
go run build.go
|
||||
go run -mod=vendor build.go || go run build.go
|
||||
|
||||
clean:
|
||||
rm -f restic
|
||||
|
||||
18
README.rst
18
README.rst
@@ -1,9 +1,9 @@
|
||||
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |TestCoverage|
|
||||
|Documentation| |Build Status| |Build status| |Report Card| |Say Thanks| |TestCoverage| |Reviewed by Hound|
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
restic is a backup program that is fast, efficient and secure.
|
||||
restic is a backup program that is fast, efficient and secure. It supports the three major operating systems (Linux, macOS, Windows) and a few smaller ones (FreeBSD, OpenBSD).
|
||||
|
||||
For detailed usage and installation instructions check out the `documentation <https://restic.readthedocs.io/en/latest>`__.
|
||||
|
||||
@@ -111,6 +111,14 @@ License
|
||||
Restic is licensed under `BSD 2-Clause License <https://opensource.org/licenses/BSD-2-Clause>`__. You can find the
|
||||
complete text in ``LICENSE``.
|
||||
|
||||
Sponsorship
|
||||
-----------
|
||||
|
||||
Backend integration tests for Google Cloud Storage and Microsoft Azure Blob
|
||||
Storage are sponsored by `AppsCode <https://appscode.com>`__!
|
||||
|
||||
|AppsCode|
|
||||
|
||||
.. |Documentation| image:: https://readthedocs.org/projects/restic/badge/?version=latest
|
||||
:target: https://restic.readthedocs.io/en/latest/?badge=latest
|
||||
.. |Build Status| image:: https://travis-ci.com/restic/restic.svg?branch=master
|
||||
@@ -121,5 +129,7 @@ complete text in ``LICENSE``.
|
||||
:target: https://goreportcard.com/report/github.com/restic/restic
|
||||
.. |Say Thanks| image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg
|
||||
:target: https://saythanks.io/to/restic
|
||||
.. |TestCoverage| image:: https://codecov.io/gh/restic/restic/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/restic/restic
|
||||
.. |AppsCode| image:: https://cdn.appscode.com/images/logo/appscode/ac-logo-color.png
|
||||
:target: https://appscode.com
|
||||
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
|
||||
:target: https://houndci.com
|
||||
|
||||
@@ -7,6 +7,9 @@ branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
cache:
|
||||
- '%LocalAppData%\go-build'
|
||||
|
||||
init:
|
||||
- ps: >-
|
||||
$app = Get-WmiObject -Class Win32_Product -Filter "Vendor = 'http://golang.org'"
|
||||
@@ -17,8 +20,8 @@ init:
|
||||
|
||||
install:
|
||||
- rmdir c:\go /s /q
|
||||
- appveyor DownloadFile https://dl.google.com/go/go1.10.windows-amd64.msi
|
||||
- msiexec /i go1.10.windows-amd64.msi /q
|
||||
- appveyor DownloadFile https://dl.google.com/go/go1.13.4.windows-amd64.msi
|
||||
- msiexec /i go1.13.4.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
|
||||
@@ -26,4 +29,4 @@ install:
|
||||
- set PATH=bin/;%PATH%
|
||||
|
||||
build_script:
|
||||
- go run run_integration_tests.go
|
||||
- go run -mod=vendor run_integration_tests.go
|
||||
|
||||
328
build.go
328
build.go
@@ -1,3 +1,18 @@
|
||||
// Description
|
||||
//
|
||||
// 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.
|
||||
|
||||
// BSD 2-Clause License
|
||||
//
|
||||
// Copyright (c) 2016-2018, Alexander Neumann <alexander@bumpern.de>
|
||||
@@ -37,7 +52,6 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -46,23 +60,22 @@ import (
|
||||
|
||||
// config contains the configuration for the program to build.
|
||||
var config = Config{
|
||||
Name: "restic", // name of the program executable and directory
|
||||
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
||||
Main: "github.com/restic/restic/cmd/restic", // package name for the main package
|
||||
Tests: []string{ // tests to run
|
||||
"github.com/restic/restic/internal/...",
|
||||
"github.com/restic/restic/cmd/...",
|
||||
},
|
||||
MinVersion: GoVersion{Major: 1, Minor: 9, Patch: 0}, // minimum Go version supported
|
||||
Name: "restic", // name of the program executable and directory
|
||||
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
||||
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
|
||||
}
|
||||
|
||||
// Config configures the build.
|
||||
type Config struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Main string
|
||||
Tests []string
|
||||
MinVersion GoVersion
|
||||
Name string
|
||||
Namespace string
|
||||
Main string
|
||||
DefaultBuildTags []string
|
||||
Tests []string
|
||||
MinVersion GoVersion
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -71,41 +84,12 @@ var (
|
||||
runTests bool
|
||||
enableCGO bool
|
||||
enablePIE bool
|
||||
goVersion = ParseGoVersion(runtime.Version())
|
||||
)
|
||||
|
||||
// specialDir returns true if the file begins with a special character ('.' or '_').
|
||||
func specialDir(name string) bool {
|
||||
if name == "." {
|
||||
return false
|
||||
}
|
||||
|
||||
base := filepath.Base(name)
|
||||
if base == "vendor" || base[0] == '_' || base[0] == '.' {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// excludePath returns true if the file should not be copied to the new GOPATH.
|
||||
func excludePath(name string) bool {
|
||||
ext := path.Ext(name)
|
||||
if ext == ".go" || ext == ".s" || ext == ".h" {
|
||||
return false
|
||||
}
|
||||
|
||||
parentDir := filepath.Base(filepath.Dir(name))
|
||||
if parentDir == "testdata" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// updateGopath builds a valid GOPATH at dst, with all Go files in src/ copied
|
||||
// to dst/prefix/, so calling
|
||||
// copy all Go files in src to dst, creating directories on the fly, so calling
|
||||
//
|
||||
// updateGopath("/tmp/gopath", "/home/u/restic", "github.com/restic/restic")
|
||||
// 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":
|
||||
@@ -116,19 +100,15 @@ func excludePath(name string) bool {
|
||||
// └── restic
|
||||
// └── restic
|
||||
// └── foo.go
|
||||
func updateGopath(dst, src, prefix string) error {
|
||||
verbosePrintf("copy contents of %v to %v\n", src, filepath.Join(dst, prefix))
|
||||
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 specialDir(name) {
|
||||
if fi.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
return nil
|
||||
if name == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -139,17 +119,13 @@ func updateGopath(dst, src, prefix string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if excludePath(name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
intermediatePath, err := filepath.Rel(src, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileSrc := filepath.Join(src, intermediatePath)
|
||||
fileDst := filepath.Join(dst, "src", prefix, intermediatePath)
|
||||
fileDst := filepath.Join(dst, intermediatePath)
|
||||
|
||||
return copyFile(fileDst, fileSrc)
|
||||
})
|
||||
@@ -164,6 +140,15 @@ func directoryExists(dirname string) bool {
|
||||
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)
|
||||
@@ -183,30 +168,34 @@ func copyFile(dst, src string) error {
|
||||
|
||||
fdst, err := os.Create(dst)
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(fdst, fsrc); err != nil {
|
||||
_, err = io.Copy(fdst, fsrc)
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
_ = fdst.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = fsrc.Close()
|
||||
err = fdst.Close()
|
||||
if err != nil {
|
||||
_ = fsrc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = fdst.Close()
|
||||
err = fsrc.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = os.Chmod(dst, fi.Mode())
|
||||
err = os.Chmod(dst, fi.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = os.Chtimes(dst, fi.ModTime(), fi.ModTime())
|
||||
}
|
||||
|
||||
return nil
|
||||
return os.Chtimes(dst, fi.ModTime(), fi.ModTime())
|
||||
}
|
||||
|
||||
// die prints the message with fmt.Fprintf() to stderr and exits with an error
|
||||
@@ -222,7 +211,7 @@ 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-gopath do not remove the GOPATH after build\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")
|
||||
@@ -241,11 +230,20 @@ func verbosePrintf(message string, args ...interface{}) {
|
||||
fmt.Printf("build: "+message, args...)
|
||||
}
|
||||
|
||||
// cleanEnv returns a clean environment with GOPATH and GOBIN removed (if
|
||||
// present).
|
||||
// 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() {
|
||||
if strings.HasPrefix(v, "GOPATH=") || strings.HasPrefix(v, "GOBIN=") {
|
||||
data := strings.SplitN(v, "=", 2)
|
||||
name := data[0]
|
||||
|
||||
if _, ok := removeKeys[name]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -256,17 +254,17 @@ func cleanEnv() (env []string) {
|
||||
}
|
||||
|
||||
// build runs "go build args..." with GOPATH set to gopath.
|
||||
func build(cwd string, ver GoVersion, goos, goarch, goarm, gopath string, args ...string) error {
|
||||
func build(cwd string, env map[string]string, args ...string) error {
|
||||
a := []string{"build"}
|
||||
|
||||
if ver.AtLeast(GoVersion{1, 10, 0}) {
|
||||
if goVersion.AtLeast(GoVersion{1, 10, 0}) {
|
||||
verbosePrintf("Go version is at least 1.10, using new syntax for -gcflags\n")
|
||||
// use new prefix
|
||||
a = append(a, "-asmflags", fmt.Sprintf("all=-trimpath=%s", gopath))
|
||||
a = append(a, "-gcflags", fmt.Sprintf("all=-trimpath=%s", gopath))
|
||||
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", gopath))
|
||||
a = append(a, "-gcflags", fmt.Sprintf("-trimpath=%s", gopath))
|
||||
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")
|
||||
@@ -274,9 +272,9 @@ func build(cwd string, ver GoVersion, goos, goarch, goarm, gopath string, args .
|
||||
|
||||
a = append(a, args...)
|
||||
cmd := exec.Command("go", a...)
|
||||
cmd.Env = append(cleanEnv(), "GOPATH="+gopath, "GOARCH="+goarch, "GOOS="+goos)
|
||||
if goarm != "" {
|
||||
cmd.Env = append(cmd.Env, "GOARM="+goarm)
|
||||
cmd.Env = append(cleanEnv(), "GOPROXY=off")
|
||||
for k, v := range env {
|
||||
cmd.Env = append(cmd.Env, k+"="+v)
|
||||
}
|
||||
if !enableCGO {
|
||||
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
|
||||
@@ -285,20 +283,30 @@ func build(cwd string, ver GoVersion, goos, goarch, goarm, gopath string, args .
|
||||
cmd.Dir = cwd
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
verbosePrintf("go %s\n", a)
|
||||
|
||||
verbosePrintf("chdir %q\n", cwd)
|
||||
verbosePrintf("go %q\n", a)
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// test runs "go test args..." with GOPATH set to gopath.
|
||||
func test(cwd, gopath string, args ...string) error {
|
||||
args = append([]string{"test"}, args...)
|
||||
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(), "GOPATH="+gopath)
|
||||
cmd.Env = append(cleanEnv(), "GOPROXY=off")
|
||||
for k, v := range env {
|
||||
cmd.Env = append(cmd.Env, k+"="+v)
|
||||
}
|
||||
if !enableCGO {
|
||||
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
|
||||
}
|
||||
cmd.Dir = cwd
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
verbosePrintf("go %s\n", args)
|
||||
|
||||
verbosePrintf("chdir %q\n", cwd)
|
||||
verbosePrintf("go %q\n", args)
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -446,22 +454,24 @@ func (v GoVersion) String() string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
ver := ParseGoVersion(runtime.Version())
|
||||
if !ver.AtLeast(config.MinVersion) {
|
||||
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", ver, config.MinVersion)
|
||||
if !goVersion.AtLeast(config.MinVersion) {
|
||||
fmt.Fprintf(os.Stderr, "%s detected, this program requires at least %s\n", goVersion, config.MinVersion)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
buildTags := []string{}
|
||||
buildTags := config.DefaultBuildTags
|
||||
|
||||
skipNext := false
|
||||
params := os.Args[1:]
|
||||
|
||||
targetGOOS := runtime.GOOS
|
||||
targetGOARCH := runtime.GOARCH
|
||||
targetGOARM := ""
|
||||
goEnv := map[string]string{}
|
||||
buildEnv := map[string]string{
|
||||
"GOOS": runtime.GOOS,
|
||||
"GOARCH": runtime.GOARCH,
|
||||
"GOARM": "",
|
||||
}
|
||||
|
||||
gopath := ""
|
||||
tempdir := ""
|
||||
|
||||
var outputFilename string
|
||||
|
||||
@@ -481,13 +491,13 @@ func main() {
|
||||
die("-t given but no tag specified")
|
||||
}
|
||||
skipNext = true
|
||||
buildTags = strings.Split(params[i+1], " ")
|
||||
buildTags = append(buildTags, strings.Split(params[i+1], " ")...)
|
||||
case "-o", "--output":
|
||||
skipNext = true
|
||||
outputFilename = params[i+1]
|
||||
case "--tempdir":
|
||||
skipNext = true
|
||||
gopath = params[i+1]
|
||||
tempdir = params[i+1]
|
||||
case "-T", "--test":
|
||||
runTests = true
|
||||
case "--enable-cgo":
|
||||
@@ -496,13 +506,13 @@ func main() {
|
||||
enablePIE = true
|
||||
case "--goos":
|
||||
skipNext = true
|
||||
targetGOOS = params[i+1]
|
||||
buildEnv["GOOS"] = params[i+1]
|
||||
case "--goarch":
|
||||
skipNext = true
|
||||
targetGOARCH = params[i+1]
|
||||
buildEnv["GOARCH"] = params[i+1]
|
||||
case "--goarm":
|
||||
skipNext = true
|
||||
targetGOARM = params[i+1]
|
||||
buildEnv["GOARM"] = params[i+1]
|
||||
case "-h":
|
||||
showUsage(os.Stdout)
|
||||
return
|
||||
@@ -513,12 +523,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
verbosePrintf("detected Go version %v\n", ver)
|
||||
|
||||
if len(buildTags) == 0 {
|
||||
verbosePrintf("adding build-tag release\n")
|
||||
buildTags = []string{"release"}
|
||||
}
|
||||
verbosePrintf("detected Go version %v\n", goVersion)
|
||||
|
||||
for i := range buildTags {
|
||||
buildTags[i] = strings.TrimSpace(buildTags[i])
|
||||
@@ -531,50 +536,16 @@ func main() {
|
||||
die("Getwd(): %v\n", err)
|
||||
}
|
||||
|
||||
if gopath == "" {
|
||||
gopath, err = ioutil.TempDir("", fmt.Sprintf("%v-build-", config.Name))
|
||||
if err != nil {
|
||||
die("TempDir(): %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
verbosePrintf("create GOPATH at %v\n", gopath)
|
||||
if err = updateGopath(gopath, root, config.Namespace); err != nil {
|
||||
die("copying files from %v/src to %v/src failed: %v\n", root, gopath, err)
|
||||
}
|
||||
|
||||
vendor := filepath.Join(root, "vendor")
|
||||
if directoryExists(vendor) {
|
||||
if err = updateGopath(gopath, vendor, filepath.Join(config.Namespace, "vendor")); err != nil {
|
||||
die("copying files from %v to %v failed: %v\n", root, gopath, err)
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if !keepGopath {
|
||||
verbosePrintf("remove %v\n", gopath)
|
||||
if err = os.RemoveAll(gopath); err != nil {
|
||||
die("remove GOPATH at %s failed: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
verbosePrintf("leaving temporary GOPATH at %v\n", gopath)
|
||||
}
|
||||
}()
|
||||
|
||||
if outputFilename == "" {
|
||||
outputFilename = config.Name
|
||||
if targetGOOS == "windows" {
|
||||
if buildEnv["GOOS"] == "windows" {
|
||||
outputFilename += ".exe"
|
||||
}
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
die("Getwd() returned %v\n", err)
|
||||
}
|
||||
output := outputFilename
|
||||
if !filepath.IsAbs(output) {
|
||||
output = filepath.Join(cwd, output)
|
||||
output = filepath.Join(root, output)
|
||||
}
|
||||
|
||||
version := getVersion()
|
||||
@@ -585,13 +556,68 @@ func main() {
|
||||
ldflags := "-s -w " + constants.LDFlags()
|
||||
verbosePrintf("ldflags: %s\n", ldflags)
|
||||
|
||||
args := []string{
|
||||
"-tags", strings.Join(buildTags, " "),
|
||||
"-ldflags", ldflags,
|
||||
"-o", output, config.Main,
|
||||
var (
|
||||
buildArgs []string
|
||||
testArgs []string
|
||||
)
|
||||
|
||||
mainPackage := config.Main
|
||||
if strings.HasPrefix(mainPackage, config.Namespace) {
|
||||
mainPackage = strings.Replace(mainPackage, config.Namespace, "./", 1)
|
||||
}
|
||||
|
||||
err = build(filepath.Join(gopath, "src"), ver, targetGOOS, targetGOARCH, targetGOARM, gopath, args...)
|
||||
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
|
||||
}
|
||||
|
||||
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...)
|
||||
if err != nil {
|
||||
die("build failed: %v\n", err)
|
||||
}
|
||||
@@ -599,7 +625,9 @@ func main() {
|
||||
if runTests {
|
||||
verbosePrintf("running tests\n")
|
||||
|
||||
err = test(cwd, gopath, config.Tests...)
|
||||
testArgs = append(testArgs, config.Tests...)
|
||||
|
||||
err = test(buildCWD, goEnv, testArgs...)
|
||||
if err != nil {
|
||||
die("running tests failed: %v\n", err)
|
||||
}
|
||||
|
||||
16
changelog/0.9.2_2018-08-06/issue-1854
Normal file
16
changelog/0.9.2_2018-08-06/issue-1854
Normal file
@@ -0,0 +1,16 @@
|
||||
Bugfix: Allow saving files/dirs on different fs with `--one-file-system`
|
||||
|
||||
restic now allows saving files/dirs on a different file system in a subdir
|
||||
correctly even when `--one-file-system` is specified.
|
||||
|
||||
The first thing the restic archiver code does is to build a tree of the target
|
||||
files/directories. If it detects that a parent directory is already included
|
||||
(e.g. `restic backup /foo /foo/bar/baz`), it'll ignore the latter argument.
|
||||
|
||||
Without `--one-file-system`, that's perfectly valid: If `/foo` is to be
|
||||
archived, it will include `/foo/bar/baz`. But with `--one-file-system`,
|
||||
`/foo/bar/baz` may reside on a different file system, so it won't be included
|
||||
with `/foo`.
|
||||
|
||||
https://github.com/restic/restic/issues/1854
|
||||
https://github.com/restic/restic/pull/1855
|
||||
6
changelog/0.9.2_2018-08-06/issue-1870
Normal file
6
changelog/0.9.2_2018-08-06/issue-1870
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Fix restore with --include
|
||||
|
||||
We fixed a bug which prevented restic to restore files with an include filter.
|
||||
|
||||
https://github.com/restic/restic/issues/1870
|
||||
https://github.com/restic/restic/pull/1900
|
||||
12
changelog/0.9.2_2018-08-06/issue-1880
Normal file
12
changelog/0.9.2_2018-08-06/issue-1880
Normal file
@@ -0,0 +1,12 @@
|
||||
Bugfix: Use `--cache-dir` argument for `check` command
|
||||
|
||||
`check` command now uses a temporary sub-directory of the specified directory
|
||||
if set using the `--cache-dir` argument. If not set, the cache directory is
|
||||
created in the default temporary directory as before.
|
||||
In either case a temporary cache is used to ensure the actual repository is
|
||||
checked (rather than a local copy).
|
||||
|
||||
The `--cache-dir` argument was not used by the `check` command, instead a
|
||||
cache directory was created in the temporary directory.
|
||||
|
||||
https://github.com/restic/restic/issues/1880
|
||||
8
changelog/0.9.2_2018-08-06/issue-1893
Normal file
8
changelog/0.9.2_2018-08-06/issue-1893
Normal file
@@ -0,0 +1,8 @@
|
||||
Bugfix: Return error when exclude file cannot be read
|
||||
|
||||
A bug was found: when multiple exclude files were passed to restic and one of
|
||||
them could not be read, an error was printed and restic continued, ignoring
|
||||
even the existing exclude files. Now, an error message is printed and restic
|
||||
aborts when an exclude file cannot be read.
|
||||
|
||||
https://github.com/restic/restic/issues/1893
|
||||
8
changelog/0.9.2_2018-08-06/issue-1906
Normal file
8
changelog/0.9.2_2018-08-06/issue-1906
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Add support for B2 application keys
|
||||
|
||||
Restic can now use so-called "application keys" which can be created in the B2
|
||||
dashboard and were only introduced recently. In contrast to the "master key",
|
||||
such keys can be restricted to a specific bucket and/or path.
|
||||
|
||||
https://github.com/restic/restic/issues/1906
|
||||
https://github.com/restic/restic/pull/1914
|
||||
4
changelog/0.9.2_2018-08-06/pull-1729
Normal file
4
changelog/0.9.2_2018-08-06/pull-1729
Normal file
@@ -0,0 +1,4 @@
|
||||
Enhancement: Add stats command to get information about a repository
|
||||
|
||||
https://github.com/restic/restic/issues/874
|
||||
https://github.com/restic/restic/pull/1729
|
||||
6
changelog/0.9.2_2018-08-06/pull-1772
Normal file
6
changelog/0.9.2_2018-08-06/pull-1772
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Add restore --verify to verify restored file content
|
||||
|
||||
Restore will print error message if restored file content does not match
|
||||
expected SHA256 checksum
|
||||
|
||||
https://github.com/restic/restic/pull/1772
|
||||
6
changelog/0.9.2_2018-08-06/pull-1853
Normal file
6
changelog/0.9.2_2018-08-06/pull-1853
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Add JSON output support to `restic key list`
|
||||
|
||||
This PR enables users to get the output of `restic key list` in JSON in addition
|
||||
to the existing table format.
|
||||
|
||||
https://github.com/restic/restic/pull/1853
|
||||
6
changelog/0.9.2_2018-08-06/pull-1861
Normal file
6
changelog/0.9.2_2018-08-06/pull-1861
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Fix case-insensitive search with restic find
|
||||
|
||||
We've fixed the behavior for `restic find -i PATTERN`, which was
|
||||
broken in v0.9.1.
|
||||
|
||||
https://github.com/restic/restic/pull/1861
|
||||
8
changelog/0.9.2_2018-08-06/pull-1882
Normal file
8
changelog/0.9.2_2018-08-06/pull-1882
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: S3 backend: accept AWS_SESSION_TOKEN
|
||||
|
||||
Before, it was not possible to use s3 backend with AWS temporary security credentials(with AWS_SESSION_TOKEN).
|
||||
This change gives higher priority to credentials.EnvAWS credentials provider.
|
||||
|
||||
https://github.com/restic/restic/issues/1477
|
||||
https://github.com/restic/restic/pull/1479
|
||||
https://github.com/restic/restic/pull/1647
|
||||
9
changelog/0.9.2_2018-08-06/pull-1901
Normal file
9
changelog/0.9.2_2018-08-06/pull-1901
Normal file
@@ -0,0 +1,9 @@
|
||||
Enhancement: Update the Backblaze B2 library
|
||||
|
||||
We've updated the library we're using for accessing the Backblaze B2 service to
|
||||
0.5.0 to include support for upcoming so-called "application keys". With this
|
||||
feature, you can create access credentials for B2 which are restricted to e.g.
|
||||
a single bucket or even a sub-directory of a bucket.
|
||||
|
||||
https://github.com/restic/restic/pull/1901
|
||||
https://github.com/kurin/blazer
|
||||
7
changelog/0.9.3_2018-10-13/issue-1766
Normal file
7
changelog/0.9.3_2018-10-13/issue-1766
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: restore: suppress lchown errors when not running as root
|
||||
|
||||
Like "cp" and "rsync" do, restic now only reports errors for changing
|
||||
the ownership of files during restore if it is run as root, on non-Windows
|
||||
operating systems. On Windows, the error is reported as usual.
|
||||
|
||||
https://github.com/restic/restic/issues/1766
|
||||
14
changelog/0.9.3_2018-10-13/issue-1909
Normal file
14
changelog/0.9.3_2018-10-13/issue-1909
Normal file
@@ -0,0 +1,14 @@
|
||||
Enhancement: Reject files/dirs by name first
|
||||
|
||||
The current scanner/archiver code had an architectural limitation: it always
|
||||
ran the `lstat()` system call on all files and directories before a decision to
|
||||
include/exclude the file/dir was made. This lead to a lot of unnecessary system
|
||||
calls for items that could have been rejected by their name or path only.
|
||||
|
||||
We've changed the archiver/scanner implementation so that it now first rejects
|
||||
by name/path, and only runs the system call on the remaining items. This
|
||||
reduces the number of `lstat()` system calls a lot (depending on the exclude
|
||||
settings).
|
||||
|
||||
https://github.com/restic/restic/issues/1909
|
||||
https://github.com/restic/restic/pull/1912
|
||||
8
changelog/0.9.3_2018-10-13/issue-1935
Normal file
8
changelog/0.9.3_2018-10-13/issue-1935
Normal file
@@ -0,0 +1,8 @@
|
||||
Bugfix: Remove truncated files from cache
|
||||
|
||||
When a file in the local cache is truncated, and restic tries to access data
|
||||
beyond the end of the (cached) file, it used to return an error "EOF". This is
|
||||
now fixed, such truncated files are removed and the data is fetched directly
|
||||
from the backend.
|
||||
|
||||
https://github.com/restic/restic/issues/1935
|
||||
15
changelog/0.9.3_2018-10-13/issue-1941
Normal file
15
changelog/0.9.3_2018-10-13/issue-1941
Normal file
@@ -0,0 +1,15 @@
|
||||
Enhancement: Add directory filter to ls command
|
||||
|
||||
The ls command can now be filtered by directories, so that only files in the
|
||||
given directories will be shown. If the --recursive flag is specified, then
|
||||
ls will traverse subfolders and list their files as well.
|
||||
|
||||
It used to be possible to specify multiple snapshots, but that has been
|
||||
replaced by only one snapshot and the possibility of specifying multiple
|
||||
directories.
|
||||
|
||||
Specifying directories constrains the walk, which can significantly speed up
|
||||
the listing.
|
||||
|
||||
https://github.com/restic/restic/issues/1940
|
||||
https://github.com/restic/restic/pull/1941
|
||||
7
changelog/0.9.3_2018-10-13/issue-1967
Normal file
7
changelog/0.9.3_2018-10-13/issue-1967
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Use `--host` everywhere
|
||||
|
||||
We now use the flag `--host` for all commands which need a host name, using
|
||||
`--hostname` (e.g. for `restic backup`) still works, but will print a
|
||||
deprecation warning. Also, add the short option `-H` where possible.
|
||||
|
||||
https://github.com/restic/restic/issues/1967
|
||||
12
changelog/0.9.3_2018-10-13/issue-1978
Normal file
12
changelog/0.9.3_2018-10-13/issue-1978
Normal file
@@ -0,0 +1,12 @@
|
||||
Bugfix: Do not return an error when the scanner is slower than backup
|
||||
|
||||
When restic makes a backup, there's a background task called "scanner" which
|
||||
collects information on how many files and directories are to be saved, in
|
||||
order to display progress information to the user. When the backup finishes
|
||||
faster than the scanner, it is aborted because the result is not needed any
|
||||
more. This logic contained a bug, where quitting the scanner process was
|
||||
treated as an error, and caused restic to print an unhelpful error message
|
||||
("context canceled").
|
||||
|
||||
https://github.com/restic/restic/issues/1978
|
||||
https://github.com/restic/restic/pull/1991
|
||||
7
changelog/0.9.3_2018-10-13/issue-2028
Normal file
7
changelog/0.9.3_2018-10-13/issue-2028
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Display size of cache directories
|
||||
|
||||
The `cache` command now by default shows the size of the individual cache
|
||||
directories. It can be disabled with `--no-size`.
|
||||
|
||||
https://github.com/restic/restic/issues/2028
|
||||
https://github.com/restic/restic/pull/2033
|
||||
13
changelog/0.9.3_2018-10-13/pull-1780
Normal file
13
changelog/0.9.3_2018-10-13/pull-1780
Normal file
@@ -0,0 +1,13 @@
|
||||
Enhancement: Improve the `find` command
|
||||
|
||||
We've updated the `find` command to support multiple patterns.
|
||||
|
||||
`restic find` is now able to list the snapshots containing a specific tree
|
||||
or blob, or even the snapshots that contain blobs belonging to a given pack.
|
||||
A list of IDs can be given, as long as they all have the same type.
|
||||
|
||||
The command `find` can also display the pack IDs the blobs belong to, if
|
||||
the `--show-pack-id` flag is provided.
|
||||
|
||||
https://github.com/restic/restic/issues/1777
|
||||
https://github.com/restic/restic/pull/1780
|
||||
7
changelog/0.9.3_2018-10-13/pull-1876
Normal file
7
changelog/0.9.3_2018-10-13/pull-1876
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Display reason why forget keeps snapshots
|
||||
|
||||
We've added a column to the list of snapshots `forget` keeps which details the
|
||||
reasons to keep a particuliar snapshot. This makes debugging policies for
|
||||
forget much easier. Please remember to always try things out with `--dry-run`!
|
||||
|
||||
https://github.com/restic/restic/pull/1876
|
||||
7
changelog/0.9.3_2018-10-13/pull-1891
Normal file
7
changelog/0.9.3_2018-10-13/pull-1891
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Accept glob in paths loaded via --files-from
|
||||
|
||||
Before that, behaviour was different if paths were appended to command line or
|
||||
from a file, because wild card characters were expanded by shell if appended to
|
||||
command line, but not expanded if loaded from file.
|
||||
|
||||
https://github.com/restic/restic/issues/1891
|
||||
8
changelog/0.9.3_2018-10-13/pull-1920
Normal file
8
changelog/0.9.3_2018-10-13/pull-1920
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Vendor dependencies with Go 1.11 Modules
|
||||
|
||||
Until now, we've used `dep` for managing dependencies, we've now switch to
|
||||
using Go modules. For users this does not change much, only if you want to
|
||||
compile restic without downloading anything with Go 1.11, then you need to run:
|
||||
`go build -mod=vendor build.go`
|
||||
|
||||
https://github.com/restic/restic/pull/1920
|
||||
15
changelog/0.9.3_2018-10-13/pull-1949
Normal file
15
changelog/0.9.3_2018-10-13/pull-1949
Normal file
@@ -0,0 +1,15 @@
|
||||
Enhancement: Add new command `self-update`
|
||||
|
||||
We have added a new command called `self-update` which downloads the
|
||||
latest released version of restic from GitHub and replaces the current
|
||||
binary with it. It does not rely on any external program (so it'll work
|
||||
everywhere), but still verifies the GPG signature using the embedded GPG
|
||||
public key.
|
||||
|
||||
By default, the `self-update` command is hidden behind the `selfupdate`
|
||||
built tag, which is only set when restic is built using `build.go` (including
|
||||
official releases). The reason for this is that downstream distributions will
|
||||
then not include the command by default, so users are encouraged to use the
|
||||
platform-specific distribution mechanism.
|
||||
|
||||
https://github.com/restic/restic/pull/1949
|
||||
7
changelog/0.9.3_2018-10-13/pull-1953
Normal file
7
changelog/0.9.3_2018-10-13/pull-1953
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: ls: Add JSON output support for restic ls cmd
|
||||
|
||||
We've implemented listing files in the repository with JSON as output, just
|
||||
pass `--json` as an option to `restic ls`. This makes the output of the command
|
||||
machine readable.
|
||||
|
||||
https://github.com/restic/restic/pull/1953
|
||||
13
changelog/0.9.3_2018-10-13/pull-1962
Normal file
13
changelog/0.9.3_2018-10-13/pull-1962
Normal file
@@ -0,0 +1,13 @@
|
||||
Enhancement: Stream JSON output for ls command
|
||||
|
||||
The `ls` command now supports JSON output with the global `--json`
|
||||
flag, and this change streams out JSON messages one object at a time
|
||||
rather than en entire array buffered in memory before encoding. The
|
||||
advantage is it allows large listings to be handled efficiently.
|
||||
|
||||
Two message types are printed: snapshots and nodes. A snapshot
|
||||
object will precede node objects which belong to that snapshot.
|
||||
The `struct_type` field can be used to determine which kind of
|
||||
message an object is.
|
||||
|
||||
https://github.com/restic/restic/pull/1962
|
||||
11
changelog/0.9.4_2019-01-06/issue-1605
Normal file
11
changelog/0.9.4_2019-01-06/issue-1605
Normal file
@@ -0,0 +1,11 @@
|
||||
Enhancement: Concurrent restore
|
||||
|
||||
This change significantly improves restore performance, especially
|
||||
when using high-latency remote repositories like B2.
|
||||
|
||||
The implementation now uses several concurrent threads to download and process
|
||||
multiple remote files concurrently. To further reduce restore time, each remote
|
||||
file is downloaded using a single repository request.
|
||||
|
||||
https://github.com/restic/restic/issues/1605
|
||||
https://github.com/restic/restic/pull/1719
|
||||
7
changelog/0.9.4_2019-01-06/issue-1989
Normal file
7
changelog/0.9.4_2019-01-06/issue-1989
Normal file
@@ -0,0 +1,7 @@
|
||||
Bugfix: Google Cloud Storage: Respect bandwidth limit
|
||||
|
||||
The GCS backend did not respect the bandwidth limit configured, a previous
|
||||
commit accidentally removed support for it.
|
||||
|
||||
https://github.com/restic/restic/issues/1989
|
||||
https://github.com/restic/restic/pull/2100
|
||||
11
changelog/0.9.4_2019-01-06/issue-2040
Normal file
11
changelog/0.9.4_2019-01-06/issue-2040
Normal file
@@ -0,0 +1,11 @@
|
||||
Bugfix: Add host name filter shorthand flag for `stats` command
|
||||
|
||||
The default value for `--host` flag was set to 'H' (the shorthand version of
|
||||
the flag), this caused the lookup for the latest snapshot to fail.
|
||||
|
||||
Add shorthand flag `-H` for `--host` (with empty default so if these flags
|
||||
are not specified the latest snapshot will not filter by host name).
|
||||
|
||||
Also add shorthand `-H` for `backup` command.
|
||||
|
||||
https://github.com/restic/restic/issues/2040
|
||||
9
changelog/0.9.4_2019-01-06/issue-2089
Normal file
9
changelog/0.9.4_2019-01-06/issue-2089
Normal file
@@ -0,0 +1,9 @@
|
||||
Enhancement: increase granularity of the "keep within" retention policy
|
||||
|
||||
The `keep-within` option of the `forget` command now accepts time ranges with
|
||||
an hourly granularity. For example, running `restic forget --keep-within 3d12h`
|
||||
will keep all the snapshots made within three days and twelve hours from the
|
||||
time of the latest snapshot.
|
||||
|
||||
https://github.com/restic/restic/issues/2089
|
||||
https://github.com/restic/restic/pull/2090
|
||||
12
changelog/0.9.4_2019-01-06/issue-2097
Normal file
12
changelog/0.9.4_2019-01-06/issue-2097
Normal file
@@ -0,0 +1,12 @@
|
||||
Enhancement: Add key hinting
|
||||
|
||||
Added a new option `--key-hint` and corresponding environment variable
|
||||
`RESTIC_KEY_HINT`. The key hint is a key ID to try decrypting first, before
|
||||
other keys in the repository.
|
||||
|
||||
This change will benefit repositories with many keys; if the correct key hint
|
||||
is supplied then restic only needs to check one key. If the key hint is
|
||||
incorrect (the key does not exist, or the password is incorrect) then restic
|
||||
will check all keys, as usual.
|
||||
|
||||
https://github.com/restic/restic/issues/2097
|
||||
11
changelog/0.9.4_2019-01-06/pull-2017
Normal file
11
changelog/0.9.4_2019-01-06/pull-2017
Normal file
@@ -0,0 +1,11 @@
|
||||
Enhancement: mount: Enforce FUSE Unix permissions with allow-other
|
||||
|
||||
The fuse mount (`restic mount`) now lets the kernel check the permissions of
|
||||
the files within snapshots (this is done through the `DefaultPermissions` FUSE
|
||||
option) when the option `--allow-other` is specified.
|
||||
|
||||
To restore the old behavior, we've added the `--no-default-permissions` option.
|
||||
This allows all users that have access to the mount point to access all
|
||||
files within the snapshots.
|
||||
|
||||
https://github.com/restic/restic/pull/2017
|
||||
6
changelog/0.9.4_2019-01-06/pull-2068
Normal file
6
changelog/0.9.4_2019-01-06/pull-2068
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Correctly return error loading data
|
||||
|
||||
In one case during `prune` and `check`, an error loading data from the backend is not returned properly. This is now corrected.
|
||||
|
||||
https://github.com/restic/restic/pull/2068
|
||||
https://github.com/restic/restic/issues/1999#issuecomment-433737921
|
||||
7
changelog/0.9.4_2019-01-06/pull-2070
Normal file
7
changelog/0.9.4_2019-01-06/pull-2070
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Make all commands display timestamps in local time
|
||||
|
||||
Restic used to drop the timezone information from displayed timestamps, it now
|
||||
converts timestamps to local time before printing them so the times can be
|
||||
easily compared to.
|
||||
|
||||
https://github.com/restic/restic/pull/2070
|
||||
7
changelog/0.9.4_2019-01-06/pull-2086
Normal file
7
changelog/0.9.4_2019-01-06/pull-2086
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Allow --files-from to be specified multiple times
|
||||
|
||||
Before, restic took only the last file specified with `--files-from` into
|
||||
account, this is now corrected.
|
||||
|
||||
https://github.com/restic/restic/issues/2085
|
||||
https://github.com/restic/restic/pull/2086
|
||||
8
changelog/0.9.4_2019-01-06/pull-2094
Normal file
8
changelog/0.9.4_2019-01-06/pull-2094
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Run command to get password
|
||||
|
||||
We've added the `--password-command` option which allows specifying a command
|
||||
that restic runs every time the password for the repository is needed, so it
|
||||
can be integrated with a password manager or keyring. The option can also be
|
||||
set via the environment variable `$RESTIC_PASSWORD_COMMAND`.
|
||||
|
||||
https://github.com/restic/restic/pull/2094
|
||||
7
changelog/0.9.4_2019-01-06/pull-2095
Normal file
7
changelog/0.9.4_2019-01-06/pull-2095
Normal file
@@ -0,0 +1,7 @@
|
||||
Bugfix: consistently use local time for snapshots times
|
||||
|
||||
By default snapshots created with restic backup were set to local time,
|
||||
but when the --time flag was used the provided timestamp was parsed as
|
||||
UTC. With this change all snapshots times are set to local time.
|
||||
|
||||
https://github.com/restic/restic/pull/2095
|
||||
7
changelog/0.9.5_2019-04-23/issue-1895
Normal file
7
changelog/0.9.5_2019-04-23/issue-1895
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Add case insensitive include & exclude options
|
||||
|
||||
The backup and restore commands now have --iexclude and --iinclude flags
|
||||
as case insensitive variants of --exclude and --include.
|
||||
|
||||
https://github.com/restic/restic/issues/1895
|
||||
https://github.com/restic/restic/pull/2032
|
||||
8
changelog/0.9.5_2019-04-23/issue-1937
Normal file
8
changelog/0.9.5_2019-04-23/issue-1937
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Support streaming JSON output for backup
|
||||
|
||||
We've added support for getting machine-readable status output during backup,
|
||||
just pass the flag `--json` for `restic backup` and restic will output a stream
|
||||
of JSON objects which contain the current progress.
|
||||
|
||||
https://github.com/restic/restic/issues/1937
|
||||
https://github.com/restic/restic/pull/1944
|
||||
10
changelog/0.9.5_2019-04-23/issue-2135
Normal file
10
changelog/0.9.5_2019-04-23/issue-2135
Normal file
@@ -0,0 +1,10 @@
|
||||
Bugfix: Return error when no bytes could be read from stdin
|
||||
|
||||
We assume that users reading backup data from stdin want to know when no data
|
||||
could be read, so now restic returns an error when `backup --stdin` is called
|
||||
but no bytes could be read. Usually, this means that an earlier command in a
|
||||
pipe has failed. The documentation was amended and now recommends setting the
|
||||
`pipefail` option (`set -o pipefail`).
|
||||
|
||||
https://github.com/restic/restic/pull/2135
|
||||
https://github.com/restic/restic/pull/2139
|
||||
8
changelog/0.9.5_2019-04-23/issue-2155
Normal file
8
changelog/0.9.5_2019-04-23/issue-2155
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: add Openstack application credential auth for Swift
|
||||
|
||||
Since Openstack Queens Identity (auth V3) service supports an application
|
||||
credential auth method. It allows to create a technical account with the
|
||||
limited roles. This commit adds an application credential authentication
|
||||
method for the Swift backend.
|
||||
|
||||
https://github.com/restic/restic/issues/2155
|
||||
15
changelog/0.9.5_2019-04-23/issue-2179
Normal file
15
changelog/0.9.5_2019-04-23/issue-2179
Normal file
@@ -0,0 +1,15 @@
|
||||
Enhancement: 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
|
||||
3
changelog/0.9.5_2019-04-23/issue-2181
Normal file
3
changelog/0.9.5_2019-04-23/issue-2181
Normal file
@@ -0,0 +1,3 @@
|
||||
Bugfix: Don't cancel timeout after 30 seconds for self-update
|
||||
|
||||
https://github.com/restic/restic/issues/2181
|
||||
8
changelog/0.9.5_2019-04-23/issue-2184
Normal file
8
changelog/0.9.5_2019-04-23/issue-2184
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Add --json support to forget command
|
||||
|
||||
The forget command now supports the --json argument, outputting the
|
||||
information about what is (or would-be) kept and removed from the
|
||||
repository.
|
||||
|
||||
https://github.com/restic/restic/issues/2184
|
||||
https://github.com/restic/restic/pull/2185
|
||||
6
changelog/0.9.5_2019-04-23/issue-2203
Normal file
6
changelog/0.9.5_2019-04-23/issue-2203
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Fix reading passwords from stdin
|
||||
|
||||
Passwords for the `init`, `key add`, and `key passwd` commands can now be read from
|
||||
non-terminal stdin.
|
||||
|
||||
https://github.com/restic/restic/issues/2203
|
||||
9
changelog/0.9.5_2019-04-23/issue-2224
Normal file
9
changelog/0.9.5_2019-04-23/issue-2224
Normal file
@@ -0,0 +1,9 @@
|
||||
Bugfix: Don't abort the find command when a tree can't be loaded
|
||||
|
||||
Change the find command so that missing trees don't result in a crash.
|
||||
Instead, the error is logged to the debug log, and the tree ID is displayed
|
||||
along with the snapshot it belongs to. This makes it possible to recover
|
||||
repositories that are missing trees by forgetting the snapshots they are used
|
||||
in.
|
||||
|
||||
https://github.com/restic/restic/issues/2224
|
||||
10
changelog/0.9.5_2019-04-23/pull-2087
Normal file
10
changelog/0.9.5_2019-04-23/pull-2087
Normal file
@@ -0,0 +1,10 @@
|
||||
Enhancement: Add group-by option to snapshots command
|
||||
|
||||
We have added an option to group the output of the snapshots command, similar
|
||||
to the output of the forget command. The option has been called "--group-by"
|
||||
and accepts any combination of the values "host", "paths" and "tags", separated
|
||||
by commas. Default behavior (not specifying --group-by) has not been changed.
|
||||
We have added support of the grouping to the JSON output.
|
||||
|
||||
https://github.com/restic/restic/issues/2037
|
||||
https://github.com/restic/restic/pull/2087
|
||||
8
changelog/0.9.5_2019-04-23/pull-2124
Normal file
8
changelog/0.9.5_2019-04-23/pull-2124
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Ability to dump folders to tar via stdout
|
||||
|
||||
We've added the ability to dump whole folders to stdout via the `dump` command.
|
||||
Restic now requires at least Go 1.10 due to a limitation of the standard
|
||||
library for Go <= 1.9.
|
||||
|
||||
https://github.com/restic/restic/pull/2124
|
||||
https://github.com/restic/restic/issues/2123
|
||||
8
changelog/0.9.5_2019-04-23/pull-2139
Normal file
8
changelog/0.9.5_2019-04-23/pull-2139
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Return error if no bytes could be read for `backup --stdin`
|
||||
|
||||
When restic is used to backup the output of a program, like `mysqldump | restic
|
||||
backup --stdin`, it now returns an error if no bytes could be read at all. This
|
||||
catches the failure case when `mysqldump` failed for some reason and did not
|
||||
output any data to stdout.
|
||||
|
||||
https://github.com/restic/restic/pull/2139
|
||||
10
changelog/0.9.5_2019-04-23/pull-2205
Normal file
10
changelog/0.9.5_2019-04-23/pull-2205
Normal file
@@ -0,0 +1,10 @@
|
||||
Enhancement: Add --ignore-inode option to backup cmd
|
||||
|
||||
This option handles backup of virtual filesystems that do not keep fixed
|
||||
inodes for files, like Fuse-based, pCloud, etc. Ignoring inode changes allows
|
||||
to consider the file as unchanged if last modification date and size
|
||||
are unchanged.
|
||||
|
||||
https://github.com/restic/restic/pull/2205
|
||||
https://github.com/restic/restic/pull/2047
|
||||
https://github.com/restic/restic/issues/1631
|
||||
16
changelog/0.9.5_2019-04-23/pull-2220
Normal file
16
changelog/0.9.5_2019-04-23/pull-2220
Normal file
@@ -0,0 +1,16 @@
|
||||
Enhancement: Add config option to set S3 storage class
|
||||
|
||||
The `s3.storage-class` option can be passed to restic (using `-o`) to
|
||||
specify the storage class to be used for S3 objects created by restic.
|
||||
|
||||
The storage class is passed as-is to S3, so it needs to be understood by
|
||||
the API. On AWS, it can be one of `STANDARD`, `STANDARD_IA`,
|
||||
`ONEZONE_IA`, `INTELLIGENT_TIERING` and `REDUCED_REDUNDANCY`. If
|
||||
unspecified, the default storage class is used (`STANDARD` on AWS).
|
||||
|
||||
You can mix storage classes in the same bucket, and the setting isn't
|
||||
stored in the restic repository, so be sure to specify it with each
|
||||
command that writes to S3.
|
||||
|
||||
https://github.com/restic/restic/pull/2220
|
||||
https://github.com/restic/restic/issues/706
|
||||
6
changelog/0.9.6_2019-11-22/issue-2063
Normal file
6
changelog/0.9.6_2019-11-22/issue-2063
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Allow absolute path for filename when backing up from stdin
|
||||
|
||||
When backing up from stdin, handle directory path for `--stdin-filename`.
|
||||
This can be used to specify the full path for the backed-up file.
|
||||
|
||||
https://github.com/restic/restic/issues/2063
|
||||
10
changelog/0.9.6_2019-11-22/issue-2174
Normal file
10
changelog/0.9.6_2019-11-22/issue-2174
Normal file
@@ -0,0 +1,10 @@
|
||||
Bugfix: Save files with invalid timestamps
|
||||
|
||||
When restic reads invalid timestamps (year is before 0000 or after 9999) it
|
||||
refused to read and archive the file. We've changed the behavior and will now
|
||||
save modified timestamps with the year set to either 0000 or 9999, the rest of
|
||||
the timestamp stays the same, so the file will be saved (albeit with a bogus
|
||||
timestamp).
|
||||
|
||||
https://github.com/restic/restic/issues/2174
|
||||
https://github.com/restic/restic/issues/1173
|
||||
6
changelog/0.9.6_2019-11-22/issue-2249
Normal file
6
changelog/0.9.6_2019-11-22/issue-2249
Normal file
@@ -0,0 +1,6 @@
|
||||
Bugfix: Read fresh metadata for unmodified files
|
||||
|
||||
Restic took all metadata for files which were detected as unmodified, not taking into account changed metadata (ownership, mode). This is now corrected.
|
||||
|
||||
https://github.com/restic/restic/issues/2249
|
||||
https://github.com/restic/restic/pull/2252
|
||||
7
changelog/0.9.6_2019-11-22/issue-2301
Normal file
7
changelog/0.9.6_2019-11-22/issue-2301
Normal file
@@ -0,0 +1,7 @@
|
||||
Bugfix: Add upper bound for t in --read-data-subset=n/t
|
||||
|
||||
256 is the effective maximum for t, but restic would allow larger
|
||||
values, leading to strange behavior.
|
||||
|
||||
https://github.com/restic/restic/issues/2301
|
||||
https://github.com/restic/restic/pull/2304
|
||||
7
changelog/0.9.6_2019-11-22/issue-2306
Normal file
7
changelog/0.9.6_2019-11-22/issue-2306
Normal file
@@ -0,0 +1,7 @@
|
||||
Enhancement: Allow multiple retries for interactive password input
|
||||
|
||||
Restic used to quit if the repository password was typed incorrectly once.
|
||||
Restic will now ask the user again for the repository password if typed incorrectly.
|
||||
The user will now get three tries to input the correct password before restic quits.
|
||||
|
||||
https://github.com/restic/restic/issues/2306
|
||||
6
changelog/0.9.6_2019-11-22/issue-2330
Normal file
6
changelog/0.9.6_2019-11-22/issue-2330
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Make `--group-by` accept both singular and plural
|
||||
|
||||
One can now use the values `host`/`hosts`, `path`/`paths` and
|
||||
`tag` / `tags` interchangeably in the `--group-by` argument.
|
||||
|
||||
https://github.com/restic/restic/issues/2330
|
||||
8
changelog/0.9.6_2019-11-22/pull-2321
Normal file
8
changelog/0.9.6_2019-11-22/pull-2321
Normal file
@@ -0,0 +1,8 @@
|
||||
Bugfix: Check errors when loading index files
|
||||
|
||||
Restic now checks and handles errors which occur when loading index files, the
|
||||
missing check leads to odd errors (and a stack trace printed to users) later.
|
||||
This was reported in the forum.
|
||||
|
||||
https://github.com/restic/restic/pull/2321
|
||||
https://forum.restic.net/t/check-rebuild-index-prune/1848/13
|
||||
8
changelog/0.9.6_2019-11-22/pull-2350
Normal file
8
changelog/0.9.6_2019-11-22/pull-2350
Normal file
@@ -0,0 +1,8 @@
|
||||
Enhancement: Add option to configure S3 region
|
||||
|
||||
We've added a new option for setting the region when accessing an S3-compatible
|
||||
service. For some providers, it is required to set this to a valid value. You
|
||||
can do that either by setting the environment variable `AWS_DEFAULT_REGION` or
|
||||
using the option `s3.region`, e.g. like this: `-o s3.region="us-east-1"`.
|
||||
|
||||
https://github.com/restic/restic/pull/2350
|
||||
131
cmd/restic/acl.go
Normal file
131
cmd/restic/acl.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package main
|
||||
|
||||
// Adapted from https://github.com/maxymania/go-system/blob/master/posix_acl/posix_acl.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
aclUserOwner = 0x0001
|
||||
aclUser = 0x0002
|
||||
aclGroupOwner = 0x0004
|
||||
aclGroup = 0x0008
|
||||
aclMask = 0x0010
|
||||
aclOthers = 0x0020
|
||||
)
|
||||
|
||||
type aclSID uint64
|
||||
|
||||
type aclElem struct {
|
||||
Tag uint16
|
||||
Perm uint16
|
||||
ID uint32
|
||||
}
|
||||
|
||||
type acl struct {
|
||||
Version uint32
|
||||
List []aclElement
|
||||
}
|
||||
|
||||
type aclElement struct {
|
||||
aclSID
|
||||
Perm uint16
|
||||
}
|
||||
|
||||
func (a *aclSID) setUID(uid uint32) {
|
||||
*a = aclSID(uid) | (aclUser << 32)
|
||||
}
|
||||
func (a *aclSID) setGID(gid uint32) {
|
||||
*a = aclSID(gid) | (aclGroup << 32)
|
||||
}
|
||||
|
||||
func (a *aclSID) setType(tp int) {
|
||||
*a = aclSID(tp) << 32
|
||||
}
|
||||
|
||||
func (a aclSID) getType() int {
|
||||
return int(a >> 32)
|
||||
}
|
||||
func (a aclSID) getID() uint32 {
|
||||
return uint32(a & 0xffffffff)
|
||||
}
|
||||
func (a aclSID) String() string {
|
||||
switch a >> 32 {
|
||||
case aclUserOwner:
|
||||
return "user::"
|
||||
case aclUser:
|
||||
return fmt.Sprintf("user:%v:", a.getID())
|
||||
case aclGroupOwner:
|
||||
return "group::"
|
||||
case aclGroup:
|
||||
return fmt.Sprintf("group:%v:", a.getID())
|
||||
case aclMask:
|
||||
return "mask::"
|
||||
case aclOthers:
|
||||
return "other::"
|
||||
}
|
||||
return "?:"
|
||||
}
|
||||
|
||||
func (a aclElement) String() string {
|
||||
str := ""
|
||||
if (a.Perm & 4) != 0 {
|
||||
str += "r"
|
||||
} else {
|
||||
str += "-"
|
||||
}
|
||||
if (a.Perm & 2) != 0 {
|
||||
str += "w"
|
||||
} else {
|
||||
str += "-"
|
||||
}
|
||||
if (a.Perm & 1) != 0 {
|
||||
str += "x"
|
||||
} else {
|
||||
str += "-"
|
||||
}
|
||||
return fmt.Sprintf("%v%v", a.aclSID, str)
|
||||
}
|
||||
|
||||
func (a *acl) decode(xattr []byte) {
|
||||
var elem aclElement
|
||||
ae := new(aclElem)
|
||||
nr := bytes.NewReader(xattr)
|
||||
e := binary.Read(nr, binary.LittleEndian, &a.Version)
|
||||
if e != nil {
|
||||
a.Version = 0
|
||||
return
|
||||
}
|
||||
if len(a.List) > 0 {
|
||||
a.List = a.List[:0]
|
||||
}
|
||||
for binary.Read(nr, binary.LittleEndian, ae) == nil {
|
||||
elem.aclSID = (aclSID(ae.Tag) << 32) | aclSID(ae.ID)
|
||||
elem.Perm = ae.Perm
|
||||
a.List = append(a.List, elem)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *acl) encode() []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
ae := new(aclElem)
|
||||
binary.Write(buf, binary.LittleEndian, &a.Version)
|
||||
for _, elem := range a.List {
|
||||
ae.Tag = uint16(elem.getType())
|
||||
ae.Perm = elem.Perm
|
||||
ae.ID = elem.getID()
|
||||
binary.Write(buf, binary.LittleEndian, ae)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (a *acl) String() string {
|
||||
var finalacl string
|
||||
for _, acl := range a.List {
|
||||
finalacl += acl.String() + "\n"
|
||||
}
|
||||
return finalacl
|
||||
}
|
||||
96
cmd/restic/acl_test.go
Normal file
96
cmd/restic/acl_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_acl_decode(t *testing.T) {
|
||||
type args struct {
|
||||
xattr []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "decode string",
|
||||
args: args{
|
||||
xattr: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255},
|
||||
},
|
||||
want: "user::rw-\nuser:0:rwx\nuser:65534:rwx\ngroup::rwx\nmask::rwx\nother::r--\n",
|
||||
},
|
||||
{
|
||||
name: "decode fail",
|
||||
args: args{
|
||||
xattr: []byte("abctest"),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &acl{}
|
||||
a.decode(tt.args.xattr)
|
||||
if tt.want != a.String() {
|
||||
t.Errorf("acl.decode() = %v, want: %v", a.String(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_acl_encode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []byte
|
||||
args []aclElement
|
||||
}{
|
||||
{
|
||||
name: "encode values",
|
||||
want: []byte{2, 0, 0, 0, 1, 0, 6, 0, 255, 255, 255, 255, 2, 0, 7, 0, 0, 0, 0, 0, 2, 0, 7, 0, 254, 255, 0, 0, 4, 0, 7, 0, 255, 255, 255, 255, 16, 0, 7, 0, 255, 255, 255, 255, 32, 0, 4, 0, 255, 255, 255, 255},
|
||||
args: []aclElement{
|
||||
{
|
||||
aclSID: 8589934591,
|
||||
Perm: 6,
|
||||
},
|
||||
{
|
||||
aclSID: 8589934592,
|
||||
Perm: 7,
|
||||
},
|
||||
{
|
||||
aclSID: 8590000126,
|
||||
Perm: 7,
|
||||
},
|
||||
{
|
||||
aclSID: 21474836479,
|
||||
Perm: 7,
|
||||
},
|
||||
{
|
||||
aclSID: 73014444031,
|
||||
Perm: 7,
|
||||
},
|
||||
{
|
||||
aclSID: 141733920767,
|
||||
Perm: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "encode fail",
|
||||
want: []byte{2, 0, 0, 0},
|
||||
args: []aclElement{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &acl{
|
||||
Version: 2,
|
||||
List: tt.args,
|
||||
}
|
||||
if got := a.encode(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("acl.encode() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,12 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -21,6 +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/termstatus"
|
||||
)
|
||||
|
||||
@@ -32,19 +37,23 @@ The "backup" command creates a new snapshot and saves the files and directories
|
||||
given as the arguments.
|
||||
`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
if backupOptions.Hostname == "" {
|
||||
if backupOptions.Host == "" {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
debug.Log("os.Hostname() returned err: %v", err)
|
||||
return
|
||||
}
|
||||
backupOptions.Hostname = hostname
|
||||
backupOptions.Host = hostname
|
||||
}
|
||||
},
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if backupOptions.Stdin && backupOptions.FilesFrom == "-" {
|
||||
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
|
||||
if backupOptions.Stdin {
|
||||
for _, filename := range backupOptions.FilesFrom {
|
||||
if filename == "-" {
|
||||
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var t tomb.Tomb
|
||||
@@ -62,20 +71,22 @@ given as the arguments.
|
||||
|
||||
// BackupOptions bundles all options for the backup command.
|
||||
type BackupOptions struct {
|
||||
Parent string
|
||||
Force bool
|
||||
Excludes []string
|
||||
ExcludeFiles []string
|
||||
ExcludeOtherFS bool
|
||||
ExcludeIfPresent []string
|
||||
ExcludeCaches bool
|
||||
Stdin bool
|
||||
StdinFilename string
|
||||
Tags []string
|
||||
Hostname string
|
||||
FilesFrom string
|
||||
TimeStamp string
|
||||
WithAtime bool
|
||||
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
|
||||
}
|
||||
|
||||
var backupOptions BackupOptions
|
||||
@@ -87,17 +98,23 @@ func init() {
|
||||
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.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
|
||||
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
|
||||
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
||||
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file`)
|
||||
f.BoolVar(&backupOptions.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.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
|
||||
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "file name to use when reading from stdin")
|
||||
f.StringArrayVar(&backupOptions.Tags, "tag", nil, "add a `tag` for the new snapshot (can be specified multiple times)")
|
||||
f.StringVar(&backupOptions.Hostname, "hostname", "", "set the `hostname` for the snapshot manually. To prevent an expensive rescan use the \"parent\" flag")
|
||||
f.StringVar(&backupOptions.FilesFrom, "files-from", "", "read the files to backup from file (can be combined with file args)")
|
||||
|
||||
f.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.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")
|
||||
}
|
||||
|
||||
// filterExisting returns a slice of all existing items, or an error if no
|
||||
@@ -169,12 +186,16 @@ func readLinesFromFile(filename string) ([]string, error) {
|
||||
|
||||
// Check returns an error when an invalid combination of options was set.
|
||||
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
||||
if opts.FilesFrom == "-" && gopts.password == "" {
|
||||
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
|
||||
if gopts.password == "" {
|
||||
for _, filename := range opts.FilesFrom {
|
||||
if filename == "-" {
|
||||
return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Stdin {
|
||||
if opts.FilesFrom != "" {
|
||||
if len(opts.FilesFrom) > 0 {
|
||||
return errors.Fatal("--stdin and --files-from cannot be used together")
|
||||
}
|
||||
|
||||
@@ -186,18 +207,9 @@ func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectRejectFuncs returns a list of all functions which may reject data
|
||||
// from being saved in a snapshot
|
||||
func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, err error) {
|
||||
// allowed devices
|
||||
if opts.ExcludeOtherFS {
|
||||
f, err := rejectByDevice(targets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fs = append(fs, f)
|
||||
}
|
||||
|
||||
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
||||
// from being saved in a snapshot based on path only
|
||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectByNameFunc, err error) {
|
||||
// exclude restic cache
|
||||
if repo.Cache != nil {
|
||||
f, err := rejectResticCache(repo)
|
||||
@@ -210,7 +222,15 @@ func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets
|
||||
|
||||
// add patterns from file
|
||||
if len(opts.ExcludeFiles) > 0 {
|
||||
opts.Excludes = append(opts.Excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...)
|
||||
excludes, err := readExcludePatternsFromFiles(opts.ExcludeFiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.Excludes = append(opts.Excludes, excludes...)
|
||||
}
|
||||
|
||||
if len(opts.InsensitiveExcludes) > 0 {
|
||||
fs = append(fs, rejectByInsensitivePattern(opts.InsensitiveExcludes))
|
||||
}
|
||||
|
||||
if len(opts.Excludes) > 0 {
|
||||
@@ -233,9 +253,34 @@ func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// collectRejectFuncs returns a list of all functions which may reject data
|
||||
// from being saved in a snapshot based on path and file info
|
||||
func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, err error) {
|
||||
// allowed devices
|
||||
if opts.ExcludeOtherFS && !opts.Stdin {
|
||||
f, err := rejectByDevice(targets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fs = append(fs, f)
|
||||
}
|
||||
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// readExcludePatternsFromFiles reads all exclude files and returns the list of
|
||||
// exclude patterns.
|
||||
func readExcludePatternsFromFiles(excludeFiles []string) []string {
|
||||
// exclude patterns. For each line, leading and trailing white space is removed
|
||||
// and comment lines are ignored. For each remaining pattern, environment
|
||||
// variables are resolved. For adding a literal dollar sign ($), write $$ to
|
||||
// the file.
|
||||
func readExcludePatternsFromFiles(excludeFiles []string) ([]string, error) {
|
||||
getenvOrDollar := func(s string) string {
|
||||
if s == "$" {
|
||||
return "$"
|
||||
}
|
||||
return os.Getenv(s)
|
||||
}
|
||||
|
||||
var excludes []string
|
||||
for _, filename := range excludeFiles {
|
||||
err := func() (err error) {
|
||||
@@ -258,17 +303,16 @@ func readExcludePatternsFromFiles(excludeFiles []string) []string {
|
||||
continue
|
||||
}
|
||||
|
||||
line = os.ExpandEnv(line)
|
||||
line = os.Expand(line, getenvOrDollar)
|
||||
excludes = append(excludes, line)
|
||||
}
|
||||
return scanner.Err()
|
||||
}()
|
||||
if err != nil {
|
||||
Warnf("error reading exclude patterns: %v:", err)
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return excludes
|
||||
return excludes, nil
|
||||
}
|
||||
|
||||
// collectTargets returns a list of target files/dirs from several sources.
|
||||
@@ -277,15 +321,31 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fromfile, err := readLinesFromFile(opts.FilesFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var lines []string
|
||||
for _, file := range opts.FilesFrom {
|
||||
fromfile, err := readLinesFromFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// expand wildcards
|
||||
for _, line := range fromfile {
|
||||
var expanded []string
|
||||
expanded, err := filepath.Glob(line)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, fmt.Sprintf("pattern: %s", line))
|
||||
}
|
||||
if len(expanded) == 0 {
|
||||
Warnf("pattern %q does not match any files, skipping\n", line)
|
||||
}
|
||||
lines = append(lines, expanded...)
|
||||
}
|
||||
}
|
||||
|
||||
// merge files from files-from into normal args so we can reuse the normal
|
||||
// args checks and have the ability to use both files-from and args at the
|
||||
// same time
|
||||
args = append(args, fromfile...)
|
||||
args = append(args, lines...)
|
||||
if len(args) == 0 && !opts.Stdin {
|
||||
return nil, errors.Fatal("nothing to backup, please specify target files/dirs")
|
||||
}
|
||||
@@ -314,7 +374,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.Hostname)
|
||||
id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, opts.Host)
|
||||
if err == nil {
|
||||
parentID = &id
|
||||
} else if err != restic.ErrNoSnapshotFound {
|
||||
@@ -338,7 +398,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
|
||||
timeStamp := time.Now()
|
||||
if opts.TimeStamp != "" {
|
||||
timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp)
|
||||
timeStamp, err = time.ParseInLocation(TimeFormat, opts.TimeStamp, time.Local)
|
||||
if err != nil {
|
||||
return errors.Fatalf("error in time option: %v\n", err)
|
||||
}
|
||||
@@ -346,7 +406,43 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
|
||||
var t tomb.Tomb
|
||||
|
||||
p := ui.NewBackup(term, gopts.verbosity)
|
||||
if gopts.verbosity >= 2 && !gopts.JSON {
|
||||
term.Print("open repository\n")
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type ArchiveProgressReporter interface {
|
||||
CompleteItem(item string, previous, current *restic.Node, s archiver.ItemStats, d time.Duration)
|
||||
StartFile(filename string)
|
||||
CompleteBlob(filename string, bytes uint64)
|
||||
ScannerError(item string, fi os.FileInfo, err error) error
|
||||
ReportTotal(item string, s archiver.ScanStats)
|
||||
SetMinUpdatePause(d time.Duration)
|
||||
Run(ctx context.Context) error
|
||||
Error(item string, fi os.FileInfo, err error) error
|
||||
Finish(snapshotID restic.ID)
|
||||
|
||||
// ui.StdioWrapper
|
||||
Stdout() io.WriteCloser
|
||||
Stderr() io.WriteCloser
|
||||
|
||||
// ui.Message
|
||||
E(msg string, args ...interface{})
|
||||
P(msg string, args ...interface{})
|
||||
V(msg string, args ...interface{})
|
||||
VV(msg string, args ...interface{})
|
||||
}
|
||||
|
||||
var p ArchiveProgressReporter
|
||||
if gopts.JSON {
|
||||
p = jsonstatus.NewBackup(term, gopts.verbosity)
|
||||
} else {
|
||||
p = ui.NewBackup(term, gopts.verbosity)
|
||||
}
|
||||
|
||||
// use the terminal for stdout/stderr
|
||||
prevStdout, prevStderr := gopts.stdout, gopts.stderr
|
||||
@@ -361,32 +457,36 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
if fps > 60 {
|
||||
fps = 60
|
||||
}
|
||||
p.MinUpdatePause = time.Second / time.Duration(fps)
|
||||
p.SetMinUpdatePause(time.Second / time.Duration(fps))
|
||||
}
|
||||
}
|
||||
|
||||
t.Go(func() error { return p.Run(t.Context(gopts.ctx)) })
|
||||
|
||||
p.V("open repository")
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
if !gopts.JSON {
|
||||
p.V("lock repository")
|
||||
}
|
||||
|
||||
p.V("lock repository")
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// rejectFuncs collect functions that can reject items from the backup
|
||||
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
|
||||
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, targets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
||||
rejectFuncs, err := collectRejectFuncs(opts, repo, targets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.V("load index files")
|
||||
if !gopts.JSON {
|
||||
p.V("load index files")
|
||||
}
|
||||
err = repo.LoadIndex(gopts.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -397,10 +497,19 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
return err
|
||||
}
|
||||
|
||||
if parentSnapshotID != nil {
|
||||
if !gopts.JSON && parentSnapshotID != nil {
|
||||
p.V("using parent snapshot %v\n", parentSnapshotID.Str())
|
||||
}
|
||||
|
||||
selectByNameFilter := func(item string) bool {
|
||||
for _, reject := range rejectByNameFuncs {
|
||||
if reject(item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
selectFilter := func(item string, fi os.FileInfo) bool {
|
||||
for _, reject := range rejectFuncs {
|
||||
if reject(item, fi) {
|
||||
@@ -412,31 +521,39 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
|
||||
var targetFS fs.FS = fs.Local{}
|
||||
if opts.Stdin {
|
||||
p.V("read data from stdin")
|
||||
if !gopts.JSON {
|
||||
p.V("read data from stdin")
|
||||
}
|
||||
filename := path.Join("/", opts.StdinFilename)
|
||||
targetFS = &fs.Reader{
|
||||
ModTime: timeStamp,
|
||||
Name: opts.StdinFilename,
|
||||
Name: filename,
|
||||
Mode: 0644,
|
||||
ReadCloser: os.Stdin,
|
||||
}
|
||||
targets = []string{opts.StdinFilename}
|
||||
targets = []string{filename}
|
||||
}
|
||||
|
||||
sc := archiver.NewScanner(targetFS)
|
||||
sc.SelectByName = selectByNameFilter
|
||||
sc.Select = selectFilter
|
||||
sc.Error = p.ScannerError
|
||||
sc.Result = p.ReportTotal
|
||||
|
||||
p.V("start scan on %v", targets)
|
||||
if !gopts.JSON {
|
||||
p.V("start scan on %v", targets)
|
||||
}
|
||||
t.Go(func() error { return sc.Scan(t.Context(gopts.ctx), targets) })
|
||||
|
||||
arch := archiver.New(repo, targetFS, archiver.Options{})
|
||||
arch.SelectByName = selectByNameFilter
|
||||
arch.Select = selectFilter
|
||||
arch.WithAtime = opts.WithAtime
|
||||
arch.Error = p.Error
|
||||
arch.CompleteItem = p.CompleteItemFn
|
||||
arch.CompleteItem = p.CompleteItem
|
||||
arch.StartFile = p.StartFile
|
||||
arch.CompleteBlob = p.CompleteBlob
|
||||
arch.IgnoreInode = opts.IgnoreInode
|
||||
|
||||
if parentSnapshotID == nil {
|
||||
parentSnapshotID = &restic.ID{}
|
||||
@@ -446,17 +563,21 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
Excludes: opts.Excludes,
|
||||
Tags: opts.Tags,
|
||||
Time: timeStamp,
|
||||
Hostname: opts.Hostname,
|
||||
Hostname: opts.Host,
|
||||
ParentSnapshot: *parentSnapshotID,
|
||||
}
|
||||
|
||||
uploader := archiver.IndexUploader{
|
||||
Repository: repo,
|
||||
Start: func() {
|
||||
p.VV("uploading intermediate index")
|
||||
if !gopts.JSON {
|
||||
p.VV("uploading intermediate index")
|
||||
}
|
||||
},
|
||||
Complete: func(id restic.ID) {
|
||||
p.V("uploaded intermediate index %v", id.Str())
|
||||
if !gopts.JSON {
|
||||
p.V("uploaded intermediate index %v", id.Str())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -464,14 +585,18 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
|
||||
return uploader.Upload(gopts.ctx, t.Context(gopts.ctx), 30*time.Second)
|
||||
})
|
||||
|
||||
p.V("start backup on %v", targets)
|
||||
if !gopts.JSON {
|
||||
p.V("start backup on %v", targets)
|
||||
}
|
||||
_, id, err := arch.Snapshot(gopts.ctx, targets, snapshotOpts)
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||
}
|
||||
|
||||
p.Finish()
|
||||
p.P("snapshot %s saved\n", id.Str())
|
||||
p.Finish(id)
|
||||
if !gopts.JSON {
|
||||
p.P("snapshot %s saved\n", id.Str())
|
||||
}
|
||||
|
||||
// cleanly shutdown all running goroutines
|
||||
t.Kill(nil)
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/restic/restic/internal/cache"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -28,6 +30,7 @@ The "cache" command allows listing and cleaning local cache directories.
|
||||
type CacheOptions struct {
|
||||
Cleanup bool
|
||||
MaxAge uint
|
||||
NoSize bool
|
||||
}
|
||||
|
||||
var cacheOptions CacheOptions
|
||||
@@ -38,6 +41,7 @@ func init() {
|
||||
f := cmdCache.Flags()
|
||||
f.BoolVar(&cacheOptions.Cleanup, "cleanup", false, "remove old cache directories")
|
||||
f.UintVar(&cacheOptions.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old")
|
||||
f.BoolVar(&cacheOptions.NoSize, "no-size", false, "do not output the size of the cache directories")
|
||||
}
|
||||
|
||||
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
||||
@@ -85,9 +89,22 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
tab := NewTable()
|
||||
tab.Header = fmt.Sprintf("%-14s %-16s %s", "Repository ID", "Last Used", "Old")
|
||||
tab.RowFormat = "%-14s %-16s %s"
|
||||
tab := table.New()
|
||||
|
||||
type data struct {
|
||||
ID string
|
||||
Last string
|
||||
Old string
|
||||
Size string
|
||||
}
|
||||
|
||||
tab.AddColumn("Repo ID", "{{ .ID }}")
|
||||
tab.AddColumn("Last Used", "{{ .Last }}")
|
||||
tab.AddColumn("Old", "{{ .Old }}")
|
||||
|
||||
if !opts.NoSize {
|
||||
tab.AddColumn("Size", "{{ .Size }}")
|
||||
}
|
||||
|
||||
dirs, err := cache.All(cachedir)
|
||||
if err != nil {
|
||||
@@ -109,14 +126,41 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
||||
old = "yes"
|
||||
}
|
||||
|
||||
tab.Rows = append(tab.Rows, []interface{}{
|
||||
var size string
|
||||
if !opts.NoSize {
|
||||
bytes, err := dirSize(filepath.Join(cachedir, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size = fmt.Sprintf("%11s", formatBytes(uint64(bytes)))
|
||||
}
|
||||
|
||||
tab.AddRow(data{
|
||||
entry.Name()[:10],
|
||||
fmt.Sprintf("%d days ago", uint(time.Since(entry.ModTime()).Hours()/24)),
|
||||
old,
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
tab.Write(gopts.stdout)
|
||||
Printf("%d cache dirs in %s\n", len(dirs), cachedir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dirSize(path string) (int64, error) {
|
||||
var size int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil || info == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return size, err
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
fmt.Println(string(buf))
|
||||
return nil
|
||||
case "index":
|
||||
buf, err := repo.LoadAndDecrypt(gopts.ctx, restic.IndexFile, id)
|
||||
buf, err := repo.LoadAndDecrypt(gopts.ctx, nil, restic.IndexFile, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
return nil
|
||||
case "key":
|
||||
h := restic.Handle{Type: restic.KeyFile, Name: id.String()}
|
||||
buf, err := backend.LoadAll(gopts.ctx, repo.Backend(), h)
|
||||
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func runCat(gopts GlobalOptions, args []string) error {
|
||||
switch tpe {
|
||||
case "pack":
|
||||
h := restic.Handle{Type: restic.DataFile, Name: id.String()}
|
||||
buf, err := backend.LoadAll(gopts.ctx, repo.Backend(), h)
|
||||
buf, err := backend.LoadAll(gopts.ctx, nil, repo.Backend(), h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func init() {
|
||||
|
||||
f := cmdCheck.Flags()
|
||||
f.BoolVar(&checkOptions.ReadData, "read-data", false, "read all data blobs")
|
||||
f.StringVar(&checkOptions.ReadDataSubset, "read-data-subset", "", "read subset of data packs")
|
||||
f.StringVar(&checkOptions.ReadDataSubset, "read-data-subset", "", "read subset n of m data packs (format: `n/m`)")
|
||||
f.BoolVar(&checkOptions.CheckUnused, "check-unused", false, "find unused blobs")
|
||||
f.BoolVar(&checkOptions.WithCache, "with-cache", false, "use the cache")
|
||||
}
|
||||
@@ -67,11 +67,17 @@ func checkFlags(opts CheckOptions) error {
|
||||
if dataSubset[0] == 0 || dataSubset[1] == 0 || dataSubset[0] > dataSubset[1] {
|
||||
return errors.Fatalf("check flag --read-data-subset=n/t values must be positive integers, and n <= t, e.g. --read-data-subset=1/2")
|
||||
}
|
||||
if dataSubset[1] > totalBucketsMax {
|
||||
return errors.Fatalf("check flag --read-data-subset=n/t t must be at most %d", totalBucketsMax)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// See doReadData in runCheck below for why this is 256.
|
||||
const totalBucketsMax = 256
|
||||
|
||||
// stringToIntSlice converts string to []uint, using '/' as element separator
|
||||
func stringToIntSlice(param string) (split []uint, err error) {
|
||||
if param == "" {
|
||||
@@ -123,6 +129,7 @@ func newReadProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress {
|
||||
//
|
||||
// * if --with-cache is specified, the default cache is used
|
||||
// * if the user explicitly requested --no-cache, we don't use any cache
|
||||
// * if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check
|
||||
// * by default, we use a cache in a temporary directory that is deleted after the check
|
||||
func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func()) {
|
||||
cleanup = func() {}
|
||||
@@ -136,8 +143,10 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions) (cleanup func())
|
||||
return cleanup
|
||||
}
|
||||
|
||||
cachedir := gopts.CacheDir
|
||||
|
||||
// use a cache in a temporary directory
|
||||
tempdir, err := ioutil.TempDir("", "restic-check-cache-")
|
||||
tempdir, err := ioutil.TempDir(cachedir, "restic-check-cache-")
|
||||
if err != nil {
|
||||
// if an error occurs, don't use any cache
|
||||
Warnf("unable to create temporary directory for cache during check, disabling cache: %v\n", err)
|
||||
@@ -254,6 +263,8 @@ func runCheck(opts CheckOptions, gopts GlobalOptions, args []string) error {
|
||||
doReadData := func(bucket, totalBuckets uint) {
|
||||
packs := restic.IDSet{}
|
||||
for pack := range chkr.GetPacks() {
|
||||
// If we ever check more than the first byte
|
||||
// of pack, update totalBucketsMax.
|
||||
if (uint(pack[0]) % totalBuckets) == (bucket - 1) {
|
||||
packs.Insert(pack)
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ The "diff" command shows differences from the first to the second snapshot. The
|
||||
first characters in each line display what has happened to a particular file or
|
||||
directory:
|
||||
|
||||
+ The item was added
|
||||
- The item was removed
|
||||
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
|
||||
* + The item was added
|
||||
* - The item was removed
|
||||
* 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
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -47,43 +52,20 @@ func init() {
|
||||
flags.StringArrayVar(&dumpOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` for snapshot ID \"latest\"")
|
||||
}
|
||||
|
||||
func splitPath(path string) []string {
|
||||
d, f := filepath.Split(path)
|
||||
if d == "" || d == "/" {
|
||||
func splitPath(p string) []string {
|
||||
d, f := path.Split(p)
|
||||
if d == "" {
|
||||
return []string{f}
|
||||
}
|
||||
s := splitPath(filepath.Clean(d))
|
||||
if d == "/" {
|
||||
return []string{d}
|
||||
}
|
||||
s := splitPath(path.Clean(d))
|
||||
return append(s, f)
|
||||
}
|
||||
|
||||
func dumpNode(ctx context.Context, repo restic.Repository, node *restic.Node) error {
|
||||
var buf []byte
|
||||
for _, id := range node.Content {
|
||||
size, found := repo.LookupBlobSize(id, restic.DataBlob)
|
||||
if !found {
|
||||
return errors.Errorf("id %v not found in repository", id)
|
||||
}
|
||||
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string, pathToPrint string) error {
|
||||
|
||||
buf = buf[:cap(buf)]
|
||||
if len(buf) < restic.CiphertextLength(int(size)) {
|
||||
buf = restic.NewBlobBuffer(int(size))
|
||||
}
|
||||
|
||||
n, err := repo.LoadBlob(ctx, restic.DataBlob, id, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
_, err = os.Stdout.Write(buf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Write")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repository, prefix string, pathComponents []string) error {
|
||||
if tree == nil {
|
||||
return fmt.Errorf("called with a nil tree")
|
||||
}
|
||||
@@ -96,18 +78,21 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.Repositor
|
||||
}
|
||||
item := filepath.Join(prefix, pathComponents[0])
|
||||
for _, node := range tree.Nodes {
|
||||
if node.Name == pathComponents[0] {
|
||||
if node.Name == pathComponents[0] || pathComponents[0] == "/" {
|
||||
switch {
|
||||
case l == 1 && node.Type == "file":
|
||||
return dumpNode(ctx, repo, node)
|
||||
return getNodeData(ctx, os.Stdout, repo, node)
|
||||
case l > 1 && node.Type == "dir":
|
||||
subtree, err := repo.LoadTree(ctx, *node.Subtree)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
||||
}
|
||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:])
|
||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], pathToPrint)
|
||||
case node.Type == "dir":
|
||||
node.Path = pathToPrint
|
||||
return tarTree(ctx, repo, node, pathToPrint)
|
||||
case l > 1:
|
||||
return fmt.Errorf("%q should be a dir, but s a %q", item, node.Type)
|
||||
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
||||
case node.Type != "file":
|
||||
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
||||
}
|
||||
@@ -128,7 +113,7 @@ func runDump(opts DumpOptions, gopts GlobalOptions, args []string) error {
|
||||
|
||||
debug.Log("dump file %q from %q", pathToPrint, snapshotIDString)
|
||||
|
||||
splittedPath := splitPath(pathToPrint)
|
||||
splittedPath := splitPath(path.Clean(pathToPrint))
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
@@ -172,10 +157,143 @@ 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)
|
||||
err = printFromTree(ctx, tree, repo, "", splittedPath, pathToPrint)
|
||||
if err != nil {
|
||||
Exitf(2, "cannot dump file: %v", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -16,27 +16,38 @@ import (
|
||||
)
|
||||
|
||||
var cmdFind = &cobra.Command{
|
||||
Use: "find [flags] PATTERN",
|
||||
Short: "Find a file or directory",
|
||||
Use: "find [flags] PATTERN...",
|
||||
Short: "Find a file, a directory or restic IDs",
|
||||
Long: `
|
||||
The "find" command searches for files or directories in snapshots stored in the
|
||||
repo. `,
|
||||
repo.
|
||||
It can also be used to search for restic blobs or trees for troubleshooting.`,
|
||||
Example: `restic find config.json
|
||||
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`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runFind(findOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
const shortStr = 8 // Length of short IDs: 4 bytes as hex strings
|
||||
|
||||
// FindOptions bundles all options for the find command.
|
||||
type FindOptions struct {
|
||||
Oldest string
|
||||
Newest string
|
||||
Snapshots []string
|
||||
CaseInsensitive bool
|
||||
ListLong bool
|
||||
Host string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
Oldest string
|
||||
Newest string
|
||||
Snapshots []string
|
||||
BlobID, TreeID bool
|
||||
PackID, ShowPackID bool
|
||||
CaseInsensitive bool
|
||||
ListLong bool
|
||||
Host string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
}
|
||||
|
||||
var findOptions FindOptions
|
||||
@@ -48,6 +59,10 @@ func init() {
|
||||
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
|
||||
f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time")
|
||||
f.StringArrayVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
|
||||
f.BoolVar(&findOptions.BlobID, "blob", false, "pattern is a blob-ID")
|
||||
f.BoolVar(&findOptions.TreeID, "tree", false, "pattern is a tree-ID")
|
||||
f.BoolVar(&findOptions.PackID, "pack", false, "pattern is a pack-ID")
|
||||
f.BoolVar(&findOptions.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)")
|
||||
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")
|
||||
|
||||
@@ -58,7 +73,7 @@ func init() {
|
||||
|
||||
type findPattern struct {
|
||||
oldest, newest time.Time
|
||||
pattern string
|
||||
pattern []string
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
@@ -95,7 +110,7 @@ type statefulOutput struct {
|
||||
hits int
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintJSON(path string, node *restic.Node) {
|
||||
func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
||||
type findNode restic.Node
|
||||
b, err := json.Marshal(struct {
|
||||
// Add these attributes
|
||||
@@ -139,7 +154,7 @@ func (s *statefulOutput) PrintJSON(path string, node *restic.Node) {
|
||||
s.hits++
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintNormal(path string, node *restic.Node) {
|
||||
func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) {
|
||||
if s.newsn != s.oldsn {
|
||||
if s.oldsn != nil {
|
||||
Verbosef("\n")
|
||||
@@ -150,11 +165,62 @@ func (s *statefulOutput) PrintNormal(path string, node *restic.Node) {
|
||||
Printf(formatNode(path, node, s.ListLong) + "\n")
|
||||
}
|
||||
|
||||
func (s *statefulOutput) Print(path string, node *restic.Node) {
|
||||
func (s *statefulOutput) PrintPattern(path string, node *restic.Node) {
|
||||
if s.JSON {
|
||||
s.PrintJSON(path, node)
|
||||
s.PrintPatternJSON(path, node)
|
||||
} else {
|
||||
s.PrintNormal(path, node)
|
||||
s.PrintPatternNormal(path, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
|
||||
b, err := json.Marshal(struct {
|
||||
// Add these attributes
|
||||
ObjectType string `json:"object_type"`
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
ParentTree string `json:"parent_tree,omitempty"`
|
||||
SnapshotID string `json:"snapshot"`
|
||||
Time time.Time `json:"time,omitempty"`
|
||||
}{
|
||||
ObjectType: kind,
|
||||
ID: id,
|
||||
Path: nodepath,
|
||||
SnapshotID: sn.ID().String(),
|
||||
ParentTree: treeID,
|
||||
Time: sn.Time,
|
||||
})
|
||||
if err != nil {
|
||||
Warnf("Marshall failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
if !s.inuse {
|
||||
Printf("[")
|
||||
s.inuse = true
|
||||
}
|
||||
if s.hits > 0 {
|
||||
Printf(",")
|
||||
}
|
||||
Printf(string(b))
|
||||
s.hits++
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
|
||||
Printf("Found %s %s\n", kind, id)
|
||||
if kind == "blob" {
|
||||
Printf(" ... in file %s\n", nodepath)
|
||||
Printf(" (tree %s)\n", treeID)
|
||||
} else {
|
||||
Printf(" ... path %s\n", nodepath)
|
||||
}
|
||||
Printf(" ... in snapshot %s (%s)\n", sn.ID().Str(), sn.Time.Local().Format(TimeFormat))
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintObject(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
|
||||
if s.JSON {
|
||||
s.PrintObjectJSON(kind, id, nodepath, treeID, sn)
|
||||
} else {
|
||||
s.PrintObjectNormal(kind, id, nodepath, treeID, sn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +245,9 @@ type Finder struct {
|
||||
pat findPattern
|
||||
out statefulOutput
|
||||
ignoreTrees restic.IDSet
|
||||
blobIDs map[string]struct{}
|
||||
treeIDs map[string]struct{}
|
||||
itemsFound int
|
||||
}
|
||||
|
||||
func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error {
|
||||
@@ -189,23 +258,35 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
||||
}
|
||||
|
||||
f.out.newsn = sn
|
||||
return walker.Walk(ctx, f.repo, *sn.Tree, f.ignoreTrees, func(nodepath string, node *restic.Node, err error) (bool, error) {
|
||||
return walker.Walk(ctx, f.repo, *sn.Tree, f.ignoreTrees, func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
||||
|
||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
||||
|
||||
return false, walker.SkipNode
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
name := node.Name
|
||||
normalizedNodepath := nodepath
|
||||
if f.pat.ignoreCase {
|
||||
name = strings.ToLower(name)
|
||||
normalizedNodepath = strings.ToLower(nodepath)
|
||||
}
|
||||
|
||||
foundMatch, err := filter.Match(f.pat.pattern, nodepath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
var foundMatch bool
|
||||
|
||||
for _, pat := range f.pat.pattern {
|
||||
found, err := filter.Match(pat, normalizedNodepath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if found {
|
||||
foundMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -213,9 +294,16 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
||||
errIfNoMatch error
|
||||
)
|
||||
if node.Type == "dir" {
|
||||
childMayMatch, err := filter.ChildMatch(f.pat.pattern, nodepath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
var childMayMatch bool
|
||||
for _, pat := range f.pat.pattern {
|
||||
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if mayMatch {
|
||||
childMayMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !childMayMatch {
|
||||
@@ -241,20 +329,171 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
||||
}
|
||||
|
||||
debug.Log(" found match\n")
|
||||
f.out.Print(nodepath, node)
|
||||
f.out.PrintPattern(nodepath, node)
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
||||
debug.Log("searching IDs in snapshot %s", sn.ID())
|
||||
|
||||
if sn.Tree == nil {
|
||||
return errors.Errorf("snapshot %v has no tree", sn.ID().Str())
|
||||
}
|
||||
|
||||
f.out.newsn = sn
|
||||
return walker.Walk(ctx, f.repo, *sn.Tree, f.ignoreTrees, func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
|
||||
if err != nil {
|
||||
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
||||
|
||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s.\n", parentTreeID, sn.ID())
|
||||
|
||||
return false, walker.SkipNode
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if node.Type == "dir" && f.treeIDs != nil {
|
||||
treeID := node.Subtree
|
||||
found := false
|
||||
if _, ok := f.treeIDs[treeID.Str()]; ok {
|
||||
found = true
|
||||
} else if _, ok := f.treeIDs[treeID.String()]; ok {
|
||||
found = true
|
||||
}
|
||||
if found {
|
||||
f.out.PrintObject("tree", treeID.String(), nodepath, "", sn)
|
||||
f.itemsFound++
|
||||
// Terminate if we have found all trees (and we are not
|
||||
// looking for blobs)
|
||||
if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil {
|
||||
// Return an error to terminate the Walk
|
||||
return true, errors.New("OK")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if node.Type == "file" && f.blobIDs != nil {
|
||||
for _, id := range node.Content {
|
||||
idStr := id.String()
|
||||
if _, ok := f.blobIDs[idStr]; !ok {
|
||||
// Look for short ID form
|
||||
if _, ok := f.blobIDs[idStr[:shortStr]]; !ok {
|
||||
continue
|
||||
}
|
||||
// Replace the short ID with the long one
|
||||
f.blobIDs[idStr] = struct{}{}
|
||||
delete(f.blobIDs, idStr[:shortStr])
|
||||
}
|
||||
f.out.PrintObject("blob", idStr, nodepath, parentTreeID.String(), sn)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
// packsToBlobs converts the list of pack IDs to a list of blob IDs that
|
||||
// belong to those packs.
|
||||
func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
||||
packIDs := make(map[string]struct{})
|
||||
for _, p := range packs {
|
||||
packIDs[p] = struct{}{}
|
||||
}
|
||||
if f.blobIDs == nil {
|
||||
f.blobIDs = make(map[string]struct{})
|
||||
}
|
||||
|
||||
allPacksFound := false
|
||||
packsFound := 0
|
||||
|
||||
debug.Log("Looking for packs...")
|
||||
err := f.repo.List(ctx, restic.DataFile, func(id restic.ID, size int64) error {
|
||||
if allPacksFound {
|
||||
return nil
|
||||
}
|
||||
idStr := id.String()
|
||||
if _, ok := packIDs[idStr]; !ok {
|
||||
// Look for short ID form
|
||||
if _, ok := packIDs[idStr[:shortStr]]; !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
debug.Log("Found pack %s", idStr)
|
||||
blobs, _, err := f.repo.ListPack(ctx, id, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, b := range blobs {
|
||||
f.blobIDs[b.ID.String()] = struct{}{}
|
||||
}
|
||||
// Stop searching when all packs have been found
|
||||
packsFound++
|
||||
if packsFound >= len(packIDs) {
|
||||
allPacksFound = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !allPacksFound {
|
||||
return errors.Fatal("unable to find all specified pack(s)")
|
||||
}
|
||||
|
||||
debug.Log("%d blobs found", len(f.blobIDs))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Finder) findObjectPack(ctx context.Context, id string, t restic.BlobType) {
|
||||
idx := f.repo.Index()
|
||||
|
||||
rid, err := restic.ParseID(id)
|
||||
if err != nil {
|
||||
Printf("Note: cannot find pack for object '%s', unable to parse ID: %v\n", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
blobs, found := idx.Lookup(rid, t)
|
||||
if !found {
|
||||
Printf("Object %s not found in the index\n", rid.Str())
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range blobs {
|
||||
if b.ID.Equal(rid) {
|
||||
Printf("Object belongs to pack %s\n ... Pack %s: %s\n", b.PackID, b.PackID.Str(), b.String())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Finder) findObjectsPacks(ctx context.Context) {
|
||||
for i := range f.blobIDs {
|
||||
f.findObjectPack(ctx, i, restic.DataBlob)
|
||||
}
|
||||
|
||||
for i := range f.treeIDs {
|
||||
f.findObjectPack(ctx, i, restic.TreeBlob)
|
||||
}
|
||||
}
|
||||
|
||||
func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
||||
if len(args) != 1 {
|
||||
if len(args) == 0 {
|
||||
return errors.Fatal("wrong number of arguments")
|
||||
}
|
||||
|
||||
var err error
|
||||
pat := findPattern{pattern: args[0]}
|
||||
pat := findPattern{pattern: args}
|
||||
if opts.CaseInsensitive {
|
||||
pat.pattern = strings.ToLower(pat.pattern)
|
||||
for i := range pat.pattern {
|
||||
pat.pattern[i] = strings.ToLower(pat.pattern[i])
|
||||
}
|
||||
pat.ignoreCase = true
|
||||
}
|
||||
|
||||
@@ -270,6 +509,14 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Check at most only one kind of IDs is provided: currently we
|
||||
// can't mix types
|
||||
if (opts.BlobID && opts.TreeID) ||
|
||||
(opts.BlobID && opts.PackID) ||
|
||||
(opts.TreeID && opts.PackID) {
|
||||
return errors.Fatal("cannot have several ID types")
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -296,12 +543,40 @@ func runFind(opts FindOptions, gopts GlobalOptions, args []string) error {
|
||||
out: statefulOutput{ListLong: opts.ListLong, JSON: globalOptions.JSON},
|
||||
ignoreTrees: restic.NewIDSet(),
|
||||
}
|
||||
|
||||
if opts.BlobID {
|
||||
f.blobIDs = make(map[string]struct{})
|
||||
for _, pat := range f.pat.pattern {
|
||||
f.blobIDs[pat] = struct{}{}
|
||||
}
|
||||
}
|
||||
if opts.TreeID {
|
||||
f.treeIDs = make(map[string]struct{})
|
||||
for _, pat := range f.pat.pattern {
|
||||
f.treeIDs[pat] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if opts.PackID {
|
||||
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) {
|
||||
if f.blobIDs != nil || f.treeIDs != nil {
|
||||
if err = f.findIDs(ctx, sn); err != nil && err.Error() != "OK" {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err = f.findInSnapshot(ctx, sn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
f.out.Finish()
|
||||
|
||||
if opts.ShowPackID && (f.blobIDs != nil || f.treeIDs != nil) {
|
||||
f.findObjectsPacks(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"io"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -59,14 +57,15 @@ func init() {
|
||||
f.IntVarP(&forgetOptions.Weekly, "keep-weekly", "w", 0, "keep the last `n` weekly snapshots")
|
||||
f.IntVarP(&forgetOptions.Monthly, "keep-monthly", "m", 0, "keep the last `n` monthly snapshots")
|
||||
f.IntVarP(&forgetOptions.Yearly, "keep-yearly", "y", 0, "keep the last `n` yearly snapshots")
|
||||
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that were created within `duration` before the newest (e.g. 1y5m7d)")
|
||||
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)")
|
||||
// Sadly the commonly used shortcut `H` is already used.
|
||||
f.StringVar(&forgetOptions.Host, "host", "", "only consider snapshots with the given `host`")
|
||||
// Deprecated since 2017-03-07.
|
||||
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname` (deprecated)")
|
||||
f.StringVar(&forgetOptions.Host, "hostname", "", "only consider snapshots with the given `hostname`")
|
||||
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)")
|
||||
|
||||
f.StringArrayVar(&forgetOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path` (can be specified multiple times)")
|
||||
f.BoolVarP(&forgetOptions.Compact, "compact", "c", false, "use compact format")
|
||||
|
||||
@@ -89,153 +88,129 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// group by hostname and dirs
|
||||
type key struct {
|
||||
Hostname string
|
||||
Paths []string
|
||||
Tags []string
|
||||
}
|
||||
snapshotGroups := make(map[string]restic.Snapshots)
|
||||
|
||||
var GroupByTag bool
|
||||
var GroupByHost bool
|
||||
var GroupByPath bool
|
||||
var GroupOptionList []string
|
||||
|
||||
GroupOptionList = strings.Split(opts.GroupBy, ",")
|
||||
|
||||
for _, option := range GroupOptionList {
|
||||
switch option {
|
||||
case "host":
|
||||
GroupByHost = true
|
||||
case "paths":
|
||||
GroupByPath = true
|
||||
case "tags":
|
||||
GroupByTag = true
|
||||
case "":
|
||||
default:
|
||||
return errors.Fatal("unknown grouping option: '" + option + "'")
|
||||
}
|
||||
}
|
||||
|
||||
removeSnapshots := 0
|
||||
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
|
||||
var snapshots restic.Snapshots
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
if len(args) > 0 {
|
||||
// When explicit snapshots args are given, remove them immediately.
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Verbosef("removed snapshot %v\n", sn.ID().Str())
|
||||
if !gopts.JSON {
|
||||
Verbosef("removed snapshot %v\n", sn.ID().Str())
|
||||
}
|
||||
removeSnapshots++
|
||||
} else {
|
||||
Verbosef("would have removed snapshot %v\n", sn.ID().Str())
|
||||
if !gopts.JSON {
|
||||
Verbosef("would have removed snapshot %v\n", sn.ID().Str())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Determining grouping-keys
|
||||
var tags []string
|
||||
var hostname string
|
||||
var paths []string
|
||||
|
||||
if GroupByTag {
|
||||
tags = sn.Tags
|
||||
sort.StringSlice(tags).Sort()
|
||||
}
|
||||
if GroupByHost {
|
||||
hostname = sn.Hostname
|
||||
}
|
||||
if GroupByPath {
|
||||
paths = sn.Paths
|
||||
}
|
||||
|
||||
sort.StringSlice(sn.Paths).Sort()
|
||||
var k []byte
|
||||
var err error
|
||||
|
||||
k, err = json.Marshal(key{Tags: tags, Hostname: hostname, Paths: paths})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshotGroups[string(k)] = append(snapshotGroups[string(k)], sn)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policy := restic.ExpirePolicy{
|
||||
Last: opts.Last,
|
||||
Hourly: opts.Hourly,
|
||||
Daily: opts.Daily,
|
||||
Weekly: opts.Weekly,
|
||||
Monthly: opts.Monthly,
|
||||
Yearly: opts.Yearly,
|
||||
Within: opts.Within,
|
||||
Tags: opts.KeepTags,
|
||||
}
|
||||
policy := restic.ExpirePolicy{
|
||||
Last: opts.Last,
|
||||
Hourly: opts.Hourly,
|
||||
Daily: opts.Daily,
|
||||
Weekly: opts.Weekly,
|
||||
Monthly: opts.Monthly,
|
||||
Yearly: opts.Yearly,
|
||||
Within: opts.Within,
|
||||
Tags: opts.KeepTags,
|
||||
}
|
||||
|
||||
if policy.Empty() && len(args) == 0 {
|
||||
Verbosef("no policy was specified, no snapshots will be removed\n")
|
||||
}
|
||||
if policy.Empty() && len(args) == 0 {
|
||||
if !gopts.JSON {
|
||||
Verbosef("no policy was specified, no snapshots will be removed\n")
|
||||
}
|
||||
}
|
||||
|
||||
if !policy.Empty() {
|
||||
Verbosef("Applying Policy: %v\n", policy)
|
||||
|
||||
for k, snapshotGroup := range snapshotGroups {
|
||||
var key key
|
||||
if json.Unmarshal([]byte(k), &key) != nil {
|
||||
return err
|
||||
if !policy.Empty() {
|
||||
if !gopts.JSON {
|
||||
Verbosef("Applying Policy: %v\n", policy)
|
||||
}
|
||||
|
||||
// Info
|
||||
Verbosef("snapshots")
|
||||
var infoStrings []string
|
||||
if GroupByTag {
|
||||
infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]")
|
||||
}
|
||||
if GroupByHost {
|
||||
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
|
||||
}
|
||||
if GroupByPath {
|
||||
infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
|
||||
}
|
||||
if infoStrings != nil {
|
||||
Verbosef(" for (" + strings.Join(infoStrings, ", ") + ")")
|
||||
}
|
||||
Verbosef(":\n\n")
|
||||
var jsonGroups []*ForgetGroup
|
||||
|
||||
keep, remove := restic.ApplyPolicy(snapshotGroup, policy)
|
||||
|
||||
if len(keep) != 0 && !gopts.Quiet {
|
||||
Printf("keep %d snapshots:\n", len(keep))
|
||||
PrintSnapshots(globalOptions.stdout, keep, opts.Compact)
|
||||
Printf("\n")
|
||||
}
|
||||
|
||||
if len(remove) != 0 && !gopts.Quiet {
|
||||
Printf("remove %d snapshots:\n", len(remove))
|
||||
PrintSnapshots(globalOptions.stdout, remove, opts.Compact)
|
||||
Printf("\n")
|
||||
}
|
||||
|
||||
removeSnapshots += len(remove)
|
||||
|
||||
if !opts.DryRun {
|
||||
for _, sn := range remove {
|
||||
h := restic.Handle{Type: restic.SnapshotFile, Name: sn.ID().String()}
|
||||
err = repo.Backend().Remove(gopts.ctx, h)
|
||||
for k, snapshotGroup := range snapshotGroups {
|
||||
if gopts.Verbose >= 1 && !gopts.JSON {
|
||||
err = PrintSnapshotGroupHeader(gopts.stdout, k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var key restic.SnapshotGroupKey
|
||||
if json.Unmarshal([]byte(k), &key) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fg ForgetGroup
|
||||
fg.Tags = key.Tags
|
||||
fg.Host = key.Hostname
|
||||
fg.Paths = key.Paths
|
||||
|
||||
keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)
|
||||
|
||||
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
|
||||
Printf("keep %d snapshots:\n", len(keep))
|
||||
PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact)
|
||||
Printf("\n")
|
||||
}
|
||||
addJSONSnapshots(&fg.Keep, keep)
|
||||
|
||||
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
|
||||
Printf("remove %d snapshots:\n", len(remove))
|
||||
PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact)
|
||||
Printf("\n")
|
||||
}
|
||||
addJSONSnapshots(&fg.Remove, remove)
|
||||
|
||||
fg.Reasons = reasons
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if removeSnapshots > 0 && opts.Prune {
|
||||
Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
|
||||
if !gopts.JSON {
|
||||
Verbosef("%d snapshots have been removed, running prune\n", removeSnapshots)
|
||||
}
|
||||
if !opts.DryRun {
|
||||
return pruneRepository(gopts, repo)
|
||||
}
|
||||
@@ -243,3 +218,28 @@ func runForget(opts ForgetOptions, gopts GlobalOptions, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForgetGroup helps to print what is forgotten in JSON.
|
||||
type ForgetGroup struct {
|
||||
Tags []string `json:"tags"`
|
||||
Host string `json:"host"`
|
||||
Paths []string `json:"paths"`
|
||||
Keep []Snapshot `json:"keep"`
|
||||
Remove []Snapshot `json:"remove"`
|
||||
Reasons []restic.KeepReason `json:"reasons"`
|
||||
}
|
||||
|
||||
func addJSONSnapshots(js *[]Snapshot, list restic.Snapshots) {
|
||||
for _, sn := range list {
|
||||
k := Snapshot{
|
||||
Snapshot: sn,
|
||||
ID: sn.ID(),
|
||||
ShortID: sn.ID().Str(),
|
||||
}
|
||||
*js = append(*js, k)
|
||||
}
|
||||
}
|
||||
|
||||
func printJSONForget(stdout io.Writer, forgets []*ForgetGroup) error {
|
||||
return json.NewEncoder(stdout).Encode(forgets)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ var cmdGenerate = &cobra.Command{
|
||||
Use: "generate [command]",
|
||||
Short: "Generate manual pages and auto-completion files (bash, zsh)",
|
||||
Long: `
|
||||
The "generate" command writes automatically generated files like the man pages
|
||||
The "generate" command writes automatically generated files (like the man pages
|
||||
and the auto-completion files for bash and zsh).
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
|
||||
@@ -2,7 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -35,10 +36,16 @@ func init() {
|
||||
flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "the file from which to load a new password")
|
||||
}
|
||||
|
||||
func listKeys(ctx context.Context, s *repository.Repository) error {
|
||||
tab := NewTable()
|
||||
tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created")
|
||||
tab.RowFormat = "%s%-10s %-10s %-10s %s"
|
||||
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error {
|
||||
type keyInfo struct {
|
||||
Current bool `json:"current"`
|
||||
ID string `json:"id"`
|
||||
UserName string `json:"userName"`
|
||||
HostName string `json:"hostName"`
|
||||
Created string `json:"created"`
|
||||
}
|
||||
|
||||
var keys []keyInfo
|
||||
|
||||
err := s.List(ctx, restic.KeyFile, func(id restic.ID, size int64) error {
|
||||
k, err := repository.LoadKey(ctx, s, id.String())
|
||||
@@ -47,20 +54,36 @@ func listKeys(ctx context.Context, s *repository.Repository) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var current string
|
||||
if id.String() == s.KeyName() {
|
||||
current = "*"
|
||||
} else {
|
||||
current = " "
|
||||
key := keyInfo{
|
||||
Current: id.String() == s.KeyName(),
|
||||
ID: id.Str(),
|
||||
UserName: k.Username,
|
||||
HostName: k.Hostname,
|
||||
Created: k.Created.Local().Format(TimeFormat),
|
||||
}
|
||||
tab.Rows = append(tab.Rows, []interface{}{current, id.Str(),
|
||||
k.Username, k.Hostname, k.Created.Format(TimeFormat)})
|
||||
|
||||
keys = append(keys, key)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gopts.JSON {
|
||||
return json.NewEncoder(globalOptions.stdout).Encode(keys)
|
||||
}
|
||||
|
||||
tab := table.New()
|
||||
tab.AddColumn(" ID", "{{if .Current}}*{{else}} {{end}}{{ .ID }}")
|
||||
tab.AddColumn("User", "{{ .UserName }}")
|
||||
tab.AddColumn("Host", "{{ .HostName }}")
|
||||
tab.AddColumn("Created", "{{ .Created }}")
|
||||
|
||||
for _, key := range keys {
|
||||
tab.AddRow(key)
|
||||
}
|
||||
|
||||
return tab.Write(globalOptions.stdout)
|
||||
}
|
||||
|
||||
@@ -160,7 +183,7 @@ func runKey(gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return listKeys(ctx, repo)
|
||||
return listKeys(ctx, repo, gopts)
|
||||
case "add":
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
|
||||
@@ -2,21 +2,37 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
)
|
||||
|
||||
var cmdLs = &cobra.Command{
|
||||
Use: "ls [flags] [snapshot-ID ...]",
|
||||
Use: "ls [flags] [snapshotID] [dir...]",
|
||||
Short: "List files in a snapshot",
|
||||
Long: `
|
||||
The "ls" command allows listing files and directories in a snapshot.
|
||||
The "ls" command lists files and directories in a snapshot.
|
||||
|
||||
The special snapshot-ID "latest" can be used to list files and directories of the latest snapshot in the repository.
|
||||
The special snapshot ID "latest" can be used to list files and
|
||||
directories of the latest snapshot in the repository. The
|
||||
--host flag can be used in conjunction to select the latest
|
||||
snapshot originating from a certain host only.
|
||||
|
||||
File listings can optionally be filtered by directories. Any
|
||||
positional arguments after the snapshot ID are interpreted as
|
||||
absolute directory paths, and only files inside those directories
|
||||
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.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -26,10 +42,11 @@ The special snapshot-ID "latest" can be used to list files and directories of th
|
||||
|
||||
// LsOptions collects all options for the ls command.
|
||||
type LsOptions struct {
|
||||
ListLong bool
|
||||
Host string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
ListLong bool
|
||||
Host string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
var lsOptions LsOptions
|
||||
@@ -39,10 +56,31 @@ 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.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")
|
||||
}
|
||||
|
||||
type lsSnapshot struct {
|
||||
*restic.Snapshot
|
||||
ID *restic.ID `json:"id"`
|
||||
ShortID string `json:"short_id"`
|
||||
StructType string `json:"struct_type"` // "snapshot"
|
||||
}
|
||||
|
||||
type lsNode struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path"`
|
||||
UID uint32 `json:"uid"`
|
||||
GID uint32 `json:"gid"`
|
||||
Size uint64 `json:"size,omitempty"`
|
||||
Mode os.FileMode `json:"mode,omitempty"`
|
||||
ModTime time.Time `json:"mtime,omitempty"`
|
||||
AccessTime time.Time `json:"atime,omitempty"`
|
||||
ChangeTime time.Time `json:"ctime,omitempty"`
|
||||
StructType string `json:"struct_type"` // "node"
|
||||
}
|
||||
|
||||
func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
@@ -50,6 +88,51 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
return errors.Fatal("Invalid arguments, either give one or more snapshot IDs or set filters.")
|
||||
}
|
||||
|
||||
// extract any specific directories to walk
|
||||
var dirs []string
|
||||
if len(args) > 1 {
|
||||
dirs = args[1:]
|
||||
for _, dir := range dirs {
|
||||
if !strings.HasPrefix(dir, "/") {
|
||||
return errors.Fatal("All path filters must be absolute, starting with a forward slash '/'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withinDir := func(nodepath string) bool {
|
||||
if len(dirs) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
// we're within one of the selected dirs, example:
|
||||
// nodepath: "/test/foo"
|
||||
// dir: "/test"
|
||||
if fs.HasPathPrefix(dir, nodepath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
approachingMatchingTree := func(nodepath string) bool {
|
||||
if len(dirs) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
// the current node path is a prefix for one of the
|
||||
// directories, so we're interested in something deeper in the
|
||||
// tree. Example:
|
||||
// nodepath: "/test"
|
||||
// dir: "/test/foo"
|
||||
if fs.HasPathPrefix(nodepath, dir) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -61,23 +144,88 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time)
|
||||
|
||||
err := walker.Walk(ctx, repo, *sn.Tree, nil, func(nodepath string, node *restic.Node, err error) (bool, error) {
|
||||
var (
|
||||
printSnapshot func(sn *restic.Snapshot)
|
||||
printNode func(path string, node *restic.Node)
|
||||
)
|
||||
|
||||
if gopts.JSON {
|
||||
enc := json.NewEncoder(gopts.stdout)
|
||||
|
||||
printSnapshot = func(sn *restic.Snapshot) {
|
||||
enc.Encode(lsSnapshot{
|
||||
Snapshot: sn,
|
||||
ID: sn.ID(),
|
||||
ShortID: sn.ID().Str(),
|
||||
StructType: "snapshot",
|
||||
})
|
||||
}
|
||||
|
||||
printNode = func(path string, node *restic.Node) {
|
||||
enc.Encode(lsNode{
|
||||
Name: node.Name,
|
||||
Type: node.Type,
|
||||
Path: path,
|
||||
UID: node.UID,
|
||||
GID: node.GID,
|
||||
Size: node.Size,
|
||||
Mode: node.Mode,
|
||||
ModTime: node.ModTime,
|
||||
AccessTime: node.AccessTime,
|
||||
ChangeTime: node.ChangeTime,
|
||||
StructType: "node",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
printSnapshot = func(sn *restic.Snapshot) {
|
||||
Verbosef("snapshot %s of %v filtered by %v at %s):\n", sn.ID().Str(), sn.Paths, dirs, sn.Time)
|
||||
}
|
||||
printNode = func(path string, node *restic.Node) {
|
||||
Printf("%s\n", formatNode(path, node, lsOptions.ListLong))
|
||||
}
|
||||
}
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, 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) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
return false, nil
|
||||
}
|
||||
Printf("%s\n", formatNode(nodepath, node, lsOptions.ListLong))
|
||||
|
||||
if withinDir(nodepath) {
|
||||
// if we're within a dir, print the node
|
||||
printNode(nodepath, node)
|
||||
|
||||
// if recursive listing is requested, signal the walker that it
|
||||
// should continue walking recursively
|
||||
if opts.Recursive {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// if there's an upcoming match deeper in the tree (but we're not
|
||||
// there yet), signal the walker to descend into any subdirs
|
||||
if approachingMatchingTree(nodepath) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// otherwise, signal the walker to not walk recursively into any
|
||||
// subdirs
|
||||
if node.Type == "dir" {
|
||||
return false, walker.SkipNode
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// +build !netbsd
|
||||
// +build !openbsd
|
||||
// +build !solaris
|
||||
// +build !windows
|
||||
@@ -52,13 +53,14 @@ 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
|
||||
Host string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
SnapshotTemplate string
|
||||
OwnerRoot bool
|
||||
AllowRoot bool
|
||||
AllowOther bool
|
||||
NoDefaultPermissions bool
|
||||
Host string
|
||||
Tags restic.TagLists
|
||||
Paths []string
|
||||
SnapshotTemplate string
|
||||
}
|
||||
|
||||
var mountOptions MountOptions
|
||||
@@ -70,6 +72,7 @@ func init() {
|
||||
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.Var(&mountOptions.Tags, "tag", "only consider snapshots which include this `taglist`")
|
||||
@@ -117,6 +120,11 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
|
||||
if opts.AllowOther {
|
||||
mountOptions = append(mountOptions, systemFuse.AllowOther())
|
||||
|
||||
// let the kernel check permissions unless it is explicitly disabled
|
||||
if !opts.NoDefaultPermissions {
|
||||
mountOptions = append(mountOptions, systemFuse.DefaultPermissions())
|
||||
}
|
||||
}
|
||||
|
||||
c, err := systemFuse.Mount(mountpoint, mountOptions...)
|
||||
@@ -141,7 +149,7 @@ func mount(opts MountOptions, gopts GlobalOptions, mountpoint string) error {
|
||||
}
|
||||
|
||||
Printf("Now serving the repository at %s\n", mountpoint)
|
||||
Printf("Don't forget to umount after quitting!\n")
|
||||
Printf("When finished, quit with Ctrl-c or umount the mountpoint.\n")
|
||||
|
||||
debug.Log("serving mount at %v", mountpoint)
|
||||
err = fs.Serve(c, root)
|
||||
|
||||
@@ -149,8 +149,8 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
len(idx.Packs), blobs, formatBytes(uint64(stats.bytes)))
|
||||
|
||||
blobCount := make(map[restic.BlobHandle]int)
|
||||
duplicateBlobs := 0
|
||||
duplicateBytes := 0
|
||||
var duplicateBlobs uint64
|
||||
var duplicateBytes uint64
|
||||
|
||||
// find duplicate blobs
|
||||
for _, p := range idx.Packs {
|
||||
@@ -161,7 +161,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
|
||||
if blobCount[h] > 1 {
|
||||
duplicateBlobs++
|
||||
duplicateBytes += int(entry.Length)
|
||||
duplicateBytes += uint64(entry.Length)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@ func pruneRepository(gopts GlobalOptions, repo restic.Repository) error {
|
||||
continue
|
||||
}
|
||||
|
||||
removeBytes += int(blob.Length)
|
||||
removeBytes += uint64(blob.Length)
|
||||
}
|
||||
|
||||
if hasActiveBlob {
|
||||
|
||||
148
cmd/restic/cmd_recover.go
Normal file
148
cmd/restic/cmd_recover.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdRecover = &cobra.Command{
|
||||
Use: "recover [flags]",
|
||||
Short: "Recover data from the repository",
|
||||
Long: `
|
||||
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".
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRecover(globalOptions)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdRecover)
|
||||
}
|
||||
|
||||
func runRecover(gopts GlobalOptions) error {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Verbosef("load index files\n")
|
||||
if err = repo.LoadIndex(gopts.ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// trees maps a tree ID to whether or not it is referenced by a different
|
||||
// tree. If it is not referenced, we have a root tree.
|
||||
trees := make(map[restic.ID]bool)
|
||||
|
||||
for blob := range repo.Index().Each(gopts.ctx) {
|
||||
if blob.Blob.Type != restic.TreeBlob {
|
||||
continue
|
||||
}
|
||||
trees[blob.Blob.ID] = false
|
||||
}
|
||||
|
||||
cur := 0
|
||||
max := len(trees)
|
||||
Verbosef("load %d trees\n\n", len(trees))
|
||||
|
||||
for id := range trees {
|
||||
cur++
|
||||
Verbosef("\rtree (%v/%v)", cur, max)
|
||||
|
||||
if !trees[id] {
|
||||
trees[id] = false
|
||||
}
|
||||
|
||||
tree, err := repo.LoadTree(gopts.ctx, id)
|
||||
if err != nil {
|
||||
Warnf("unable to load tree %v: %v\n", id.Str(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, node := range tree.Nodes {
|
||||
if node.Type != "dir" || node.Subtree == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
subtree := *node.Subtree
|
||||
trees[subtree] = true
|
||||
}
|
||||
}
|
||||
Verbosef("\ndone\n")
|
||||
|
||||
roots := restic.NewIDSet()
|
||||
for id, seen := range trees {
|
||||
if seen {
|
||||
continue
|
||||
}
|
||||
|
||||
roots.Insert(id)
|
||||
}
|
||||
|
||||
Verbosef("found %d roots\n", len(roots))
|
||||
|
||||
tree := restic.NewTree()
|
||||
for id := range roots {
|
||||
var subtreeID = id
|
||||
node := restic.Node{
|
||||
Type: "dir",
|
||||
Name: id.Str(),
|
||||
Mode: 0755,
|
||||
Subtree: &subtreeID,
|
||||
AccessTime: time.Now(),
|
||||
ModTime: time.Now(),
|
||||
ChangeTime: time.Now(),
|
||||
}
|
||||
tree.Insert(&node)
|
||||
}
|
||||
|
||||
treeID, err := repo.SaveTree(gopts.ctx, tree)
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to save new tree to the repo: %v", err)
|
||||
}
|
||||
|
||||
err = repo.Flush(gopts.ctx)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
sn.Tree = &treeID
|
||||
|
||||
id, err := repo.SaveJSONUnpacked(gopts.ctx, restic.SnapshotFile, sn)
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||
}
|
||||
|
||||
Printf("saved new snapshot %v\n", id.Str())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -27,12 +29,15 @@ repository.
|
||||
|
||||
// RestoreOptions collects all options for the restore command.
|
||||
type RestoreOptions struct {
|
||||
Exclude []string
|
||||
Include []string
|
||||
Target string
|
||||
Host string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
Exclude []string
|
||||
InsensitiveExclude []string
|
||||
Include []string
|
||||
InsensitiveInclude []string
|
||||
Target string
|
||||
Host string
|
||||
Paths []string
|
||||
Tags restic.TagLists
|
||||
Verify bool
|
||||
}
|
||||
|
||||
var restoreOptions RestoreOptions
|
||||
@@ -42,16 +47,29 @@ func init() {
|
||||
|
||||
flags := cmdRestore.Flags()
|
||||
flags.StringArrayVarP(&restoreOptions.Exclude, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
|
||||
flags.StringArrayVar(&restoreOptions.InsensitiveExclude, "iexclude", nil, "same as `--exclude` but ignores the casing of filenames")
|
||||
flags.StringArrayVarP(&restoreOptions.Include, "include", "i", nil, "include a `pattern`, exclude everything else (can be specified multiple times)")
|
||||
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.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")
|
||||
}
|
||||
|
||||
func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||
ctx := gopts.ctx
|
||||
hasExcludes := len(opts.Exclude) > 0 || len(opts.InsensitiveExclude) > 0
|
||||
hasIncludes := len(opts.Include) > 0 || len(opts.InsensitiveInclude) > 0
|
||||
|
||||
for i, str := range opts.InsensitiveExclude {
|
||||
opts.InsensitiveExclude[i] = strings.ToLower(str)
|
||||
}
|
||||
|
||||
for i, str := range opts.InsensitiveInclude {
|
||||
opts.InsensitiveInclude[i] = strings.ToLower(str)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(args) == 0:
|
||||
@@ -64,7 +82,7 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||
return errors.Fatal("please specify a directory to restore to (--target)")
|
||||
}
|
||||
|
||||
if len(opts.Exclude) > 0 && len(opts.Include) > 0 {
|
||||
if hasExcludes && hasIncludes {
|
||||
return errors.Fatal("exclude and include patterns are mutually exclusive")
|
||||
}
|
||||
|
||||
@@ -104,14 +122,14 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
res, err := restic.NewRestorer(repo, id)
|
||||
res, err := restorer.NewRestorer(repo, id)
|
||||
if err != nil {
|
||||
Exitf(2, "creating restorer failed: %v\n", err)
|
||||
}
|
||||
|
||||
totalErrors := 0
|
||||
res.Error = func(dir string, node *restic.Node, err error) error {
|
||||
Warnf("ignoring error for %s: %s\n", dir, err)
|
||||
res.Error = func(location string, err error) error {
|
||||
Warnf("ignoring error for %s: %s\n", location, err)
|
||||
totalErrors++
|
||||
return nil
|
||||
}
|
||||
@@ -122,11 +140,16 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||
Warnf("error for exclude pattern: %v", err)
|
||||
}
|
||||
|
||||
matchedInsensitive, _, err := filter.List(opts.InsensitiveExclude, strings.ToLower(item))
|
||||
if err != nil {
|
||||
Warnf("error for iexclude pattern: %v", err)
|
||||
}
|
||||
|
||||
// An exclude filter is basically a 'wildcard but foo',
|
||||
// so even if a childMayMatch, other children of a dir may not,
|
||||
// therefore childMayMatch does not matter, but we should not go down
|
||||
// unless the dir is selected for restore
|
||||
selectedForRestore = !matched
|
||||
selectedForRestore = !matched && !matchedInsensitive
|
||||
childMayBeSelected = selectedForRestore && node.Type == "dir"
|
||||
|
||||
return selectedForRestore, childMayBeSelected
|
||||
@@ -138,21 +161,32 @@ func runRestore(opts RestoreOptions, gopts GlobalOptions, args []string) error {
|
||||
Warnf("error for include pattern: %v", err)
|
||||
}
|
||||
|
||||
selectedForRestore = matched
|
||||
childMayBeSelected = childMayMatch && node.Type == "dir"
|
||||
matchedInsensitive, childMayMatchInsensitive, err := filter.List(opts.InsensitiveInclude, strings.ToLower(item))
|
||||
if err != nil {
|
||||
Warnf("error for iexclude pattern: %v", err)
|
||||
}
|
||||
|
||||
selectedForRestore = matched || matchedInsensitive
|
||||
childMayBeSelected = (childMayMatch || childMayMatchInsensitive) && node.Type == "dir"
|
||||
|
||||
return selectedForRestore, childMayBeSelected
|
||||
}
|
||||
|
||||
if len(opts.Exclude) > 0 {
|
||||
if hasExcludes {
|
||||
res.SelectFilter = selectExcludeFilter
|
||||
} else if len(opts.Include) > 0 {
|
||||
} else if hasIncludes {
|
||||
res.SelectFilter = selectIncludeFilter
|
||||
}
|
||||
|
||||
Verbosef("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
||||
|
||||
err = res.RestoreTo(ctx, opts.Target)
|
||||
if err == nil && opts.Verify {
|
||||
Verbosef("verifying files in %s\n", opts.Target)
|
||||
var count int
|
||||
count, err = res.VerifyFiles(ctx, opts.Target)
|
||||
Verbosef("finished verifying %d files in %s\n", count, opts.Target)
|
||||
}
|
||||
if totalErrors > 0 {
|
||||
Printf("There were %d errors\n", totalErrors)
|
||||
}
|
||||
|
||||
73
cmd/restic/cmd_self_update.go
Normal file
73
cmd/restic/cmd_self_update.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// xbuild selfupdate
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/selfupdate"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdSelfUpdate = &cobra.Command{
|
||||
Use: "self-update [flags]",
|
||||
Short: "Update the restic binary",
|
||||
Long: `
|
||||
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.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runSelfUpdate(selfUpdateOptions, globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
// SelfUpdateOptions collects all options for the update-restic command.
|
||||
type SelfUpdateOptions struct {
|
||||
Output string
|
||||
}
|
||||
|
||||
var selfUpdateOptions SelfUpdateOptions
|
||||
|
||||
func init() {
|
||||
cmdRoot.AddCommand(cmdSelfUpdate)
|
||||
|
||||
flags := cmdSelfUpdate.Flags()
|
||||
flags.StringVar(&selfUpdateOptions.Output, "output", "", "Save the downloaded file as `filename` (default: running binary itself)")
|
||||
}
|
||||
|
||||
func runSelfUpdate(opts SelfUpdateOptions, gopts GlobalOptions, args []string) error {
|
||||
if opts.Output == "" {
|
||||
file, err := os.Executable()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to find executable")
|
||||
}
|
||||
|
||||
opts.Output = file
|
||||
}
|
||||
|
||||
fi, err := os.Lstat(opts.Output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fi.Mode().IsRegular() {
|
||||
return errors.Errorf("output file %v is not a normal file, use --output to specify a different file", opts.Output)
|
||||
}
|
||||
|
||||
Printf("writing restic to %v\n", opts.Output)
|
||||
|
||||
v, err := selfupdate.DownloadLatestStableRelease(gopts.ctx, opts.Output, version, Verbosef)
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to update restic: %v", err)
|
||||
}
|
||||
|
||||
if v != version {
|
||||
Printf("successfully updated restic to version %v\n", v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,7 @@ type SnapshotOptions struct {
|
||||
Paths []string
|
||||
Compact bool
|
||||
Last bool
|
||||
GroupBy string
|
||||
}
|
||||
|
||||
var snapshotOptions SnapshotOptions
|
||||
@@ -44,6 +46,7 @@ func init() {
|
||||
f.StringArrayVar(&snapshotOptions.Paths, "path", nil, "only consider snapshots for this `path` (can be specified multiple times)")
|
||||
f.BoolVarP(&snapshotOptions.Compact, "compact", "c", false, "use compact format")
|
||||
f.BoolVar(&snapshotOptions.Last, "last", false, "only show the last snapshot for each host and path")
|
||||
f.StringVarP(&snapshotOptions.GroupBy, "group-by", "g", "", "string for grouping snapshots by host,paths,tags")
|
||||
}
|
||||
|
||||
func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) error {
|
||||
@@ -63,25 +66,41 @@ func runSnapshots(opts SnapshotOptions, gopts GlobalOptions, args []string) erro
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
|
||||
var list restic.Snapshots
|
||||
var snapshots restic.Snapshots
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
|
||||
list = append(list, sn)
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
snapshotGroups, grouped, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Last {
|
||||
list = FilterLastSnapshots(list)
|
||||
for k, list := range snapshotGroups {
|
||||
if opts.Last {
|
||||
list = FilterLastSnapshots(list)
|
||||
}
|
||||
sort.Sort(sort.Reverse(list))
|
||||
snapshotGroups[k] = list
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(list))
|
||||
|
||||
if gopts.JSON {
|
||||
err := printSnapshotsJSON(gopts.stdout, list)
|
||||
err := printSnapshotGroupJSON(gopts.stdout, snapshotGroups, grouped)
|
||||
if err != nil {
|
||||
Warnf("error printing snapshot: %v\n", err)
|
||||
Warnf("error printing snapshots: %v\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
PrintSnapshots(gopts.stdout, list, opts.Compact)
|
||||
|
||||
for k, list := range snapshotGroups {
|
||||
if grouped {
|
||||
err := PrintSnapshotGroupHeader(gopts.stdout, k)
|
||||
if err != nil {
|
||||
Warnf("error printing snapshots: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
PrintSnapshots(gopts.stdout, list, nil, opts.Compact)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -123,7 +142,16 @@ func FilterLastSnapshots(list restic.Snapshots) restic.Snapshots {
|
||||
}
|
||||
|
||||
// PrintSnapshots prints a text table of the snapshots in list to stdout.
|
||||
func PrintSnapshots(stdout io.Writer, list restic.Snapshots, compact bool) {
|
||||
func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.KeepReason, compact bool) {
|
||||
// keep the reasons a snasphot is being kept in a map, so that it doesn't
|
||||
// get lost when the list of snapshots is sorted
|
||||
keepReasons := make(map[restic.ID]restic.KeepReason, len(reasons))
|
||||
if len(reasons) > 0 {
|
||||
for i, sn := range list {
|
||||
id := sn.ID()
|
||||
keepReasons[*id] = reasons[i]
|
||||
}
|
||||
}
|
||||
|
||||
// always sort the snapshots so that the newer ones are listed last
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
@@ -143,75 +171,112 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, compact bool) {
|
||||
}
|
||||
}
|
||||
|
||||
tab := NewTable()
|
||||
if !compact {
|
||||
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s %-3s %s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags", "", "Directory")
|
||||
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%%ds %%-3s %%s", -maxHost, -maxTag)
|
||||
tab := table.New()
|
||||
|
||||
if compact {
|
||||
tab.AddColumn("ID", "{{ .ID }}")
|
||||
tab.AddColumn("Time", "{{ .Timestamp }}")
|
||||
tab.AddColumn("Host", "{{ .Hostname }}")
|
||||
tab.AddColumn("Tags ", `{{ join .Tags "\n" }}`)
|
||||
} else {
|
||||
tab.Header = fmt.Sprintf("%-8s %-19s %-*s %-*s", "ID", "Date", -maxHost, "Host", -maxTag, "Tags")
|
||||
tab.RowFormat = fmt.Sprintf("%%-8s %%-19s %%%ds %%s", -maxHost)
|
||||
tab.AddColumn("ID", "{{ .ID }}")
|
||||
tab.AddColumn("Time", "{{ .Timestamp }}")
|
||||
tab.AddColumn("Host ", "{{ .Hostname }}")
|
||||
tab.AddColumn("Tags ", `{{ join .Tags "," }}`)
|
||||
if len(reasons) > 0 {
|
||||
tab.AddColumn("Reasons", `{{ join .Reasons "\n" }}`)
|
||||
}
|
||||
tab.AddColumn("Paths", `{{ join .Paths "\n" }}`)
|
||||
}
|
||||
|
||||
type snapshot struct {
|
||||
ID string
|
||||
Timestamp string
|
||||
Hostname string
|
||||
Tags []string
|
||||
Reasons []string
|
||||
Paths []string
|
||||
}
|
||||
|
||||
var multiline bool
|
||||
for _, sn := range list {
|
||||
if len(sn.Paths) == 0 {
|
||||
continue
|
||||
data := snapshot{
|
||||
ID: sn.ID().Str(),
|
||||
Timestamp: sn.Time.Local().Format(TimeFormat),
|
||||
Hostname: sn.Hostname,
|
||||
Tags: sn.Tags,
|
||||
Paths: sn.Paths,
|
||||
}
|
||||
|
||||
firstTag := ""
|
||||
if len(sn.Tags) > 0 {
|
||||
firstTag = sn.Tags[0]
|
||||
if len(reasons) > 0 {
|
||||
id := sn.ID()
|
||||
data.Reasons = keepReasons[*id].Matches
|
||||
}
|
||||
|
||||
rows := len(sn.Paths)
|
||||
if rows < len(sn.Tags) {
|
||||
rows = len(sn.Tags)
|
||||
if len(sn.Paths) > 1 && !compact {
|
||||
multiline = true
|
||||
}
|
||||
|
||||
treeElement := " "
|
||||
if rows != 1 {
|
||||
treeElement = "┌──"
|
||||
}
|
||||
|
||||
if !compact {
|
||||
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, firstTag, treeElement, sn.Paths[0]})
|
||||
} else {
|
||||
allTags := ""
|
||||
for _, tag := range sn.Tags {
|
||||
allTags += tag + " "
|
||||
}
|
||||
tab.Rows = append(tab.Rows, []interface{}{sn.ID().Str(), sn.Time.Format(TimeFormat), sn.Hostname, allTags})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(sn.Tags) > rows {
|
||||
rows = len(sn.Tags)
|
||||
}
|
||||
|
||||
for i := 1; i < rows; i++ {
|
||||
path := ""
|
||||
if len(sn.Paths) > i {
|
||||
path = sn.Paths[i]
|
||||
}
|
||||
|
||||
tag := ""
|
||||
if len(sn.Tags) > i {
|
||||
tag = sn.Tags[i]
|
||||
}
|
||||
|
||||
treeElement := "│"
|
||||
if i == (rows - 1) {
|
||||
treeElement = "└──"
|
||||
}
|
||||
|
||||
tab.Rows = append(tab.Rows, []interface{}{"", "", "", tag, treeElement, path})
|
||||
}
|
||||
tab.AddRow(data)
|
||||
}
|
||||
|
||||
tab.Footer = fmt.Sprintf("%d snapshots", len(list))
|
||||
tab.AddFooter(fmt.Sprintf("%d snapshots", len(list)))
|
||||
|
||||
if multiline {
|
||||
// print an additional blank line between snapshots
|
||||
|
||||
var last int
|
||||
tab.PrintData = func(w io.Writer, idx int, s string) error {
|
||||
var err error
|
||||
if idx == last {
|
||||
_, err = fmt.Fprintf(w, "%s\n", s)
|
||||
} else {
|
||||
_, err = fmt.Fprintf(w, "\n%s\n", s)
|
||||
}
|
||||
last = idx
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tab.Write(stdout)
|
||||
}
|
||||
|
||||
// PrintSnapshotGroupHeader prints which group of the group-by option the
|
||||
// following snapshots belong to.
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if key.Hostname == "" && key.Tags == nil && key.Paths == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info
|
||||
fmt.Fprintf(stdout, "snapshots")
|
||||
var infoStrings []string
|
||||
if key.Hostname != "" {
|
||||
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
|
||||
}
|
||||
if key.Tags != nil {
|
||||
infoStrings = append(infoStrings, "tags ["+strings.Join(key.Tags, ", ")+"]")
|
||||
}
|
||||
if key.Paths != nil {
|
||||
infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
|
||||
}
|
||||
if infoStrings != nil {
|
||||
fmt.Fprintf(stdout, " for (%s)", strings.Join(infoStrings, ", "))
|
||||
}
|
||||
fmt.Fprintf(stdout, ":\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshot helps to print Snaphots as JSON with their ID included.
|
||||
type Snapshot struct {
|
||||
*restic.Snapshot
|
||||
@@ -220,19 +285,58 @@ type Snapshot struct {
|
||||
ShortID string `json:"short_id"`
|
||||
}
|
||||
|
||||
// printSnapshotsJSON writes the JSON representation of list to stdout.
|
||||
func printSnapshotsJSON(stdout io.Writer, list restic.Snapshots) error {
|
||||
// SnapshotGroup helps to print SnaphotGroups as JSON with their GroupReasons included.
|
||||
type SnapshotGroup struct {
|
||||
GroupKey restic.SnapshotGroupKey `json:"group_key"`
|
||||
Snapshots []Snapshot `json:"snapshots"`
|
||||
}
|
||||
|
||||
// printSnapshotsJSON writes the JSON representation of list to stdout.
|
||||
func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]restic.Snapshots, grouped bool) error {
|
||||
if grouped {
|
||||
var snapshotGroups []SnapshotGroup
|
||||
|
||||
for k, list := range snGroups {
|
||||
var key restic.SnapshotGroupKey
|
||||
var err error
|
||||
var snapshots []Snapshot
|
||||
|
||||
err = json.Unmarshal([]byte(k), &key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, sn := range list {
|
||||
k := Snapshot{
|
||||
Snapshot: sn,
|
||||
ID: sn.ID(),
|
||||
ShortID: sn.ID().Str(),
|
||||
}
|
||||
snapshots = append(snapshots, k)
|
||||
}
|
||||
|
||||
group := SnapshotGroup{
|
||||
GroupKey: key,
|
||||
Snapshots: snapshots,
|
||||
}
|
||||
snapshotGroups = append(snapshotGroups, group)
|
||||
}
|
||||
|
||||
return json.NewEncoder(stdout).Encode(snapshotGroups)
|
||||
}
|
||||
|
||||
// Old behavior
|
||||
var snapshots []Snapshot
|
||||
|
||||
for _, sn := range list {
|
||||
|
||||
k := Snapshot{
|
||||
Snapshot: sn,
|
||||
ID: sn.ID(),
|
||||
ShortID: sn.ID().Str(),
|
||||
for _, list := range snGroups {
|
||||
for _, sn := range list {
|
||||
k := Snapshot{
|
||||
Snapshot: sn,
|
||||
ID: sn.ID(),
|
||||
ShortID: sn.ID().Str(),
|
||||
}
|
||||
snapshots = append(snapshots, k)
|
||||
}
|
||||
snapshots = append(snapshots, k)
|
||||
}
|
||||
|
||||
return json.NewEncoder(stdout).Encode(snapshots)
|
||||
|
||||
325
cmd/restic/cmd_stats.go
Normal file
325
cmd/restic/cmd_stats.go
Normal file
@@ -0,0 +1,325 @@
|
||||
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/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdStats = &cobra.Command{
|
||||
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 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.
|
||||
|
||||
The modes are:
|
||||
|
||||
* restore-size: (default) Counts the size of the restored files.
|
||||
* files-by-contents: Counts total size of files, where a file is
|
||||
considered unique if it has unique contents.
|
||||
* raw-data: Counts the size of blobs in the repository, regardless of
|
||||
how many files reference them.
|
||||
* blobs-per-file: A combination of files-by-contents and raw-data.
|
||||
|
||||
Refer to the online manual for more details about each mode.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runStats(globalOptions, args)
|
||||
},
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func runStats(gopts GlobalOptions, args []string) error {
|
||||
err := verifyStatsInput(gopts, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(gopts.ctx)
|
||||
defer cancel()
|
||||
|
||||
repo, err := OpenRepository(gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = repo.LoadIndex(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gopts.NoLock {
|
||||
lock, err := lockRepo(repo)
|
||||
defer unlockRepo(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !gopts.JSON {
|
||||
Printf("scanning...\n")
|
||||
}
|
||||
|
||||
// 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(),
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.Fatalf("error loading snapshot from repo: %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 {
|
||||
// the blob handles have been collected, but not yet counted
|
||||
for blobHandle := range stats.blobs {
|
||||
blobSize, found := repo.LookupBlobSize(blobHandle.ID, blobHandle.Type)
|
||||
if !found {
|
||||
return fmt.Errorf("blob %v not found", blobHandle)
|
||||
}
|
||||
stats.TotalSize += uint64(blobSize)
|
||||
stats.TotalBlobCount++
|
||||
}
|
||||
}
|
||||
|
||||
if gopts.JSON {
|
||||
err = json.NewEncoder(os.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)
|
||||
|
||||
if stats.TotalBlobCount > 0 {
|
||||
Printf(" Total Blob Count: %d\n", stats.TotalBlobCount)
|
||||
}
|
||||
if stats.TotalFileCount > 0 {
|
||||
Printf(" Total File Count: %d\n", stats.TotalFileCount)
|
||||
}
|
||||
Printf(" Total Size: %-5s\n", formatBytes(stats.TotalSize))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func statsWalkSnapshot(ctx context.Context, snapshot *restic.Snapshot, repo restic.Repository, stats *statsContainer) error {
|
||||
if snapshot.Tree == nil {
|
||||
return fmt.Errorf("snapshot %s has nil tree", snapshot.ID().Str())
|
||||
}
|
||||
|
||||
if 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
if nodeErr != nil {
|
||||
return true, nodeErr
|
||||
}
|
||||
if node == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if countMode == countModeUniqueFilesByContents || 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 {
|
||||
// simply count the size of each unique file (unique by contents only)
|
||||
stats.TotalSize += node.Size
|
||||
stats.TotalFileCount++
|
||||
}
|
||||
if 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 {
|
||||
// ensure we have this file (by path) in our map; in this
|
||||
// mode, a file is unique by both contents and path
|
||||
nodePath := filepath.Join(npath, node.Name)
|
||||
if _, ok := stats.fileBlobs[nodePath]; !ok {
|
||||
stats.fileBlobs[nodePath] = restic.NewIDSet()
|
||||
stats.TotalFileCount++
|
||||
}
|
||||
if _, ok := stats.fileBlobs[nodePath][blobID]; !ok {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// count the blob's size, then add this blob by this
|
||||
// file (path) so we don't double-count it
|
||||
stats.TotalSize += uint64(blobSize)
|
||||
stats.fileBlobs[nodePath].Insert(blobID)
|
||||
// this mode also counts total unique blob _references_ per file
|
||||
stats.TotalBlobCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if 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++
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// makeFileIDByContents returns a hash of the blob IDs of the
|
||||
// node's Content in sequence.
|
||||
func makeFileIDByContents(node *restic.Node) fileID {
|
||||
var bb []byte
|
||||
for _, c := range node.Content {
|
||||
bb = append(bb, []byte(c[:])...)
|
||||
}
|
||||
return sha256.Sum256(bb)
|
||||
}
|
||||
|
||||
func verifyStatsInput(gopts GlobalOptions, args []string) error {
|
||||
// require a recognized counting mode
|
||||
switch 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 nil
|
||||
}
|
||||
|
||||
// statsContainer holds information during a walk of a repository
|
||||
// to collect information about it, as well as state needed
|
||||
// for a successful and efficient walk.
|
||||
type statsContainer struct {
|
||||
TotalSize uint64 `json:"total_size"`
|
||||
TotalFileCount uint64 `json:"total_file_count"`
|
||||
TotalBlobCount uint64 `json:"total_blob_count,omitempty"`
|
||||
|
||||
// uniqueFiles marks visited files according to their
|
||||
// contents (hashed sequence of content blob IDs)
|
||||
uniqueFiles map[fileID]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
|
||||
}
|
||||
|
||||
// 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"
|
||||
countModeBlobsPerFile = "blobs-per-file"
|
||||
countModeRawData = "raw-data"
|
||||
)
|
||||
@@ -60,15 +60,20 @@ func (rc *rejectionCache) Store(dir string, rejected bool) {
|
||||
rc.m[dir] = rejected
|
||||
}
|
||||
|
||||
// RejectByNameFunc is a function that takes a filename of a
|
||||
// file that would be included in the backup. The function returns true if it
|
||||
// should be excluded (rejected) from the backup.
|
||||
type RejectByNameFunc func(path string) bool
|
||||
|
||||
// RejectFunc is a function that takes a filename and os.FileInfo of a
|
||||
// file that would be included in the backup. The function returns true if it
|
||||
// should be excluded (rejected) from the backup.
|
||||
type RejectFunc func(path string, fi os.FileInfo) bool
|
||||
|
||||
// rejectByPattern returns a RejectFunc which rejects files that match
|
||||
// rejectByPattern returns a RejectByNameFunc which rejects files that match
|
||||
// one of the patterns.
|
||||
func rejectByPattern(patterns []string) RejectFunc {
|
||||
return func(item string, fi os.FileInfo) bool {
|
||||
func rejectByPattern(patterns []string) RejectByNameFunc {
|
||||
return func(item string) bool {
|
||||
matched, _, err := filter.List(patterns, item)
|
||||
if err != nil {
|
||||
Warnf("error for exclude pattern: %v", err)
|
||||
@@ -83,14 +88,26 @@ func rejectByPattern(patterns []string) RejectFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// rejectIfPresent returns a RejectFunc which itself returns whether a path
|
||||
// should be excluded. The RejectFunc considers a file to be excluded when
|
||||
// Same as `rejectByPattern` but case insensitive.
|
||||
func rejectByInsensitivePattern(patterns []string) RejectByNameFunc {
|
||||
for index, path := range patterns {
|
||||
patterns[index] = strings.ToLower(path)
|
||||
}
|
||||
|
||||
rejFunc := rejectByPattern(patterns)
|
||||
return func(item string) bool {
|
||||
return rejFunc(strings.ToLower(item))
|
||||
}
|
||||
}
|
||||
|
||||
// rejectIfPresent returns a RejectByNameFunc which itself returns whether a path
|
||||
// should be excluded. The RejectByNameFunc considers a file to be excluded when
|
||||
// it resides in a directory with an exclusion file, that is specified by
|
||||
// excludeFileSpec in the form "filename[:content]". The returned error is
|
||||
// non-nil if the filename component of excludeFileSpec is empty. If rc is
|
||||
// non-nil, it is going to be used in the RejectFunc to expedite the evaluation
|
||||
// non-nil, it is going to be used in the RejectByNameFunc to expedite the evaluation
|
||||
// of a directory based on previous visits.
|
||||
func rejectIfPresent(excludeFileSpec string) (RejectFunc, error) {
|
||||
func rejectIfPresent(excludeFileSpec string) (RejectByNameFunc, error) {
|
||||
if excludeFileSpec == "" {
|
||||
return nil, errors.New("name for exclusion tagfile is empty")
|
||||
}
|
||||
@@ -107,7 +124,7 @@ func rejectIfPresent(excludeFileSpec string) (RejectFunc, error) {
|
||||
}
|
||||
debug.Log("using %q as exclusion tagfile", tf)
|
||||
rc := &rejectionCache{}
|
||||
fn := func(filename string, _ os.FileInfo) bool {
|
||||
fn := func(filename string) bool {
|
||||
return isExcludedByFile(filename, tf, tc, rc)
|
||||
}
|
||||
return fn, nil
|
||||
@@ -252,11 +269,11 @@ func rejectByDevice(samples []string) (RejectFunc, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// rejectResticCache returns a RejectFunc that rejects the restic cache
|
||||
// rejectResticCache returns a RejectByNameFunc that rejects the restic cache
|
||||
// directory (if set).
|
||||
func rejectResticCache(repo *repository.Repository) (RejectFunc, error) {
|
||||
func rejectResticCache(repo *repository.Repository) (RejectByNameFunc, error) {
|
||||
if repo.Cache == nil {
|
||||
return func(string, os.FileInfo) bool {
|
||||
return func(string) bool {
|
||||
return false
|
||||
}, nil
|
||||
}
|
||||
@@ -266,7 +283,7 @@ func rejectResticCache(repo *repository.Repository) (RejectFunc, error) {
|
||||
return nil, errors.New("cacheBase is empty string")
|
||||
}
|
||||
|
||||
return func(item string, _ os.FileInfo) bool {
|
||||
return func(item string) bool {
|
||||
if fs.HasPathPrefix(cacheBase, item) {
|
||||
debug.Log("rejecting restic cache directory %v", item)
|
||||
return true
|
||||
|
||||
@@ -27,7 +27,34 @@ func TestRejectByPattern(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
reject := rejectByPattern(patterns)
|
||||
res := reject(tc.filename, nil)
|
||||
res := reject(tc.filename)
|
||||
if res != tc.reject {
|
||||
t.Fatalf("wrong result for filename %v: want %v, got %v",
|
||||
tc.filename, tc.reject, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectByInsensitivePattern(t *testing.T) {
|
||||
var tests = []struct {
|
||||
filename string
|
||||
reject bool
|
||||
}{
|
||||
{filename: "/home/user/foo.GO", reject: true},
|
||||
{filename: "/home/user/foo.c", reject: false},
|
||||
{filename: "/home/user/foobar", reject: false},
|
||||
{filename: "/home/user/FOObar/x", reject: true},
|
||||
{filename: "/home/user/README", reject: false},
|
||||
{filename: "/home/user/readme.md", reject: true},
|
||||
}
|
||||
|
||||
patterns := []string{"*.go", "README.md", "/home/user/foobar/*"}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
reject := rejectByInsensitivePattern(patterns)
|
||||
res := reject(tc.filename)
|
||||
if res != tc.reject {
|
||||
t.Fatalf("wrong result for filename %v: want %v, got %v",
|
||||
tc.filename, tc.reject, res)
|
||||
@@ -140,8 +167,8 @@ func TestMultipleIsExcludedByFile(t *testing.T) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
excludedByFoo := fooExclude(p, fi)
|
||||
excludedByBar := barExclude(p, fi)
|
||||
excludedByFoo := fooExclude(p)
|
||||
excludedByBar := barExclude(p)
|
||||
excluded := excludedByFoo || excludedByBar
|
||||
// the log message helps debugging in case the test fails
|
||||
t.Logf("%q: %v || %v = %v", p, excludedByFoo, excludedByBar, excluded)
|
||||
|
||||
@@ -21,7 +21,7 @@ func formatBytes(c uint64) string {
|
||||
case c > 1<<10:
|
||||
return fmt.Sprintf("%.3f KiB", b/(1<<10))
|
||||
default:
|
||||
return fmt.Sprintf("%dB", c)
|
||||
return fmt.Sprintf("%d B", c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,6 @@ func formatNode(path string, n *restic.Node, long bool) string {
|
||||
|
||||
return fmt.Sprintf("%s %5d %5d %6d %s %s%s",
|
||||
mode|n.Mode, n.UID, n.GID, n.Size,
|
||||
n.ModTime.Format(TimeFormat), path,
|
||||
n.ModTime.Local().Format(TimeFormat), path,
|
||||
target)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -33,24 +34,31 @@ import (
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
|
||||
"os/exec"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
var version = "compiled manually"
|
||||
var version = "0.9.6"
|
||||
|
||||
// TimeFormat is the format used for all timestamps printed by restic.
|
||||
const TimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
// GlobalOptions hold all global options for restic.
|
||||
type GlobalOptions struct {
|
||||
Repo string
|
||||
PasswordFile string
|
||||
Quiet bool
|
||||
Verbose int
|
||||
NoLock bool
|
||||
JSON bool
|
||||
CacheDir string
|
||||
NoCache bool
|
||||
CACerts []string
|
||||
TLSClientCert string
|
||||
CleanupCache bool
|
||||
Repo string
|
||||
PasswordFile string
|
||||
PasswordCommand string
|
||||
KeyHint string
|
||||
Quiet bool
|
||||
Verbose int
|
||||
NoLock bool
|
||||
JSON bool
|
||||
CacheDir string
|
||||
NoCache bool
|
||||
CACerts []string
|
||||
TLSClientCert string
|
||||
CleanupCache bool
|
||||
|
||||
LimitUploadKb int
|
||||
LimitDownloadKb int
|
||||
@@ -64,7 +72,7 @@ type GlobalOptions struct {
|
||||
// 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 --debug is specified
|
||||
// 3 means: print very detailed debug messages, this is used when --verbose 2 is specified
|
||||
verbosity uint
|
||||
|
||||
Options []string
|
||||
@@ -88,11 +96,13 @@ 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.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.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
|
||||
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
|
||||
f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory")
|
||||
f.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")
|
||||
@@ -176,7 +186,6 @@ func Printf(format string, args ...interface{}) {
|
||||
_, err := fmt.Fprintf(globalOptions.stdout, format, args...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err)
|
||||
Exit(100)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +226,6 @@ func Warnf(format string, args ...interface{}) {
|
||||
_, err := fmt.Fprintf(globalOptions.stderr, format, args...)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err)
|
||||
Exit(100)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +241,23 @@ func Exitf(exitcode int, format string, args ...interface{}) {
|
||||
}
|
||||
|
||||
// resolvePassword determines the password to be used for opening the repository.
|
||||
func resolvePassword(opts GlobalOptions, env string) (string, error) {
|
||||
func resolvePassword(opts GlobalOptions) (string, error) {
|
||||
if opts.PasswordFile != "" && opts.PasswordCommand != "" {
|
||||
return "", errors.Fatalf("Password file and command are mutually exclusive options")
|
||||
}
|
||||
if opts.PasswordCommand != "" {
|
||||
args, err := backend.SplitShellStrings(opts.PasswordCommand)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stderr = os.Stderr
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return (strings.TrimSpace(string(output))), nil
|
||||
}
|
||||
if opts.PasswordFile != "" {
|
||||
s, err := textfile.Read(opts.PasswordFile)
|
||||
if os.IsNotExist(errors.Cause(err)) {
|
||||
@@ -242,7 +266,7 @@ func resolvePassword(opts GlobalOptions, env string) (string, error) {
|
||||
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
|
||||
}
|
||||
|
||||
if pwd := os.Getenv(env); pwd != "" {
|
||||
if pwd := os.Getenv("RESTIC_PASSWORD"); pwd != "" {
|
||||
return pwd, nil
|
||||
}
|
||||
|
||||
@@ -251,15 +275,10 @@ func resolvePassword(opts GlobalOptions, env string) (string, error) {
|
||||
|
||||
// readPassword reads the password from the given reader directly.
|
||||
func readPassword(in io.Reader) (password string, err error) {
|
||||
buf := make([]byte, 1000)
|
||||
n, err := io.ReadFull(in, buf)
|
||||
buf = buf[:n]
|
||||
sc := bufio.NewScanner(in)
|
||||
sc.Scan()
|
||||
|
||||
if err != nil && errors.Cause(err) != io.ErrUnexpectedEOF {
|
||||
return "", errors.Wrap(err, "ReadFull")
|
||||
}
|
||||
|
||||
return strings.TrimRight(string(buf), "\r\n"), nil
|
||||
return sc.Text(), errors.Wrap(err, "Scan")
|
||||
}
|
||||
|
||||
// readPasswordTerminal reads the password from the given reader which must be a
|
||||
@@ -293,6 +312,7 @@ func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
|
||||
password, err = readPasswordTerminal(os.Stdin, os.Stderr, prompt)
|
||||
} else {
|
||||
password, err = readPassword(os.Stdin)
|
||||
Verbosef("read password from stdin\n")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -300,7 +320,7 @@ func ReadPassword(opts GlobalOptions, prompt string) (string, error) {
|
||||
}
|
||||
|
||||
if len(password) == 0 {
|
||||
return "", errors.Fatal("an empty password is not a password")
|
||||
return "", errors.New("an empty password is not a password")
|
||||
}
|
||||
|
||||
return password, nil
|
||||
@@ -313,13 +333,15 @@ func ReadPasswordTwice(gopts GlobalOptions, prompt1, prompt2 string) (string, er
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pw2, err := ReadPassword(gopts, prompt2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if stdinIsTerminal() {
|
||||
pw2, err := ReadPassword(gopts, prompt2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pw1 != pw2 {
|
||||
return "", errors.Fatal("passwords do not match")
|
||||
if pw1 != pw2 {
|
||||
return "", errors.Fatal("passwords do not match")
|
||||
}
|
||||
}
|
||||
|
||||
return pw1, nil
|
||||
@@ -344,22 +366,42 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
||||
|
||||
s := repository.New(be)
|
||||
|
||||
opts.password, err = ReadPassword(opts, "enter password for repository: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
passwordTriesLeft := 1
|
||||
if stdinIsTerminal() && opts.password == "" {
|
||||
passwordTriesLeft = 3
|
||||
}
|
||||
|
||||
err = s.SearchKey(opts.ctx, opts.password, maxKeys)
|
||||
for ; passwordTriesLeft > 0; passwordTriesLeft-- {
|
||||
opts.password, err = ReadPassword(opts, "enter password for repository: ")
|
||||
if err != nil && passwordTriesLeft > 1 {
|
||||
opts.password = ""
|
||||
fmt.Printf("%s. Try again\n", err)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
err = s.SearchKey(opts.ctx, opts.password, maxKeys, opts.KeyHint)
|
||||
if err != nil && passwordTriesLeft > 1 {
|
||||
opts.password = ""
|
||||
fmt.Printf("%s. Try again\n", err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if errors.IsFatal(err) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
if stdoutIsTerminal() {
|
||||
if stdoutIsTerminal() && !opts.JSON {
|
||||
id := s.Config().ID
|
||||
if len(id) > 8 {
|
||||
id = id[:8]
|
||||
}
|
||||
Verbosef("repository %v opened successfully, password is correct\n", id)
|
||||
if !opts.JSON {
|
||||
Verbosef("repository %v opened successfully, password is correct\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.NoCache {
|
||||
@@ -372,6 +414,10 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
if c.Created && !opts.JSON {
|
||||
Verbosef("created new cache in %v\n", c.Base)
|
||||
}
|
||||
|
||||
// start using the cache
|
||||
s.UseCache(c)
|
||||
|
||||
@@ -398,7 +444,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
|
||||
}
|
||||
} else {
|
||||
if stdoutIsTerminal() {
|
||||
Verbosef("found %d old cache directories in %v, pass --cleanup-cache to remove them\n",
|
||||
Verbosef("found %d old cache directories in %v, run `restic cache --cleanup` to remove them\n",
|
||||
len(oldCacheDirs), c.Base)
|
||||
}
|
||||
}
|
||||
@@ -439,6 +485,10 @@ func parseConfig(loc location.Location, opts options.Options) (interface{}, erro
|
||||
cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
|
||||
}
|
||||
|
||||
if cfg.Region == "" {
|
||||
cfg.Region = os.Getenv("AWS_DEFAULT_REGION")
|
||||
}
|
||||
|
||||
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// +build !netbsd
|
||||
// +build !openbsd
|
||||
// +build !solaris
|
||||
// +build !windows
|
||||
|
||||
@@ -66,7 +66,7 @@ func isSymlink(fi os.FileInfo) bool {
|
||||
|
||||
func sameModTime(fi1, fi2 os.FileInfo) bool {
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "freebsd", "openbsd":
|
||||
case "darwin", "freebsd", "openbsd", "netbsd":
|
||||
if isSymlink(fi1) && isSymlink(fi2) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -219,6 +219,35 @@ func testRunForget(t testing.TB, gopts GlobalOptions, args ...string) {
|
||||
rtest.OK(t, runForget(opts, gopts, args))
|
||||
}
|
||||
|
||||
func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
oldJSON := gopts.JSON
|
||||
gopts.stdout = buf
|
||||
gopts.JSON = true
|
||||
defer func() {
|
||||
gopts.stdout = os.Stdout
|
||||
gopts.JSON = oldJSON
|
||||
}()
|
||||
|
||||
opts := ForgetOptions{
|
||||
DryRun: true,
|
||||
Last: 1,
|
||||
}
|
||||
|
||||
rtest.OK(t, runForget(opts, gopts, args))
|
||||
|
||||
var forgets []*ForgetGroup
|
||||
rtest.OK(t, json.Unmarshal(buf.Bytes(), &forgets))
|
||||
|
||||
rtest.Assert(t, len(forgets) == 1,
|
||||
"Expected 1 snapshot group, got %v", len(forgets))
|
||||
rtest.Assert(t, len(forgets[0].Keep) == 1,
|
||||
"Expected 1 snapshot to be kept, got %v", len(forgets[0].Keep))
|
||||
rtest.Assert(t, len(forgets[0].Remove) == 2,
|
||||
"Expected 2 snapshots to be removed, got %v", len(forgets[0].Remove))
|
||||
return
|
||||
}
|
||||
|
||||
func testRunPrune(t testing.TB, gopts GlobalOptions) {
|
||||
rtest.OK(t, runPrune(gopts))
|
||||
}
|
||||
@@ -875,9 +904,6 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) {
|
||||
fi, err := os.Stat(f1)
|
||||
rtest.OK(t, err)
|
||||
|
||||
rtest.Assert(t, fi.ModTime() != time.Unix(0, 0),
|
||||
"meta data of intermediate directory has been restore although it was ignored")
|
||||
|
||||
// restore with filter "*", this should restore meta data on everything.
|
||||
testRunRestoreIncludes(t, env.gopts, filepath.Join(env.base, "restore1"), snapshotID, []string{"*"})
|
||||
|
||||
@@ -1054,6 +1080,7 @@ func TestPrune(t *testing.T) {
|
||||
rtest.Assert(t, len(snapshotIDs) == 3,
|
||||
"expected 3 snapshot, got %v", snapshotIDs)
|
||||
|
||||
testRunForgetJSON(t, env.gopts)
|
||||
testRunForget(t, env.gopts, firstSnapshot[0].String())
|
||||
testRunPrune(t, env.gopts)
|
||||
testRunCheck(t, env.gopts)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user