mirror of
https://github.com/restic/restic.git
synced 2025-08-17 13:37:24 +00:00
Compare commits
729 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9b2c0a0c54 | ||
![]() |
64273ea027 | ||
![]() |
5a00d26431 | ||
![]() |
3faad5751d | ||
![]() |
f487eb1c66 | ||
![]() |
72636238d0 | ||
![]() |
51098157e2 | ||
![]() |
0b080c44d7 | ||
![]() |
b71fe91643 | ||
![]() |
9c3b8d171a | ||
![]() |
ddb7fb837b | ||
![]() |
3433c5abac | ||
![]() |
09bc58c950 | ||
![]() |
20eb9018a0 | ||
![]() |
651f553530 | ||
![]() |
aad4b53ead | ||
![]() |
e467496ace | ||
![]() |
f2de260524 | ||
![]() |
c17d5ab2e1 | ||
![]() |
a8535aba58 | ||
![]() |
521fbad701 | ||
![]() |
15b7d7c3fc | ||
![]() |
7d39b1bfe8 | ||
![]() |
e4a7f4aadf | ||
![]() |
10cfe96cd4 | ||
![]() |
2eaa79d33f | ||
![]() |
99ee5696f3 | ||
![]() |
e8dbb69a94 | ||
![]() |
f4e21cdb75 | ||
![]() |
e5bdc3c74f | ||
![]() |
7e51c928c4 | ||
![]() |
21e87851aa | ||
![]() |
2bc1bf2702 | ||
![]() |
df110060d1 | ||
![]() |
337a7d1205 | ||
![]() |
322e271dd2 | ||
![]() |
1ac224458f | ||
![]() |
126ad04568 | ||
![]() |
e732bdbfb8 | ||
![]() |
2db08fd749 | ||
![]() |
debb110a7c | ||
![]() |
5eb4f5af61 | ||
![]() |
287b601f01 | ||
![]() |
64c82a5d9c | ||
![]() |
12f36ebf07 | ||
![]() |
45e09dca2a | ||
![]() |
5bb9d0d996 | ||
![]() |
9f39e8a1d3 | ||
![]() |
ddd48f1e98 | ||
![]() |
6e91ea3397 | ||
![]() |
e7c1e4f1ff | ||
![]() |
70e1037a49 | ||
![]() |
19f48084ea | ||
![]() |
3a995172b7 | ||
![]() |
0dffa1208d | ||
![]() |
6fbcce1d1a | ||
![]() |
e8d458be7e | ||
![]() |
4471c7847b | ||
![]() |
f13e9c10a4 | ||
![]() |
f768683162 | ||
![]() |
0b7bdfed7e | ||
![]() |
a4fe94ec82 | ||
![]() |
6684d1d2f5 | ||
![]() |
e1f7522174 | ||
![]() |
d1649affb2 | ||
![]() |
936c783c0f | ||
![]() |
5614cf4758 | ||
![]() |
6db0d84ab0 | ||
![]() |
88b599c4f3 | ||
![]() |
eefff0d793 | ||
![]() |
3d14e92905 | ||
![]() |
d401ad6c1e | ||
![]() |
ab024e6a51 | ||
![]() |
0e5f41c842 | ||
![]() |
321ac6c1c9 | ||
![]() |
94b1af580b | ||
![]() |
cc6fbbe6ad | ||
![]() |
3f70485671 | ||
![]() |
d4772aa469 | ||
![]() |
13cb90b83a | ||
![]() |
823cc3d93a | ||
![]() |
9eee32131a | ||
![]() |
5e519a25f7 | ||
![]() |
c4eb2be31f | ||
![]() |
0b22d8dc64 | ||
![]() |
2b65ef5710 | ||
![]() |
ccb92f5bf0 | ||
![]() |
37aa4f824f | ||
![]() |
47b048f437 | ||
![]() |
cd7f384d77 | ||
![]() |
9d58a27428 | ||
![]() |
9aad8e9ea5 | ||
![]() |
3adf7d4efb | ||
![]() |
66ec735ac2 | ||
![]() |
63a71f70e3 | ||
![]() |
e3ddc8a463 | ||
![]() |
66a8e897a9 | ||
![]() |
ffd63f893a | ||
![]() |
ec19d67512 | ||
![]() |
ef18feaeeb | ||
![]() |
171f303399 | ||
![]() |
dda652614e | ||
![]() |
784097a4f8 | ||
![]() |
f5989964ed | ||
![]() |
cfa3c5884d | ||
![]() |
d60acc5697 | ||
![]() |
2240d1801c | ||
![]() |
99fdb00d39 | ||
![]() |
2409078d55 | ||
![]() |
0b6c355678 | ||
![]() |
f7f48b3026 | ||
![]() |
1221453d08 | ||
![]() |
4b975bda37 | ||
![]() |
f8b481fd9b | ||
![]() |
f88d5adaa2 | ||
![]() |
89909d41aa | ||
![]() |
06535e62c1 | ||
![]() |
c99c76ada8 | ||
![]() |
4350b95d27 | ||
![]() |
2e58561ad6 | ||
![]() |
17b585f7c7 | ||
![]() |
4640b3c41a | ||
![]() |
c36970074d | ||
![]() |
15e90b7a4c | ||
![]() |
8d2d50d095 | ||
![]() |
62453f9356 | ||
![]() |
6caad10840 | ||
![]() |
4420fde378 | ||
![]() |
a389977bd7 | ||
![]() |
6e45c51509 | ||
![]() |
5e7333d28d | ||
![]() |
c617364d15 | ||
![]() |
e2ccb18e22 | ||
![]() |
d2c5241961 | ||
![]() |
f238f81ba6 | ||
![]() |
3788605127 | ||
![]() |
29b4680873 | ||
![]() |
092899df8b | ||
![]() |
2099ec1cd6 | ||
![]() |
1daf5317f8 | ||
![]() |
db8daeb192 | ||
![]() |
ef692991a4 | ||
![]() |
062cfc549d | ||
![]() |
3e58b15ace | ||
![]() |
69249372bf | ||
![]() |
445477312c | ||
![]() |
cc4712f8e9 | ||
![]() |
c405e9e748 | ||
![]() |
5f40e4b7c5 | ||
![]() |
0b0987233f | ||
![]() |
d66e9cfff5 | ||
![]() |
e40996f0f1 | ||
![]() |
818cb386a5 | ||
![]() |
9f724f7dc5 | ||
![]() |
3f42c0ad96 | ||
![]() |
794341a494 | ||
![]() |
74b76ca0df | ||
![]() |
3b21c7da3d | ||
![]() |
f838bf1056 | ||
![]() |
664971eb1d | ||
![]() |
de9a040d27 | ||
![]() |
89826ef5ce | ||
![]() |
a2a1309fd9 | ||
![]() |
6309952a82 | ||
![]() |
5e7ce45ede | ||
![]() |
cb8575f001 | ||
![]() |
8d1185b3b8 | ||
![]() |
c970e58739 | ||
![]() |
5ddda7f5e9 | ||
![]() |
8c12291f56 | ||
![]() |
5190933561 | ||
![]() |
00e69f242e | ||
![]() |
00628e952f | ||
![]() |
39e63ee4e3 | ||
![]() |
3b8d15d651 | ||
![]() |
2fd8a3865c | ||
![]() |
0c4e65228a | ||
![]() |
120bd08c0d | ||
![]() |
d378a171c8 | ||
![]() |
c752867f0a | ||
![]() |
412d6d9ec5 | ||
![]() |
5497217018 | ||
![]() |
aa9cdf93cf | ||
![]() |
aacd6a47e3 | ||
![]() |
dc9b6378f3 | ||
![]() |
4e58902de6 | ||
![]() |
39823c5f6c | ||
![]() |
421842f41f | ||
![]() |
59b7007534 | ||
![]() |
da47967316 | ||
![]() |
49a411f7ac | ||
![]() |
7cc1aa0cd4 | ||
![]() |
a58a8f2ce0 | ||
![]() |
79d435efb1 | ||
![]() |
9cdf91b406 | ||
![]() |
4104a8e6a5 | ||
![]() |
6cc06e0812 | ||
![]() |
c32613a624 | ||
![]() |
1807627dda | ||
![]() |
993eb112cd | ||
![]() |
36d8916354 | ||
![]() |
060a44202f | ||
![]() |
d79681b987 | ||
![]() |
90e2c419e4 | ||
![]() |
7ab5bb6df4 | ||
![]() |
efd2ec086f | ||
![]() |
8d970e36cf | ||
![]() |
58f58a995d | ||
![]() |
d71ddfb89b | ||
![]() |
536ebefff4 | ||
![]() |
9566e2db4a | ||
![]() |
7829728182 | ||
![]() |
72b343fe5a | ||
![]() |
9c8c59c889 | ||
![]() |
c4d988faf8 | ||
![]() |
080c8de1a9 | ||
![]() |
c1781e0abb | ||
![]() |
2b9113721c | ||
![]() |
afe4fcc0d9 | ||
![]() |
c2e404a0ee | ||
![]() |
c4be05dbc2 | ||
![]() |
d0d887138c | ||
![]() |
8eaa4b6602 | ||
![]() |
e77681f2cd | ||
![]() |
a63500663a | ||
![]() |
fde64133df | ||
![]() |
6301250d83 | ||
![]() |
9331461a13 | ||
![]() |
ed3922ac82 | ||
![]() |
8b63e1cd72 | ||
![]() |
5e8654c71d | ||
![]() |
d5a94583ed | ||
![]() |
115ecb3c92 | ||
![]() |
e6f9cfb8c8 | ||
![]() |
b7ff8ea9cd | ||
![]() |
99e105eeb6 | ||
![]() |
5bf0204caf | ||
![]() |
14d02df8bb | ||
![]() |
bd4ce8aac1 | ||
![]() |
da71e77b28 | ||
![]() |
27189e03ee | ||
![]() |
4e1eeeb721 | ||
![]() |
3b37983a60 | ||
![]() |
99646fdf62 | ||
![]() |
0331891545 | ||
![]() |
2b45c004be | ||
![]() |
44cef25077 | ||
![]() |
cd84fe0853 | ||
![]() |
3ac697d03d | ||
![]() |
24422e20a6 | ||
![]() |
f457b16b23 | ||
![]() |
af839f9548 | ||
![]() |
bbb492ee65 | ||
![]() |
01405f1e1b | ||
![]() |
caa59bb81b | ||
![]() |
de3acd7937 | ||
![]() |
9e85119d73 | ||
![]() |
37969ae8e3 | ||
![]() |
6808004ad1 | ||
![]() |
8d45a4b283 | ||
![]() |
4fb9aa4351 | ||
![]() |
d422e75e08 | ||
![]() |
144221b430 | ||
![]() |
d7d9af4c9f | ||
![]() |
2f0049cd6c | ||
![]() |
72c02fa759 | ||
![]() |
770841f95d | ||
![]() |
5e0a045481 | ||
![]() |
3fecddafe8 | ||
![]() |
40987a5f80 | ||
![]() |
875976f4a8 | ||
![]() |
2dc00cfd36 | ||
![]() |
45d2b4cd3c | ||
![]() |
a4d776ec8f | ||
![]() |
098db935f7 | ||
![]() |
ead57ec501 | ||
![]() |
8f9d755b44 | ||
![]() |
1062546563 | ||
![]() |
0bf8af7188 | ||
![]() |
9a674ecc34 | ||
![]() |
9a99141a5f | ||
![]() |
847b2efba2 | ||
![]() |
641390103d | ||
![]() |
806fa534ce | ||
![]() |
5df6bf80b1 | ||
![]() |
dc89aad722 | ||
![]() |
3c0ceda536 | ||
![]() |
c5fb46da53 | ||
![]() |
8642049532 | ||
![]() |
8644bb145b | ||
![]() |
0997f26461 | ||
![]() |
a5c49e5340 | ||
![]() |
b51bf0c0c4 | ||
![]() |
6cb19e0190 | ||
![]() |
d7f4b9db60 | ||
![]() |
087f95a298 | ||
![]() |
6084848e5a | ||
![]() |
48dbefc37e | ||
![]() |
2f2ce9add2 | ||
![]() |
623ba92b98 | ||
![]() |
b402e8a6fc | ||
![]() |
548fa07577 | ||
![]() |
f8031561f2 | ||
![]() |
49ef3ebec3 | ||
![]() |
dfbd4fb983 | ||
![]() |
1133498ef8 | ||
![]() |
9c758313e3 | ||
![]() |
82c5043fc9 | ||
![]() |
a73ae7ba1a | ||
![]() |
bd16804812 | ||
![]() |
e2a98aa955 | ||
![]() |
bc64921a8e | ||
![]() |
633883bdb6 | ||
![]() |
8348024664 | ||
![]() |
c3f5748e5b | ||
![]() |
06ba4af436 | ||
![]() |
fb4d9b3232 | ||
![]() |
7bfe3d99ae | ||
![]() |
d46525a51b | ||
![]() |
3800eac54b | ||
![]() |
75f317eaf1 | ||
![]() |
b8527f4b38 | ||
![]() |
b8b7896d4c | ||
![]() |
d0c5b5a9b7 | ||
![]() |
8aebea7ba2 | ||
![]() |
0e9716a6e6 | ||
![]() |
de4f8b344e | ||
![]() |
75ec7d3269 | ||
![]() |
d8e0384940 | ||
![]() |
408ec41a1d | ||
![]() |
270e7b7679 | ||
![]() |
97f3e15039 | ||
![]() |
d5bd3fcda5 | ||
![]() |
62222edc4a | ||
![]() |
f9a90aae89 | ||
![]() |
289159beaf | ||
![]() |
4052a5927c | ||
![]() |
d3c3390a51 | ||
![]() |
569a117a1d | ||
![]() |
41fa41b28b | ||
![]() |
3eb9556f6a | ||
![]() |
f5b1f9c8b1 | ||
![]() |
e65f4e2231 | ||
![]() |
bcf5fbe498 | ||
![]() |
ded9fc7690 | ||
![]() |
b3b173a47c | ||
![]() |
e18a2a0072 | ||
![]() |
1eea41c49e | ||
![]() |
71c185313e | ||
![]() |
868efe4968 | ||
![]() |
3be2b8a54b | ||
![]() |
b5bc76cdc7 | ||
![]() |
58dc4a6892 | ||
![]() |
74c783b850 | ||
![]() |
fc92a04284 | ||
![]() |
2f698d1cff | ||
![]() |
d8bf327d8b | ||
![]() |
2b3672198c | ||
![]() |
de847a48bf | ||
![]() |
d1d8ae7368 | ||
![]() |
a32c98a39c | ||
![]() |
53cb6200fa | ||
![]() |
ae9268dadf | ||
![]() |
a494bf661d | ||
![]() |
962279479d | ||
![]() |
0aee70b496 | ||
![]() |
4380627cb7 | ||
![]() |
e38f6794cd | ||
![]() |
f77e67086c | ||
![]() |
51cd1c847b | ||
![]() |
14370fbf9e | ||
![]() |
62af5f0b4a | ||
![]() |
cb9247530e | ||
![]() |
1d0d5d87bc | ||
![]() |
03aad742d3 | ||
![]() |
15b7fb784f | ||
![]() |
33da501c35 | ||
![]() |
cd44b2bf8b | ||
![]() |
1f0f6ad63d | ||
![]() |
ca4bd1b8ca | ||
![]() |
7eec85b4eb | ||
![]() |
2fb07dcdb1 | ||
![]() |
5dcee7f0a3 | ||
![]() |
44968c7d43 | ||
![]() |
dbb5fb9fbd | ||
![]() |
e320edd416 | ||
![]() |
3a4a5a8215 | ||
![]() |
d8d955e0aa | ||
![]() |
2ce485063f | ||
![]() |
f72febb34f | ||
![]() |
821000cb68 | ||
![]() |
db686592a1 | ||
![]() |
bff3341d10 | ||
![]() |
5fe6607127 | ||
![]() |
8f20d5dcd5 | ||
![]() |
f967a33ccc | ||
![]() |
ee9a5cdf70 | ||
![]() |
46dce1f4fa | ||
![]() |
841f8bfef0 | ||
![]() |
1f5791222a | ||
![]() |
ec43594003 | ||
![]() |
a7b13bd603 | ||
![]() |
0c711f5605 | ||
![]() |
4df2e33568 | ||
![]() |
11c1fbce20 | ||
![]() |
e1faf7b18c | ||
![]() |
9553d873ff | ||
![]() |
048c3bb240 | ||
![]() |
d6e76a22a8 | ||
![]() |
e3a022f9b5 | ||
![]() |
fe269c752a | ||
![]() |
fc1fc00aa4 | ||
![]() |
3c82fe6ef5 | ||
![]() |
986d981bf6 | ||
![]() |
0df2fa8135 | ||
![]() |
49ccb7734c | ||
![]() |
491cc65e3a | ||
![]() |
8c1d6a50c1 | ||
![]() |
9386acc4a6 | ||
![]() |
5b60d49654 | ||
![]() |
8056181301 | ||
![]() |
fc6f1b4b06 | ||
![]() |
9f206601af | ||
![]() |
ca79cb92e3 | ||
![]() |
352605d9f0 | ||
![]() |
26b77a543d | ||
![]() |
b988754a6d | ||
![]() |
60960d2405 | ||
![]() |
7c02141548 | ||
![]() |
b434f560cc | ||
![]() |
7bdfcf13fb | ||
![]() |
2e704c69ac | ||
![]() |
5838896962 | ||
![]() |
bcd5ac34bb | ||
![]() |
618f306f13 | ||
![]() |
75711446e1 | ||
![]() |
c3b3120e10 | ||
![]() |
e29d38f8bf | ||
![]() |
da3c02405b | ||
![]() |
55c150054d | ||
![]() |
012cb06fe9 | ||
![]() |
f44b7cdf8c | ||
![]() |
e91a456656 | ||
![]() |
e21496f217 | ||
![]() |
0c0d8b8cfd | ||
![]() |
60cba55647 | ||
![]() |
221fa0fa7c | ||
![]() |
7cfd8a6715 | ||
![]() |
0ada0b56b6 | ||
![]() |
7c12bd59a0 | ||
![]() |
888abff7e0 | ||
![]() |
783901726e | ||
![]() |
eac00eb933 | ||
![]() |
96c1c1a0fc | ||
![]() |
8d7f4574b4 | ||
![]() |
ddf65b04f3 | ||
![]() |
2b609d3e77 | ||
![]() |
19653f9e06 | ||
![]() |
e10e2bb50f | ||
![]() |
b5c28a7ba2 | ||
![]() |
f3f629bb69 | ||
![]() |
e90085b375 | ||
![]() |
3f08dee685 | ||
![]() |
8c7a6daa47 | ||
![]() |
3d976562fa | ||
![]() |
1a7fafc7eb | ||
![]() |
4469fe1575 | ||
![]() |
bad6c54a33 | ||
![]() |
7680f48258 | ||
![]() |
efec1a5e96 | ||
![]() |
bd2c986592 | ||
![]() |
cab6b15603 | ||
![]() |
4105e4a356 | ||
![]() |
ccf5be235a | ||
![]() |
5ce6ca2219 | ||
![]() |
51173c5003 | ||
![]() |
e9940f39dc | ||
![]() |
6ec2b62ec5 | ||
![]() |
4795143d6d | ||
![]() |
a84e65b7f9 | ||
![]() |
6f08dbb2d7 | ||
![]() |
c1532179d4 | ||
![]() |
34fe73ea42 | ||
![]() |
37d5bd61a0 | ||
![]() |
7b1a15916d | ||
![]() |
113439c69b | ||
![]() |
5468e85222 | ||
![]() |
b69c6408a6 | ||
![]() |
d656a50852 | ||
![]() |
87f30bc787 | ||
![]() |
4f0affd4f7 | ||
![]() |
3df8337d63 | ||
![]() |
76a647febf | ||
![]() |
975aa41e1e | ||
![]() |
a98370cc9e | ||
![]() |
d8870a2f73 | ||
![]() |
17e54b04ab | ||
![]() |
00ca0b371b | ||
![]() |
8a0edde407 | ||
![]() |
0a225049d8 | ||
![]() |
3023b2f566 | ||
![]() |
a6490feab2 | ||
![]() |
daa6448a77 | ||
![]() |
07a8b73f25 | ||
![]() |
9a6059eb71 | ||
![]() |
05a8b05773 | ||
![]() |
790dbd442b | ||
![]() |
daf156a76a | ||
![]() |
154ca4d9e8 | ||
![]() |
ebd8f0c74a | ||
![]() |
5d658f216c | ||
![]() |
6f9513d88c | ||
![]() |
d8be8f1e06 | ||
![]() |
b91ef3f1ff | ||
![]() |
e2bce1b9ee | ||
![]() |
ebdd946ac1 | ||
![]() |
2aa1e2615b | ||
![]() |
6c16733dfd | ||
![]() |
f0329bb4e6 | ||
![]() |
6d3a5260d3 | ||
![]() |
cf051e777a | ||
![]() |
cc7f99125a | ||
![]() |
65a7157383 | ||
![]() |
24f4e780f1 | ||
![]() |
ca1e5e10b6 | ||
![]() |
3b438e5c7c | ||
![]() |
7bb92dc7bd | ||
![]() |
e79dca644e | ||
![]() |
70fbad6623 | ||
![]() |
6fd5d5f2d5 | ||
![]() |
f1585af0f2 | ||
![]() |
5d58945718 | ||
![]() |
41c031a19e | ||
![]() |
f9dbcd2531 | ||
![]() |
c6fae0320e | ||
![]() |
e5cdae9c84 | ||
![]() |
507842b614 | ||
![]() |
263709da8c | ||
![]() |
80ed863aab | ||
![]() |
0ddb4441d7 | ||
![]() |
fc549c9462 | ||
![]() |
b9b32e5647 | ||
![]() |
a2e54eac64 | ||
![]() |
5644079707 | ||
![]() |
3e0c081bed | ||
![]() |
97f696b937 | ||
![]() |
af989aab4e | ||
![]() |
6024597028 | ||
![]() |
943b6ccfba | ||
![]() |
7b0b9539b1 | ||
![]() |
259caf942d | ||
![]() |
ba71141f0a | ||
![]() |
174f20dc4a | ||
![]() |
361fbbf58f | ||
![]() |
1f4c9d2806 | ||
![]() |
a5533344f9 | ||
![]() |
ddf35a60ad | ||
![]() |
a12a6edfd1 | ||
![]() |
ac5bc7c2f9 | ||
![]() |
3e4c1ea196 | ||
![]() |
8828c76f92 | ||
![]() |
55ff4e046e | ||
![]() |
7ea558db99 | ||
![]() |
71e8068d86 | ||
![]() |
a45d21e2b9 | ||
![]() |
97eb81564a | ||
![]() |
262e85c37f | ||
![]() |
f451001f75 | ||
![]() |
5980daea64 | ||
![]() |
6b4f16f77b | ||
![]() |
64d628bd75 | ||
![]() |
6eece31dc3 | ||
![]() |
8206cd19c8 | ||
![]() |
a99b824508 | ||
![]() |
424740f62c | ||
![]() |
e5a08e6808 | ||
![]() |
cb16add8c8 | ||
![]() |
bc1aecfb15 | ||
![]() |
61aaddac28 | ||
![]() |
a5f2d0cf56 | ||
![]() |
00f63d72fa | ||
![]() |
12089054d8 | ||
![]() |
f6e8d92590 | ||
![]() |
a8032c932c | ||
![]() |
8a7ae17d4d | ||
![]() |
0ca9355bc0 | ||
![]() |
b10d7ccdda | ||
![]() |
1e68fbca90 | ||
![]() |
0cf1737289 | ||
![]() |
fac1d9fea1 | ||
![]() |
48e3832322 | ||
![]() |
61e1f4a916 | ||
![]() |
7642e05eed | ||
![]() |
51fad2eecb | ||
![]() |
111490b8be | ||
![]() |
8861421cd6 | ||
![]() |
c83b529c47 | ||
![]() |
d15e693045 | ||
![]() |
4fcedb4bae | ||
![]() |
a0f2dfbc19 | ||
![]() |
0aadfe32bb | ||
![]() |
dab3e549af | ||
![]() |
5c238ea359 | ||
![]() |
2c85d2468a | ||
![]() |
7bbf75237d | ||
![]() |
dd90e1926b | ||
![]() |
d19f706d50 | ||
![]() |
8eff4e0e5c | ||
![]() |
45d05eb691 | ||
![]() |
9c70794886 | ||
![]() |
6fbfccc2d3 | ||
![]() |
1931beab8e | ||
![]() |
2296fdf668 | ||
![]() |
89d216ca76 | ||
![]() |
5cffd40002 | ||
![]() |
e24dd5a162 | ||
![]() |
2063bf5de4 | ||
![]() |
36c4475ad9 | ||
![]() |
dc5d3fc473 | ||
![]() |
05077eaa20 | ||
![]() |
908d097904 | ||
![]() |
828c8bc1e8 | ||
![]() |
b8f409723d | ||
![]() |
8a8f5f3986 | ||
![]() |
7de53a51b8 | ||
![]() |
9649a9c62b | ||
![]() |
354c2c38cc | ||
![]() |
ff9ef08f65 | ||
![]() |
311b27ced8 | ||
![]() |
43b36ad2b0 | ||
![]() |
2e55209b34 | ||
![]() |
e7db5febcf | ||
![]() |
7739aa685c | ||
![]() |
5988d825b7 | ||
![]() |
a8efaee03c | ||
![]() |
8672cef972 | ||
![]() |
551dfee707 | ||
![]() |
1b8ca32e7d | ||
![]() |
489af2a670 | ||
![]() |
97df01b9b8 | ||
![]() |
68f7abcff1 | ||
![]() |
ceb45d9816 | ||
![]() |
5cca6e66be | ||
![]() |
c9097994b9 | ||
![]() |
c636ad51a8 | ||
![]() |
88174cd0a4 | ||
![]() |
b7d014b685 | ||
![]() |
56f28c9bd5 | ||
![]() |
7462471c6b | ||
![]() |
74d3f92cc7 | ||
![]() |
80f24584a5 | ||
![]() |
8e00158c34 | ||
![]() |
36b5580c1c | ||
![]() |
19f487750e | ||
![]() |
f1407afd1f | ||
![]() |
4401265e36 | ||
![]() |
5fd984ba6f | ||
![]() |
506e07127f | ||
![]() |
720609f8ba | ||
![]() |
a23e7bfb82 | ||
![]() |
f66624f5bf | ||
![]() |
d3f9c05312 | ||
![]() |
6283915f86 | ||
![]() |
2d250a9135 | ||
![]() |
33c670dd7a | ||
![]() |
849c441455 | ||
![]() |
b5b5c1fe8e | ||
![]() |
1d392a36f9 | ||
![]() |
049186371f | ||
![]() |
910f64ce47 | ||
![]() |
b3b71e78cd | ||
![]() |
f2e2e5f5ab | ||
![]() |
ecd03b4fc6 | ||
![]() |
3f5e2160de | ||
![]() |
283225f15f | ||
![]() |
400ae55940 | ||
![]() |
84c79f1456 | ||
![]() |
0b19f6cf5a | ||
![]() |
fbecc9db66 | ||
![]() |
ad48751adb | ||
![]() |
86390b453d | ||
![]() |
fa35e72214 | ||
![]() |
05571286b2 | ||
![]() |
4ee3c9c8b9 | ||
![]() |
18e9d71d7a | ||
![]() |
853a686994 | ||
![]() |
a164789321 | ||
![]() |
09fd599057 | ||
![]() |
fb815abca5 | ||
![]() |
71632a8197 | ||
![]() |
85639f5159 | ||
![]() |
c13725b5d0 | ||
![]() |
89712f6640 | ||
![]() |
9dedba6dfc | ||
![]() |
8c8a066c0e | ||
![]() |
041c0705e4 | ||
![]() |
56113a8da7 | ||
![]() |
73c9780321 | ||
![]() |
88f59fc2d6 | ||
![]() |
03be64a094 | ||
![]() |
a48baf6f3a | ||
![]() |
a376323331 | ||
![]() |
e622135e7e | ||
![]() |
d8ea178e69 | ||
![]() |
ad2585af67 | ||
![]() |
f4bdfea1c9 | ||
![]() |
d2f7c5a9c6 | ||
![]() |
068d5b95c3 | ||
![]() |
d4db5a364e | ||
![]() |
f3af264674 | ||
![]() |
4266dca1b6 | ||
![]() |
d407abb50f | ||
![]() |
3faeddcd5f | ||
![]() |
1c775feecc | ||
![]() |
b3bfb5ed44 | ||
![]() |
db77919550 | ||
![]() |
7b423a0915 | ||
![]() |
a639454f28 | ||
![]() |
ae1cb889dd | ||
![]() |
6a97833337 | ||
![]() |
8d5e188218 | ||
![]() |
98c73eeca9 | ||
![]() |
a9be986782 | ||
![]() |
62c4a5e9a0 | ||
![]() |
7448a15f72 | ||
![]() |
bb50d86e68 | ||
![]() |
76d56e24d6 | ||
![]() |
d4b28cea6c | ||
![]() |
ebc15b8680 |
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -28,13 +28,15 @@ Checklist
|
|||||||
You do not need to check all the boxes below all at once. Feel free to take
|
You do not need to check all the boxes below all at once. Feel free to take
|
||||||
your time and add more commits. If you're done and ready for review, please
|
your time and add more commits. If you're done and ready for review, please
|
||||||
check the last box. Enable a checkbox by replacing [ ] with [x].
|
check the last box. Enable a checkbox by replacing [ ] with [x].
|
||||||
|
|
||||||
|
Please always follow these steps:
|
||||||
|
- Read the [contribution guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches).
|
||||||
|
- Enable [maintainer edits](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
|
||||||
|
- Run `gofmt` on the code in all commits.
|
||||||
|
- Format all commit messages in the same style as [the other commits in the repository](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I have read the [contribution guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches).
|
|
||||||
- [ ] I have [enabled maintainer edits](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
|
|
||||||
- [ ] I have added tests for all code changes.
|
- [ ] I have added tests for all code changes.
|
||||||
- [ ] I have added documentation for relevant changes (in the manual).
|
- [ ] I have added documentation for relevant changes (in the manual).
|
||||||
- [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/restic/blob/master/changelog/TEMPLATE)).
|
- [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/restic/blob/master/changelog/TEMPLATE)).
|
||||||
- [ ] I have run `gofmt` on the code in all commits.
|
|
||||||
- [ ] All commit messages are formatted in the same style as [the other commits in the repo](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
|
|
||||||
- [ ] I'm done! This pull request is ready for review.
|
- [ ] I'm done! This pull request is ready for review.
|
||||||
|
31
.github/workflows/docker.yml
vendored
31
.github/workflows/docker.yml
vendored
@@ -20,12 +20,16 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
image: ${{ steps.image.outputs.image }}
|
||||||
|
digest: ${{ steps.build-and-push.outputs.digest }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -37,6 +41,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
|
type=sha
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
@@ -55,6 +60,7 @@ jobs:
|
|||||||
if: github.ref != 'refs/heads/master'
|
if: github.ref != 'refs/heads/master'
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
|
id: build-and-push
|
||||||
uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1
|
uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
@@ -64,3 +70,26 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Output image
|
||||||
|
id: image
|
||||||
|
run: |
|
||||||
|
# NOTE: Set the image as an output because the `env` context is not
|
||||||
|
# available to the inputs of a reusable workflow call.
|
||||||
|
image_name="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
echo "image=$image_name" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
provenance:
|
||||||
|
needs: [build-and-push-image]
|
||||||
|
permissions:
|
||||||
|
actions: read # for detecting the Github Actions environment.
|
||||||
|
id-token: write # for creating OIDC tokens for signing.
|
||||||
|
packages: write # for uploading attestations.
|
||||||
|
if: github.repository == 'restic/restic'
|
||||||
|
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||||
|
with:
|
||||||
|
image: ${{ needs.build-and-push-image.outputs.image }}
|
||||||
|
digest: ${{ needs.build-and-push-image.outputs.digest }}
|
||||||
|
registry-username: ${{ github.actor }}
|
||||||
|
secrets:
|
||||||
|
registry-password: ${{ secrets.GITHUB_TOKEN }}
|
45
.github/workflows/tests.yml
vendored
45
.github/workflows/tests.yml
vendored
@@ -13,7 +13,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
latest_go: "1.22.x"
|
latest_go: "1.24.x"
|
||||||
GO111MODULE: on
|
GO111MODULE: on
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -23,39 +23,29 @@ jobs:
|
|||||||
# list of jobs to run:
|
# list of jobs to run:
|
||||||
include:
|
include:
|
||||||
- job_name: Windows
|
- job_name: Windows
|
||||||
go: 1.22.x
|
go: 1.24.x
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
|
|
||||||
- job_name: macOS
|
- job_name: macOS
|
||||||
go: 1.22.x
|
go: 1.24.x
|
||||||
os: macOS-latest
|
os: macOS-latest
|
||||||
test_fuse: false
|
test_fuse: false
|
||||||
|
|
||||||
- job_name: Linux
|
- job_name: Linux
|
||||||
go: 1.22.x
|
go: 1.24.x
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
test_cloud_backends: true
|
test_cloud_backends: true
|
||||||
test_fuse: true
|
test_fuse: true
|
||||||
check_changelog: true
|
check_changelog: true
|
||||||
|
|
||||||
- job_name: Linux (race)
|
- job_name: Linux (race)
|
||||||
go: 1.22.x
|
go: 1.24.x
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
test_fuse: true
|
test_fuse: true
|
||||||
test_opts: "-race"
|
test_opts: "-race"
|
||||||
|
|
||||||
- job_name: Linux
|
- job_name: Linux
|
||||||
go: 1.21.x
|
go: 1.23.x
|
||||||
os: ubuntu-latest
|
|
||||||
test_fuse: true
|
|
||||||
|
|
||||||
- job_name: Linux
|
|
||||||
go: 1.20.x
|
|
||||||
os: ubuntu-latest
|
|
||||||
test_fuse: true
|
|
||||||
|
|
||||||
- job_name: Linux
|
|
||||||
go: 1.19.x
|
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
test_fuse: true
|
test_fuse: true
|
||||||
|
|
||||||
@@ -66,6 +56,9 @@ jobs:
|
|||||||
GOPROXY: https://proxy.golang.org
|
GOPROXY: https://proxy.golang.org
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Go ${{ matrix.go }}
|
- name: Set up Go ${{ matrix.go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
@@ -139,9 +132,6 @@ jobs:
|
|||||||
echo $Env:USERPROFILE\tar\bin >> $Env:GITHUB_PATH
|
echo $Env:USERPROFILE\tar\bin >> $Env:GITHUB_PATH
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build with build.go
|
- name: Build with build.go
|
||||||
run: |
|
run: |
|
||||||
go run build.go
|
go run build.go
|
||||||
@@ -195,7 +185,7 @@ jobs:
|
|||||||
# prepare credentials for Google Cloud Storage tests in a temp file
|
# prepare credentials for Google Cloud Storage tests in a temp file
|
||||||
export GOOGLE_APPLICATION_CREDENTIALS=$(mktemp --tmpdir restic-gcs-auth-XXXXXXX)
|
export GOOGLE_APPLICATION_CREDENTIALS=$(mktemp --tmpdir restic-gcs-auth-XXXXXXX)
|
||||||
echo $RESTIC_TEST_GS_APPLICATION_CREDENTIALS_B64 | base64 -d > $GOOGLE_APPLICATION_CREDENTIALS
|
echo $RESTIC_TEST_GS_APPLICATION_CREDENTIALS_B64 | base64 -d > $GOOGLE_APPLICATION_CREDENTIALS
|
||||||
go test -cover -parallel 4 ./internal/backend/...
|
go test -cover -parallel 5 -timeout 15m ./internal/backend/...
|
||||||
|
|
||||||
# only run cloud backend tests for pull requests from and pushes to our
|
# only run cloud backend tests for pull requests from and pushes to our
|
||||||
# own repo, otherwise the secrets are not available
|
# own repo, otherwise the secrets are not available
|
||||||
@@ -214,7 +204,6 @@ jobs:
|
|||||||
|
|
||||||
cross_compile:
|
cross_compile:
|
||||||
strategy:
|
strategy:
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
# run cross-compile in three batches parallel so the overall tests run faster
|
# run cross-compile in three batches parallel so the overall tests run faster
|
||||||
subset:
|
subset:
|
||||||
@@ -230,14 +219,14 @@ jobs:
|
|||||||
name: Cross Compile for subset ${{ matrix.subset }}
|
name: Cross Compile for subset ${{ matrix.subset }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Go ${{ env.latest_go }}
|
- name: Set up Go ${{ env.latest_go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.latest_go }}
|
go-version: ${{ env.latest_go }}
|
||||||
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Cross-compile for subset ${{ matrix.subset }}
|
- name: Cross-compile for subset ${{ matrix.subset }}
|
||||||
run: |
|
run: |
|
||||||
mkdir build-output build-output-debug
|
mkdir build-output build-output-debug
|
||||||
@@ -252,19 +241,19 @@ jobs:
|
|||||||
# allow annotating code in the PR
|
# allow annotating code in the PR
|
||||||
checks: write
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Go ${{ env.latest_go }}
|
- name: Set up Go ${{ env.latest_go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.latest_go }}
|
go-version: ${{ env.latest_go }}
|
||||||
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||||
version: v1.57.1
|
version: v1.64.8
|
||||||
args: --verbose --timeout 5m
|
args: --verbose --timeout 5m
|
||||||
|
|
||||||
# only run golangci-lint for pull requests, otherwise ALL hints get
|
# only run golangci-lint for pull requests, otherwise ALL hints get
|
||||||
|
@@ -56,6 +56,7 @@ issues:
|
|||||||
# staticcheck: there's no easy way to replace these packages
|
# staticcheck: there's no easy way to replace these packages
|
||||||
- "SA1019: \"golang.org/x/crypto/poly1305\" is deprecated"
|
- "SA1019: \"golang.org/x/crypto/poly1305\" is deprecated"
|
||||||
- "SA1019: \"golang.org/x/crypto/openpgp\" is deprecated"
|
- "SA1019: \"golang.org/x/crypto/openpgp\" is deprecated"
|
||||||
|
- "redefines-builtin-id:"
|
||||||
|
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
# revive: ignore unused parameters in tests
|
# revive: ignore unused parameters in tests
|
||||||
|
761
CHANGELOG.md
761
CHANGELOG.md
@@ -1,5 +1,9 @@
|
|||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
|
* [Changelog for 0.18.0](#changelog-for-restic-0180-2025-03-27)
|
||||||
|
* [Changelog for 0.17.3](#changelog-for-restic-0173-2024-11-08)
|
||||||
|
* [Changelog for 0.17.2](#changelog-for-restic-0172-2024-10-27)
|
||||||
|
* [Changelog for 0.17.1](#changelog-for-restic-0171-2024-09-05)
|
||||||
* [Changelog for 0.17.0](#changelog-for-restic-0170-2024-07-26)
|
* [Changelog for 0.17.0](#changelog-for-restic-0170-2024-07-26)
|
||||||
* [Changelog for 0.16.5](#changelog-for-restic-0165-2024-07-01)
|
* [Changelog for 0.16.5](#changelog-for-restic-0165-2024-07-01)
|
||||||
* [Changelog for 0.16.4](#changelog-for-restic-0164-2024-02-04)
|
* [Changelog for 0.16.4](#changelog-for-restic-0164-2024-02-04)
|
||||||
@@ -35,6 +39,763 @@
|
|||||||
* [Changelog for 0.6.0](#changelog-for-restic-060-2017-05-29)
|
* [Changelog for 0.6.0](#changelog-for-restic-060-2017-05-29)
|
||||||
|
|
||||||
|
|
||||||
|
# Changelog for restic 0.18.0 (2025-03-27)
|
||||||
|
The following sections list the changes in restic 0.18.0 relevant to
|
||||||
|
restic users. The changes are ordered by importance.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
* Sec #5291: Mitigate attack on content-defined chunking algorithm
|
||||||
|
* Fix #1843: Correctly restore long filepaths' timestamp on old Windows
|
||||||
|
* Fix #2165: Ignore disappeared backup source files
|
||||||
|
* Fix #5153: Include root tree when searching using `find --tree`
|
||||||
|
* Fix #5169: Prevent Windows VSS event log 8194 warnings for backup with fs snapshot
|
||||||
|
* Fix #5212: Fix duplicate data handling in `prune --max-unused`
|
||||||
|
* Fix #5249: Fix creation of oversized index by `repair index --read-all-packs`
|
||||||
|
* Fix #5259: Fix rare crash in command output
|
||||||
|
* Chg #4938: Update dependencies and require Go 1.23 or newer
|
||||||
|
* Chg #5162: Graduate feature flags
|
||||||
|
* Enh #1378: Add JSON support to `check` command
|
||||||
|
* Enh #2511: Support generating shell completions to stdout
|
||||||
|
* Enh #3697: Allow excluding online-only cloud files (e.g. OneDrive)
|
||||||
|
* Enh #4179: Add `sort` option to `ls` command
|
||||||
|
* Enh #4433: Change default sort order for `find` output
|
||||||
|
* Enh #4521: Add support for Microsoft Blob Storage access tiers
|
||||||
|
* Enh #4942: Add snapshot summary statistics to rewritten snapshots
|
||||||
|
* Enh #4948: Format exit errors as JSON when requested
|
||||||
|
* Enh #4983: Add SLSA provenance to GHCR container images
|
||||||
|
* Enh #5054: Enable compression for ZIP archives in `dump` command
|
||||||
|
* Enh #5081: Add retry mechanism for loading repository config
|
||||||
|
* Enh #5089: Allow including/excluding extended file attributes during `restore`
|
||||||
|
* Enh #5092: Show count of deleted files and directories during `restore`
|
||||||
|
* Enh #5109: Make small pack size configurable for `prune`
|
||||||
|
* Enh #5119: Add start and end timestamps to `backup` JSON output
|
||||||
|
* Enh #5131: Add DragonFlyBSD support
|
||||||
|
* Enh #5137: Make `tag` command print which snapshots were modified
|
||||||
|
* Enh #5141: Provide clear error message if AZURE_ACCOUNT_NAME is not set
|
||||||
|
* Enh #5173: Add experimental S3 cold storage support
|
||||||
|
* Enh #5174: Add xattr support for NetBSD 10+
|
||||||
|
* Enh #5251: Improve retry handling for flaky `rclone` backends
|
||||||
|
* Enh #5287: Make `recover` automatically rebuild index when needed
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
|
* Security #5291: Mitigate attack on content-defined chunking algorithm
|
||||||
|
|
||||||
|
Restic uses [Rabin
|
||||||
|
Fingerprints](https://restic.net/blog/2015-09-12/restic-foundation1-cdc/) for
|
||||||
|
its content-defined chunker. The algorithm relies on a secret polynomial to
|
||||||
|
split files into chunks.
|
||||||
|
|
||||||
|
As shown in the paper "[Chunking Attacks on File Backup Services using
|
||||||
|
Content-Defined Chunking](https://eprint.iacr.org/2025/532.pdf)" by Boris
|
||||||
|
Alexeev, Colin Percival and Yan X Zhang, an attacker that can observe chunk
|
||||||
|
sizes for a known file can derive the secret polynomial. Knowledge of the
|
||||||
|
polynomial might in some cases allow an attacker to check whether certain large
|
||||||
|
files are stored in a repository.
|
||||||
|
|
||||||
|
A practical attack is nevertheless hard as restic merges multiple chunks into
|
||||||
|
opaque pack files and by default processes multiple files in parallel. This
|
||||||
|
likely prevents an attacker from matching pack files to the attacker-known file
|
||||||
|
and thereby prevents the attack.
|
||||||
|
|
||||||
|
Despite the low chances of a practical attack, restic now has added mitigation
|
||||||
|
that randomizes how chunks are assembled into pack files. This prevents
|
||||||
|
attackers from guessing which chunks are part of a pack file and thereby
|
||||||
|
prevents learning the chunk sizes.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5291
|
||||||
|
https://github.com/restic/restic/pull/5295
|
||||||
|
|
||||||
|
* Bugfix #1843: Correctly restore long filepaths' timestamp on old Windows
|
||||||
|
|
||||||
|
The `restore` command now correctly restores timestamps for files with paths
|
||||||
|
longer than 256 characters on Windows versions prior to Windows 10 1607.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/1843
|
||||||
|
https://github.com/restic/restic/pull/5061
|
||||||
|
|
||||||
|
* Bugfix #2165: Ignore disappeared backup source files
|
||||||
|
|
||||||
|
The `backup` command now quietly skips files that are removed between directory
|
||||||
|
listing and backup, instead of printing errors like:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lstat /some/file/name: no such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2165
|
||||||
|
https://github.com/restic/restic/issues/3098
|
||||||
|
https://github.com/restic/restic/pull/5143
|
||||||
|
https://github.com/restic/restic/pull/5145
|
||||||
|
|
||||||
|
* Bugfix #5153: Include root tree when searching using `find --tree`
|
||||||
|
|
||||||
|
The `restic find --tree` command did not find trees referenced by `restic
|
||||||
|
snapshot --json`. It now correctly includes the root tree when searching.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5153
|
||||||
|
|
||||||
|
* Bugfix #5169: Prevent Windows VSS event log 8194 warnings for backup with fs snapshot
|
||||||
|
|
||||||
|
When running `backup` with the `--use-fs-snapshot` option in Windows with admin
|
||||||
|
rights, event logs like
|
||||||
|
|
||||||
|
```
|
||||||
|
Volume Shadow Copy Service error: Unexpected error querying for the IVssWriterCallback interface. hr = 0x80070005, Access is denied.
|
||||||
|
. This is often caused by incorrect security settings in either the writer or requester process.
|
||||||
|
|
||||||
|
Operation:
|
||||||
|
Gathering Writer Data
|
||||||
|
|
||||||
|
Context:
|
||||||
|
Writer Class Id: {e8132975-6f93-4464-a53e-1050253ae220}
|
||||||
|
Writer Name: System Writer
|
||||||
|
Writer Instance ID: {54b151ac-d27d-4628-9cb0-2bc40959f50f}
|
||||||
|
```
|
||||||
|
|
||||||
|
Are created several times even though the backup itself succeeds. This has now
|
||||||
|
been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5169
|
||||||
|
https://github.com/restic/restic/pull/5170
|
||||||
|
https://forum.restic.net/t/windows-shadow-copy-snapshot-vss-unexpected-provider-error/3674/2
|
||||||
|
|
||||||
|
* Bugfix #5212: Fix duplicate data handling in `prune --max-unused`
|
||||||
|
|
||||||
|
The `prune --max-unused size` command did not correctly account for duplicate
|
||||||
|
data. If a repository contained a large amount of duplicate data, this could
|
||||||
|
previously result in pruning too little data. This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5212
|
||||||
|
https://forum.restic.net/t/restic-not-obeying-max-unused-parameter-on-prune/8879
|
||||||
|
|
||||||
|
* Bugfix #5249: Fix creation of oversized index by `repair index --read-all-packs`
|
||||||
|
|
||||||
|
Since restic 0.17.0, the new index created by `repair index --read-all-packs`
|
||||||
|
was written as a single large index. This significantly increased memory usage
|
||||||
|
while loading the index.
|
||||||
|
|
||||||
|
The index is now correctly split into multiple smaller indexes, and `repair
|
||||||
|
index` now also automatically splits oversized indexes.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5249
|
||||||
|
|
||||||
|
* Bugfix #5259: Fix rare crash in command output
|
||||||
|
|
||||||
|
Some commands could in rare cases crash when trying to print status messages and
|
||||||
|
request retries at the same time, resulting in an error like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
panic: runtime error: slice bounds out of range [468:156]
|
||||||
|
[...]
|
||||||
|
github.com/restic/restic/internal/ui/termstatus.(*lineWriter).Write(...)
|
||||||
|
/restic/internal/ui/termstatus/stdio_wrapper.go:36 +0x136
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5259
|
||||||
|
https://github.com/restic/restic/pull/5300
|
||||||
|
|
||||||
|
* Change #4938: Update dependencies and require Go 1.23 or newer
|
||||||
|
|
||||||
|
We have updated all dependencies. Restic now requires Go 1.23 or newer to build.
|
||||||
|
|
||||||
|
This also disables support for TLS versions older than TLS 1.2. On Windows,
|
||||||
|
restic now requires at least Windows 10 or Windows Server 2016. On macOS, restic
|
||||||
|
now requires at least macOS 11 Big Sur.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4938
|
||||||
|
|
||||||
|
* Change #5162: Graduate feature flags
|
||||||
|
|
||||||
|
The `deprecate-legacy-index`, `deprecate-s3-legacy-layout`,
|
||||||
|
`explicit-s3-anonymous-auth` and `safe-forget-keep-tags` features are now stable
|
||||||
|
and can no longer be disabled. The corresponding feature flags will be removed
|
||||||
|
in restic 0.19.0.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5162
|
||||||
|
|
||||||
|
* Enhancement #1378: Add JSON support to `check` command
|
||||||
|
|
||||||
|
The `check` command now supports the `--json` option to output all statistics in
|
||||||
|
JSON format.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/1378
|
||||||
|
https://github.com/restic/restic/pull/5194
|
||||||
|
|
||||||
|
* Enhancement #2511: Support generating shell completions to stdout
|
||||||
|
|
||||||
|
The `generate` command now supports using `-` as the filename with the
|
||||||
|
`--[shell]-completion` option to write the generated output to stdout.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2511
|
||||||
|
https://github.com/restic/restic/pull/5053
|
||||||
|
|
||||||
|
* Enhancement #3697: Allow excluding online-only cloud files (e.g. OneDrive)
|
||||||
|
|
||||||
|
Restic treated files synced using OneDrive Files On-Demand as though they were
|
||||||
|
regular files. This caused issues with VSS and could cause OneDrive to download
|
||||||
|
all files.
|
||||||
|
|
||||||
|
Restic now allows the user to exclude these files when backing up with the
|
||||||
|
`--exclude-cloud-files` option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3697
|
||||||
|
https://github.com/restic/restic/issues/4935
|
||||||
|
https://github.com/restic/restic/pull/4990
|
||||||
|
|
||||||
|
* Enhancement #4179: Add `sort` option to `ls` command
|
||||||
|
|
||||||
|
The `ls -l` command output can now be sorted using the new `--sort <field>`
|
||||||
|
option for the fields `name`, `size`, `time` (same as `mtime`), `mtime`,
|
||||||
|
`atime`, `ctime` and `extension`. A `--reverse` option is also available.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4179
|
||||||
|
https://github.com/restic/restic/pull/5182
|
||||||
|
|
||||||
|
* Enhancement #4433: Change default sort order for `find` output
|
||||||
|
|
||||||
|
The `find` command now sorts snapshots from newest to oldest by default. The
|
||||||
|
previous oldest-to-newest order can be restored using the new `--reverse`
|
||||||
|
option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4433
|
||||||
|
https://github.com/restic/restic/pull/5184
|
||||||
|
|
||||||
|
* Enhancement #4521: Add support for Microsoft Blob Storage access tiers
|
||||||
|
|
||||||
|
The new `-o azure.access-tier=<tier>` option allows specifying the access tier
|
||||||
|
(`Hot`, `Cool` or `Cold`) for objects created in Microsoft Blob Storage. If
|
||||||
|
unspecified, the storage account's default tier is used.
|
||||||
|
|
||||||
|
There is no official `Archive` storage support in restic, use this option at
|
||||||
|
your own risk. To restore any data, it is necessary to manually warm up the
|
||||||
|
required data in the `Archive` tier.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4521
|
||||||
|
https://github.com/restic/restic/pull/5046
|
||||||
|
|
||||||
|
* Enhancement #4942: Add snapshot summary statistics to rewritten snapshots
|
||||||
|
|
||||||
|
The `rewrite` command now supports a `--snapshot-summary` option to add
|
||||||
|
statistics data to snapshots. Only two fields in the summary will be non-zero:
|
||||||
|
`TotalFilesProcessed` and `TotalBytesProcessed`.
|
||||||
|
|
||||||
|
For snapshots rewritten using the `--exclude` options, the summary statistics
|
||||||
|
are updated accordingly.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4942
|
||||||
|
https://github.com/restic/restic/pull/5185
|
||||||
|
|
||||||
|
* Enhancement #4948: Format exit errors as JSON when requested
|
||||||
|
|
||||||
|
Restic now formats error messages as JSON when the `--json` flag is used.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4948
|
||||||
|
https://github.com/restic/restic/pull/4952
|
||||||
|
|
||||||
|
* Enhancement #4983: Add SLSA provenance to GHCR container images
|
||||||
|
|
||||||
|
Restic's GitHub Container Registry (GHCR) image build workflow now includes SLSA
|
||||||
|
(Supply-chain Levels for Software Artifacts) provenance generation.
|
||||||
|
|
||||||
|
Please see the restic documentation for more information about verifying SLSA
|
||||||
|
provenance.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4983
|
||||||
|
https://github.com/restic/restic/pull/4999
|
||||||
|
|
||||||
|
* Enhancement #5054: Enable compression for ZIP archives in `dump` command
|
||||||
|
|
||||||
|
The `dump` command now compresses ZIP archives using the DEFLATE algorithm,
|
||||||
|
reducing the size of exported archives.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5054
|
||||||
|
|
||||||
|
* Enhancement #5081: Add retry mechanism for loading repository config
|
||||||
|
|
||||||
|
Restic now retries loading the repository config file when opening a repository.
|
||||||
|
The `init` command now also retries backend operations.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5081
|
||||||
|
https://github.com/restic/restic/pull/5095
|
||||||
|
|
||||||
|
* Enhancement #5089: Allow including/excluding extended file attributes during `restore`
|
||||||
|
|
||||||
|
The `restore` command now supports the `--exclude-xattr` and `--include-xattr`
|
||||||
|
options to control which extended file attributes will be restored. By default,
|
||||||
|
all attributes are restored.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5089
|
||||||
|
https://github.com/restic/restic/pull/5129
|
||||||
|
|
||||||
|
* Enhancement #5092: Show count of deleted files and directories during `restore`
|
||||||
|
|
||||||
|
The `restore` command now reports the number of deleted files and directories,
|
||||||
|
both in the regular output and in the `files_deleted` field of the JSON output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5092
|
||||||
|
https://github.com/restic/restic/pull/5100
|
||||||
|
|
||||||
|
* Enhancement #5109: Make small pack size configurable for `prune`
|
||||||
|
|
||||||
|
The `prune` command now supports the `--repack-smaller-than` option that allows
|
||||||
|
repacking pack files smaller than a specified size.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5109
|
||||||
|
https://github.com/restic/restic/pull/5183
|
||||||
|
|
||||||
|
* Enhancement #5119: Add start and end timestamps to `backup` JSON output
|
||||||
|
|
||||||
|
The JSON output of the `backup` command now includes `backup_start` and
|
||||||
|
`backup_end` timestamps, containing the start and end time of the backup.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5119
|
||||||
|
|
||||||
|
* Enhancement #5131: Add DragonFlyBSD support
|
||||||
|
|
||||||
|
Restic can now be compiled on DragonflyBSD.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5131
|
||||||
|
https://github.com/restic/restic/pull/5138
|
||||||
|
|
||||||
|
* Enhancement #5137: Make `tag` command print which snapshots were modified
|
||||||
|
|
||||||
|
The `tag` command now outputs which snapshots were modified along with their new
|
||||||
|
snapshot ID. The command supports the `--json` option for machine-readable
|
||||||
|
output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5137
|
||||||
|
https://github.com/restic/restic/pull/5144
|
||||||
|
|
||||||
|
* Enhancement #5141: Provide clear error message if AZURE_ACCOUNT_NAME is not set
|
||||||
|
|
||||||
|
If `AZURE_ACCOUNT_NAME` was not set, commands related to an Azure repository
|
||||||
|
would result in a misleading networking error. Restic now detect this and
|
||||||
|
provides a clear warning that the variable is not defined.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5141
|
||||||
|
|
||||||
|
* Enhancement #5173: Add experimental S3 cold storage support
|
||||||
|
|
||||||
|
Introduce S3 backend options for transitioning pack files from cold to hot
|
||||||
|
storage on S3 and S3-compatible providers. Note: this only works for the
|
||||||
|
`prune`, `copy` and `restore` commands for now.
|
||||||
|
|
||||||
|
This experimental feature is gated behind the "s3-restore" feature flag.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3202
|
||||||
|
https://github.com/restic/restic/issues/2504
|
||||||
|
https://github.com/restic/restic/pull/5173
|
||||||
|
|
||||||
|
* Enhancement #5174: Add xattr support for NetBSD 10+
|
||||||
|
|
||||||
|
Extended attribute support for `backup` and `restore` operations is now
|
||||||
|
available on NetBSD version 10 and later.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5174
|
||||||
|
https://github.com/restic/restic/pull/5180
|
||||||
|
|
||||||
|
* Enhancement #5251: Improve retry handling for flaky `rclone` backends
|
||||||
|
|
||||||
|
Since restic 0.17.0, the backend retry mechanisms rely on backends correctly
|
||||||
|
reporting when a file does not exist. This is not always the case for some
|
||||||
|
`rclone` backends, which caused restic to stop retrying after the first failure.
|
||||||
|
|
||||||
|
For rclone, failed requests are now retried up to 5 times before giving up.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5251
|
||||||
|
|
||||||
|
* Enhancement #5287: Make `recover` automatically rebuild index when needed
|
||||||
|
|
||||||
|
When trying to recover data from an interrupted snapshot, it was previously
|
||||||
|
necessary to manually run `repair index` before runnning `recover`. This now
|
||||||
|
happens automatically so that only `recover` is necessary.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5287
|
||||||
|
https://github.com/restic/restic/pull/5296
|
||||||
|
|
||||||
|
|
||||||
|
# Changelog for restic 0.17.3 (2024-11-08)
|
||||||
|
The following sections list the changes in restic 0.17.3 relevant to
|
||||||
|
restic users. The changes are ordered by importance.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
* Fix #4971: Fix unusable `mount` on macOS Sonoma
|
||||||
|
* Fix #5003: Fix metadata errors during backup of removable disks on Windows
|
||||||
|
* Fix #5101: Do not retry load/list operation if SFTP connection is broken
|
||||||
|
* Fix #5107: Fix metadata error on Windows for backups using VSS
|
||||||
|
* Enh #5096: Allow `prune --dry-run` without lock
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
|
* Bugfix #4971: Fix unusable `mount` on macOS Sonoma
|
||||||
|
|
||||||
|
On macOS Sonoma when using FUSE-T, it was not possible to access files in a
|
||||||
|
mounted repository. This issue is now resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4971
|
||||||
|
https://github.com/restic/restic/pull/5048
|
||||||
|
|
||||||
|
* Bugfix #5003: Fix metadata errors during backup of removable disks on Windows
|
||||||
|
|
||||||
|
Since restic 0.17.0, backing up removable disks on Windows could report errors
|
||||||
|
with retrieving metadata like shown below.
|
||||||
|
|
||||||
|
```
|
||||||
|
error: incomplete metadata for d:\filename: get named security info failed with: Access is denied.
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5003
|
||||||
|
https://github.com/restic/restic/pull/5123
|
||||||
|
https://forum.restic.net/t/backing-up-a-folder-from-a-veracrypt-volume-brings-up-errors-since-restic-v17-0/8444
|
||||||
|
|
||||||
|
* Bugfix #5101: Do not retry load/list operation if SFTP connection is broken
|
||||||
|
|
||||||
|
When using restic with the SFTP backend, backend operations that load a file or
|
||||||
|
list files were retried even if the SFTP connection was broken. This has now
|
||||||
|
been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5101
|
||||||
|
https://forum.restic.net/t/restic-hanging-on-backup/8559
|
||||||
|
|
||||||
|
* Bugfix #5107: Fix metadata error on Windows for backups using VSS
|
||||||
|
|
||||||
|
Since restic 0.17.2, when creating a backup on Windows using
|
||||||
|
`--use-fs-snapshot`, restic would report an error like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: incomplete metadata for C:\: get EA failed while opening file handle for path \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyXX\, with: The process cannot access the file because it is being used by another process.
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed by correctly handling paths that refer to volume shadow
|
||||||
|
copy snapshots.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5107
|
||||||
|
https://github.com/restic/restic/pull/5110
|
||||||
|
https://github.com/restic/restic/pull/5112
|
||||||
|
|
||||||
|
* Enhancement #5096: Allow `prune --dry-run` without lock
|
||||||
|
|
||||||
|
The `prune --dry-run --no-lock` now allows performing a dry-run without locking
|
||||||
|
the repository. Note that if the repository is modified concurrently, `prune`
|
||||||
|
may return inaccurate statistics or errors.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5096
|
||||||
|
|
||||||
|
|
||||||
|
# Changelog for restic 0.17.2 (2024-10-27)
|
||||||
|
The following sections list the changes in restic 0.17.2 relevant to
|
||||||
|
restic users. The changes are ordered by importance.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
* Fix #4004: Support container-level SAS/SAT tokens for Azure backend
|
||||||
|
* Fix #5047: Resolve potential error during concurrent cache cleanup
|
||||||
|
* Fix #5050: Return error if `tag` fails to lock repository
|
||||||
|
* Fix #5057: Exclude irregular files from backups
|
||||||
|
* Fix #5063: Correctly `backup` extended metadata when using VSS on Windows
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
|
* Bugfix #4004: Support container-level SAS/SAT tokens for Azure backend
|
||||||
|
|
||||||
|
Restic previously expected SAS/SAT tokens to be generated at the account level,
|
||||||
|
which prevented tokens created at the container level from being used to
|
||||||
|
initialize a repository. This caused an error when attempting to initialize a
|
||||||
|
repository with container-level tokens.
|
||||||
|
|
||||||
|
Restic now supports both account-level and container-level SAS/SAT tokens for
|
||||||
|
initializing a repository.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4004
|
||||||
|
https://github.com/restic/restic/pull/5093
|
||||||
|
|
||||||
|
* Bugfix #5047: Resolve potential error during concurrent cache cleanup
|
||||||
|
|
||||||
|
When multiple restic processes ran concurrently, they could compete to remove
|
||||||
|
obsolete snapshots from the local backend cache, sometimes leading to a "no such
|
||||||
|
file or directory" error. Restic now suppresses this error to prevent issues
|
||||||
|
during cache cleanup.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5047
|
||||||
|
|
||||||
|
* Bugfix #5050: Return error if `tag` fails to lock repository
|
||||||
|
|
||||||
|
Since restic 0.17.0, the `tag` command did not return an error when it failed to
|
||||||
|
open or lock the repository. This issue has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5050
|
||||||
|
https://github.com/restic/restic/pull/5056
|
||||||
|
|
||||||
|
* Bugfix #5057: Exclude irregular files from backups
|
||||||
|
|
||||||
|
Since restic 0.17.1, files with the type `irregular` could mistakenly be
|
||||||
|
included in snapshots, especially when backing up special file types on Windows
|
||||||
|
that restic cannot process. This issue has now been fixed.
|
||||||
|
|
||||||
|
Previously, this bug caused the `check` command to report errors like the
|
||||||
|
following one:
|
||||||
|
|
||||||
|
```
|
||||||
|
tree 12345678[...]: node "example.zip" with invalid type "irregular"
|
||||||
|
```
|
||||||
|
|
||||||
|
To repair affected snapshots, upgrade to restic 0.17.2 and run:
|
||||||
|
|
||||||
|
```
|
||||||
|
restic repair snapshots --forget
|
||||||
|
```
|
||||||
|
|
||||||
|
This will remove the `irregular` files from the snapshots (creating a new
|
||||||
|
snapshot ID for each of the affected snapshots).
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5057
|
||||||
|
https://forum.restic.net/t/errors-found-by-check-1-invalid-type-irregular-2-ciphertext-verification-failed/8447/2
|
||||||
|
|
||||||
|
* Bugfix #5063: Correctly `backup` extended metadata when using VSS on Windows
|
||||||
|
|
||||||
|
On Windows, when creating a backup with the `--use-fs-snapshot` option, restic
|
||||||
|
read extended metadata from the original filesystem path instead of from the
|
||||||
|
snapshot. This could result in errors if files were removed during the backup
|
||||||
|
process.
|
||||||
|
|
||||||
|
This issue has now been resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5063
|
||||||
|
https://github.com/restic/restic/pull/5097
|
||||||
|
https://github.com/restic/restic/pull/5099
|
||||||
|
|
||||||
|
|
||||||
|
# Changelog for restic 0.17.1 (2024-09-05)
|
||||||
|
The following sections list the changes in restic 0.17.1 relevant to
|
||||||
|
restic users. The changes are ordered by importance.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
* Fix #2004: Correctly handle volume names in `backup` command on Windows
|
||||||
|
* Fix #4945: Include missing backup error text with `--json`
|
||||||
|
* Fix #4953: Correctly handle long paths on older Windows versions
|
||||||
|
* Fix #4957: Fix delayed cancellation of certain commands
|
||||||
|
* Fix #4958: Don't ignore metadata-setting errors during restore
|
||||||
|
* Fix #4969: Correctly restore timestamp for files with resource forks on macOS
|
||||||
|
* Fix #4975: Prevent `backup --stdin-from-command` from panicking
|
||||||
|
* Fix #4980: Skip extended attribute processing on unsupported Windows volumes
|
||||||
|
* Fix #5004: Fix spurious "A Required Privilege Is Not Held by the Client" error
|
||||||
|
* Fix #5005: Fix rare failures to retry locking a repository
|
||||||
|
* Fix #5018: Improve HTTP/2 support for REST backend
|
||||||
|
* Chg #4953: Also back up files with incomplete metadata
|
||||||
|
* Enh #4795: Display progress bar for `restore --verify`
|
||||||
|
* Enh #4934: Automatically clear removed snapshots from cache
|
||||||
|
* Enh #4944: Print JSON-formatted errors during `restore --json`
|
||||||
|
* Enh #4959: Return exit code 12 for "bad password" errors
|
||||||
|
* Enh #4970: Make timeout for stuck requests customizable
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
|
* Bugfix #2004: Correctly handle volume names in `backup` command on Windows
|
||||||
|
|
||||||
|
On Windows, when the specified backup target only included the volume name
|
||||||
|
without a trailing slash, for example, `C:`, then restoring the resulting
|
||||||
|
snapshot would result in an error. Note that using `C:\` as backup target worked
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
Specifying volume names is now handled correctly. To restore snapshots created
|
||||||
|
before this bugfix, use the <snapshot>:<subpath> syntax. For example, to restore
|
||||||
|
a snapshot with ID `12345678` that backed up `C:`, use the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
restic restore 12345678:/C/C:./ --target output/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2004
|
||||||
|
https://github.com/restic/restic/pull/5028
|
||||||
|
|
||||||
|
* Bugfix #4945: Include missing backup error text with `--json`
|
||||||
|
|
||||||
|
Previously, when running a backup with the `--json` option, restic failed to
|
||||||
|
include the actual error message in the output, resulting in `"error": {}` being
|
||||||
|
displayed.
|
||||||
|
|
||||||
|
This has now been fixed, and restic now includes the error text in JSON output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4945
|
||||||
|
https://github.com/restic/restic/pull/4946
|
||||||
|
|
||||||
|
* Bugfix #4953: Correctly handle long paths on older Windows versions
|
||||||
|
|
||||||
|
On older Windows versions, like Windows Server 2012, restic 0.17.0 failed to
|
||||||
|
back up files with long paths. This problem has now been resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4953
|
||||||
|
https://github.com/restic/restic/pull/4954
|
||||||
|
|
||||||
|
* Bugfix #4957: Fix delayed cancellation of certain commands
|
||||||
|
|
||||||
|
Since restic 0.17.0, some commands did not immediately respond to cancellation
|
||||||
|
via Ctrl-C (SIGINT) and continued running for a short period. The most affected
|
||||||
|
commands were `diff`,`find`, `ls`, `stats` and `rewrite`. This is now resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4957
|
||||||
|
https://github.com/restic/restic/pull/4960
|
||||||
|
|
||||||
|
* Bugfix #4958: Don't ignore metadata-setting errors during restore
|
||||||
|
|
||||||
|
Previously, restic used to ignore errors when setting timestamps, attributes, or
|
||||||
|
file modes during a restore. It now reports those errors, except for permission
|
||||||
|
related errors when running without root privileges.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4958
|
||||||
|
|
||||||
|
* Bugfix #4969: Correctly restore timestamp for files with resource forks on macOS
|
||||||
|
|
||||||
|
On macOS, timestamps were not restored for files with resource forks. This has
|
||||||
|
now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4969
|
||||||
|
https://github.com/restic/restic/pull/5006
|
||||||
|
|
||||||
|
* Bugfix #4975: Prevent `backup --stdin-from-command` from panicking
|
||||||
|
|
||||||
|
Restic would previously crash if `--stdin-from-command` was specified without
|
||||||
|
providing a command. This issue has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4975
|
||||||
|
https://github.com/restic/restic/pull/4976
|
||||||
|
|
||||||
|
* Bugfix #4980: Skip extended attribute processing on unsupported Windows volumes
|
||||||
|
|
||||||
|
With restic 0.17.0, backups of certain Windows paths, such as network drives,
|
||||||
|
failed due to errors while fetching extended attributes.
|
||||||
|
|
||||||
|
Restic now skips extended attribute processing for volumes where they are not
|
||||||
|
supported.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4955
|
||||||
|
https://github.com/restic/restic/issues/4950
|
||||||
|
https://github.com/restic/restic/pull/4980
|
||||||
|
https://github.com/restic/restic/pull/4998
|
||||||
|
|
||||||
|
* Bugfix #5004: Fix spurious "A Required Privilege Is Not Held by the Client" error
|
||||||
|
|
||||||
|
On Windows, creating a backup could sometimes trigger the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: nodeFromFileInfo [...]: get named security info failed with: a required privilege is not held by the client.
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5004
|
||||||
|
https://github.com/restic/restic/pull/5019
|
||||||
|
|
||||||
|
* Bugfix #5005: Fix rare failures to retry locking a repository
|
||||||
|
|
||||||
|
Restic 0.17.0 could in rare cases fail to retry locking a repository if one of
|
||||||
|
the lock files failed to load, resulting in the error:
|
||||||
|
|
||||||
|
```
|
||||||
|
unable to create lock in backend: circuit breaker open for file <lock/1234567890>
|
||||||
|
```
|
||||||
|
|
||||||
|
This issue has now been addressed. The error handling now properly retries the
|
||||||
|
locking operation. In addition, restic waits a few seconds between locking
|
||||||
|
retries to increase chances of successful locking.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5005
|
||||||
|
https://github.com/restic/restic/pull/5011
|
||||||
|
https://github.com/restic/restic/pull/5012
|
||||||
|
|
||||||
|
* Bugfix #5018: Improve HTTP/2 support for REST backend
|
||||||
|
|
||||||
|
If `rest-server` tried to gracefully shut down an HTTP/2 connection still in use
|
||||||
|
by the client, it could result in the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
http2: Transport: cannot retry err [http2: Transport received Server's graceful shutdown GOAWAY] after Request.Body was written; define Request.GetBody to avoid this error
|
||||||
|
```
|
||||||
|
|
||||||
|
This issue has now been resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5018
|
||||||
|
https://forum.restic.net/t/receiving-http2-goaway-messages-with-windows-restic-v0-17-0/8367
|
||||||
|
|
||||||
|
* Change #4953: Also back up files with incomplete metadata
|
||||||
|
|
||||||
|
If restic failed to read extended metadata for a file or folder during a backup,
|
||||||
|
then the file or folder was not included in the resulting snapshot. Instead, a
|
||||||
|
warning message was printed along with returning exit code 3 once the backup was
|
||||||
|
finished.
|
||||||
|
|
||||||
|
Now, restic also includes items for which the extended metadata could not be
|
||||||
|
read in a snapshot. The warning message has been updated to:
|
||||||
|
|
||||||
|
```
|
||||||
|
incomplete metadata for /path/to/file: <details about error>
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4953
|
||||||
|
https://github.com/restic/restic/pull/4977
|
||||||
|
|
||||||
|
* Enhancement #4795: Display progress bar for `restore --verify`
|
||||||
|
|
||||||
|
When the `restore` command is run with `--verify`, it now displays a progress
|
||||||
|
bar while the verification step is running. The progress bar is not shown when
|
||||||
|
the `--json` flag is specified.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4795
|
||||||
|
https://github.com/restic/restic/pull/4989
|
||||||
|
|
||||||
|
* Enhancement #4934: Automatically clear removed snapshots from cache
|
||||||
|
|
||||||
|
Previously, restic only removed snapshots from the cache on the host where the
|
||||||
|
`forget` command was executed. On other hosts that use the same repository, the
|
||||||
|
old snapshots remained in the cache.
|
||||||
|
|
||||||
|
Restic now automatically clears old snapshots from the local cache of the
|
||||||
|
current host.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4934
|
||||||
|
https://github.com/restic/restic/pull/4981
|
||||||
|
|
||||||
|
* Enhancement #4944: Print JSON-formatted errors during `restore --json`
|
||||||
|
|
||||||
|
Restic used to print any `restore` errors directly to the console as freeform
|
||||||
|
text messages, even when using the `--json` option.
|
||||||
|
|
||||||
|
Now, when `--json` is specified, restic prints them as JSON formatted messages.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4944
|
||||||
|
https://github.com/restic/restic/pull/4946
|
||||||
|
|
||||||
|
* Enhancement #4959: Return exit code 12 for "bad password" errors
|
||||||
|
|
||||||
|
Restic now returns exit code 12 when it cannot open the repository due to an
|
||||||
|
incorrect password.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4959
|
||||||
|
|
||||||
|
* Enhancement #4970: Make timeout for stuck requests customizable
|
||||||
|
|
||||||
|
Restic monitors connections to the backend to detect stuck requests. If a
|
||||||
|
request does not return any data within five minutes, restic assumes the request
|
||||||
|
is stuck and retries it. However, for large repositories this timeout might be
|
||||||
|
insufficient to collect a list of all files, causing the following error:
|
||||||
|
|
||||||
|
`List(data) returned error, retrying after 1s: [...]: request timeout`
|
||||||
|
|
||||||
|
It is now possible to increase the timeout using the `--stuck-request-timeout`
|
||||||
|
option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4970
|
||||||
|
https://github.com/restic/restic/pull/5014
|
||||||
|
|
||||||
|
|
||||||
# Changelog for restic 0.17.0 (2024-07-26)
|
# Changelog for restic 0.17.0 (2024-07-26)
|
||||||
The following sections list the changes in restic 0.17.0 relevant to
|
The following sections list the changes in restic 0.17.0 relevant to
|
||||||
restic users. The changes are ordered by importance.
|
restic users. The changes are ordered by importance.
|
||||||
|
32
build.go
32
build.go
@@ -53,12 +53,14 @@ import (
|
|||||||
|
|
||||||
// config contains the configuration for the program to build.
|
// config contains the configuration for the program to build.
|
||||||
var config = Config{
|
var config = Config{
|
||||||
Name: "restic", // name of the program executable and directory
|
Name: "restic", // name of the program executable and directory
|
||||||
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
||||||
Main: "./cmd/restic", // package name for the main package
|
Main: "./cmd/restic", // package name for the main package
|
||||||
DefaultBuildTags: []string{"selfupdate"}, // specify build tags which are always used
|
// disable_grpc_modules is necessary to reduce the binary size since cloud.google.com/go/storage v1.44.0
|
||||||
Tests: []string{"./..."}, // tests to run
|
// see https://github.com/googleapis/google-cloud-go/issues/11448
|
||||||
MinVersion: GoVersion{Major: 1, Minor: 18, Patch: 0}, // minimum Go version supported
|
DefaultBuildTags: []string{"selfupdate", "disable_grpc_modules"}, // specify build tags which are always used
|
||||||
|
Tests: []string{"./..."}, // tests to run
|
||||||
|
MinVersion: GoVersion{Major: 1, Minor: 23, Patch: 0}, // minimum Go version supported
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config configures the build.
|
// Config configures the build.
|
||||||
@@ -298,19 +300,21 @@ func (v GoVersion) AtLeast(other GoVersion) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.Major > other.Major {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if v.Major < other.Major {
|
if v.Major < other.Major {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.Minor > other.Minor {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if v.Minor < other.Minor {
|
if v.Minor < other.Minor {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.Patch < other.Patch {
|
return v.Patch >= other.Patch
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v GoVersion) String() string {
|
func (v GoVersion) String() string {
|
||||||
@@ -380,12 +384,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
solarisMinVersion := GoVersion{Major: 1, Minor: 20, Patch: 0}
|
|
||||||
if env["GOARCH"] == "solaris" && !goVersion.AtLeast(solarisMinVersion) {
|
|
||||||
fmt.Fprintf(os.Stderr, "Detected version %s is too old, restic requires at least %s for Solaris\n", goVersion, solarisMinVersion)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
verbosePrintf("detected Go version %v\n", goVersion)
|
verbosePrintf("detected Go version %v\n", goVersion)
|
||||||
|
|
||||||
preserveSymbols := false
|
preserveSymbols := false
|
||||||
|
18
changelog/0.17.1_2024-09-05/issue-2004
Normal file
18
changelog/0.17.1_2024-09-05/issue-2004
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
Bugfix: Correctly handle volume names in `backup` command on Windows
|
||||||
|
|
||||||
|
On Windows, when the specified backup target only included the volume
|
||||||
|
name without a trailing slash, for example, `C:`, then restoring the
|
||||||
|
resulting snapshot would result in an error. Note that using `C:\`
|
||||||
|
as backup target worked correctly.
|
||||||
|
|
||||||
|
Specifying volume names is now handled correctly. To restore snapshots
|
||||||
|
created before this bugfix, use the <snapshot>:<subpath> syntax. For
|
||||||
|
example, to restore a snapshot with ID `12345678` that backed up `C:`,
|
||||||
|
use the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
restic restore 12345678:/C/C:./ --target output/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2004
|
||||||
|
https://github.com/restic/restic/pull/5028
|
8
changelog/0.17.1_2024-09-05/issue-4795
Normal file
8
changelog/0.17.1_2024-09-05/issue-4795
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Display progress bar for `restore --verify`
|
||||||
|
|
||||||
|
When the `restore` command is run with `--verify`, it now displays a progress
|
||||||
|
bar while the verification step is running. The progress bar is not shown when
|
||||||
|
the `--json` flag is specified.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4795
|
||||||
|
https://github.com/restic/restic/pull/4989
|
11
changelog/0.17.1_2024-09-05/issue-4934
Normal file
11
changelog/0.17.1_2024-09-05/issue-4934
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Enhancement: Automatically clear removed snapshots from cache
|
||||||
|
|
||||||
|
Previously, restic only removed snapshots from the cache on the host where the
|
||||||
|
`forget` command was executed. On other hosts that use the same repository, the
|
||||||
|
old snapshots remained in the cache.
|
||||||
|
|
||||||
|
Restic now automatically clears old snapshots from the local cache of the
|
||||||
|
current host.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4934
|
||||||
|
https://github.com/restic/restic/pull/4981
|
9
changelog/0.17.1_2024-09-05/issue-4944
Normal file
9
changelog/0.17.1_2024-09-05/issue-4944
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Enhancement: Print JSON-formatted errors during `restore --json`
|
||||||
|
|
||||||
|
Restic used to print any `restore` errors directly to the console as freeform
|
||||||
|
text messages, even when using the `--json` option.
|
||||||
|
|
||||||
|
Now, when `--json` is specified, restic prints them as JSON formatted messages.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4944
|
||||||
|
https://github.com/restic/restic/pull/4946
|
10
changelog/0.17.1_2024-09-05/issue-4945
Normal file
10
changelog/0.17.1_2024-09-05/issue-4945
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Bugfix: Include missing backup error text with `--json`
|
||||||
|
|
||||||
|
Previously, when running a backup with the `--json` option, restic failed to
|
||||||
|
include the actual error message in the output, resulting in `"error": {}`
|
||||||
|
being displayed.
|
||||||
|
|
||||||
|
This has now been fixed, and restic now includes the error text in JSON output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4945
|
||||||
|
https://github.com/restic/restic/pull/4946
|
7
changelog/0.17.1_2024-09-05/issue-4953
Normal file
7
changelog/0.17.1_2024-09-05/issue-4953
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Correctly handle long paths on older Windows versions
|
||||||
|
|
||||||
|
On older Windows versions, like Windows Server 2012, restic 0.17.0 failed to
|
||||||
|
back up files with long paths. This problem has now been resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4953
|
||||||
|
https://github.com/restic/restic/pull/4954
|
8
changelog/0.17.1_2024-09-05/issue-4957
Normal file
8
changelog/0.17.1_2024-09-05/issue-4957
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: Fix delayed cancellation of certain commands
|
||||||
|
|
||||||
|
Since restic 0.17.0, some commands did not immediately respond to cancellation
|
||||||
|
via Ctrl-C (SIGINT) and continued running for a short period. The most affected
|
||||||
|
commands were `diff`,`find`, `ls`, `stats` and `rewrite`. This is now resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4957
|
||||||
|
https://github.com/restic/restic/pull/4960
|
7
changelog/0.17.1_2024-09-05/issue-4969
Normal file
7
changelog/0.17.1_2024-09-05/issue-4969
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Correctly restore timestamp for files with resource forks on macOS
|
||||||
|
|
||||||
|
On macOS, timestamps were not restored for files with resource forks. This has
|
||||||
|
now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4969
|
||||||
|
https://github.com/restic/restic/pull/5006
|
15
changelog/0.17.1_2024-09-05/issue-4970
Normal file
15
changelog/0.17.1_2024-09-05/issue-4970
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Enhancement: Make timeout for stuck requests customizable
|
||||||
|
|
||||||
|
Restic monitors connections to the backend to detect stuck requests. If a
|
||||||
|
request does not return any data within five minutes, restic assumes the
|
||||||
|
request is stuck and retries it. However, for large repositories this timeout
|
||||||
|
might be insufficient to collect a list of all files, causing the following
|
||||||
|
error:
|
||||||
|
|
||||||
|
`List(data) returned error, retrying after 1s: [...]: request timeout`
|
||||||
|
|
||||||
|
It is now possible to increase the timeout using the `--stuck-request-timeout`
|
||||||
|
option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4970
|
||||||
|
https://github.com/restic/restic/pull/5014
|
7
changelog/0.17.1_2024-09-05/issue-4975
Normal file
7
changelog/0.17.1_2024-09-05/issue-4975
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Prevent `backup --stdin-from-command` from panicking
|
||||||
|
|
||||||
|
Restic would previously crash if `--stdin-from-command` was specified without
|
||||||
|
providing a command. This issue has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4975
|
||||||
|
https://github.com/restic/restic/pull/4976
|
12
changelog/0.17.1_2024-09-05/issue-5004
Normal file
12
changelog/0.17.1_2024-09-05/issue-5004
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Bugfix: Fix spurious "A Required Privilege Is Not Held by the Client" error
|
||||||
|
|
||||||
|
On Windows, creating a backup could sometimes trigger the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: nodeFromFileInfo [...]: get named security info failed with: a required privilege is not held by the client.
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5004
|
||||||
|
https://github.com/restic/restic/pull/5019
|
16
changelog/0.17.1_2024-09-05/issue-5005
Normal file
16
changelog/0.17.1_2024-09-05/issue-5005
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Bugfix: Fix rare failures to retry locking a repository
|
||||||
|
|
||||||
|
Restic 0.17.0 could in rare cases fail to retry locking a repository if one of
|
||||||
|
the lock files failed to load, resulting in the error:
|
||||||
|
|
||||||
|
```
|
||||||
|
unable to create lock in backend: circuit breaker open for file <lock/1234567890>
|
||||||
|
```
|
||||||
|
|
||||||
|
This issue has now been addressed. The error handling now properly retries the
|
||||||
|
locking operation. In addition, restic waits a few seconds between locking
|
||||||
|
retries to increase chances of successful locking.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5005
|
||||||
|
https://github.com/restic/restic/pull/5011
|
||||||
|
https://github.com/restic/restic/pull/5012
|
7
changelog/0.17.1_2024-09-05/pull-4958
Normal file
7
changelog/0.17.1_2024-09-05/pull-4958
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Don't ignore metadata-setting errors during restore
|
||||||
|
|
||||||
|
Previously, restic used to ignore errors when setting timestamps, attributes,
|
||||||
|
or file modes during a restore. It now reports those errors, except for
|
||||||
|
permission related errors when running without root privileges.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4958
|
6
changelog/0.17.1_2024-09-05/pull-4959
Normal file
6
changelog/0.17.1_2024-09-05/pull-4959
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Return exit code 12 for "bad password" errors
|
||||||
|
|
||||||
|
Restic now returns exit code 12 when it cannot open the repository due to an
|
||||||
|
incorrect password.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4959
|
16
changelog/0.17.1_2024-09-05/pull-4977
Normal file
16
changelog/0.17.1_2024-09-05/pull-4977
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Change: Also back up files with incomplete metadata
|
||||||
|
|
||||||
|
If restic failed to read extended metadata for a file or folder during a
|
||||||
|
backup, then the file or folder was not included in the resulting snapshot.
|
||||||
|
Instead, a warning message was printed along with returning exit code 3 once
|
||||||
|
the backup was finished.
|
||||||
|
|
||||||
|
Now, restic also includes items for which the extended metadata could not be
|
||||||
|
read in a snapshot. The warning message has been updated to:
|
||||||
|
|
||||||
|
```
|
||||||
|
incomplete metadata for /path/to/file: <details about error>
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4953
|
||||||
|
https://github.com/restic/restic/pull/4977
|
12
changelog/0.17.1_2024-09-05/pull-4980
Normal file
12
changelog/0.17.1_2024-09-05/pull-4980
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Bugfix: Skip extended attribute processing on unsupported Windows volumes
|
||||||
|
|
||||||
|
With restic 0.17.0, backups of certain Windows paths, such as network drives,
|
||||||
|
failed due to errors while fetching extended attributes.
|
||||||
|
|
||||||
|
Restic now skips extended attribute processing for volumes where they are not
|
||||||
|
supported.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4980
|
||||||
|
https://github.com/restic/restic/pull/4998
|
||||||
|
https://github.com/restic/restic/issues/4955
|
||||||
|
https://github.com/restic/restic/issues/4950
|
13
changelog/0.17.1_2024-09-05/pull-5018
Normal file
13
changelog/0.17.1_2024-09-05/pull-5018
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Bugfix: Improve HTTP/2 support for REST backend
|
||||||
|
|
||||||
|
If `rest-server` tried to gracefully shut down an HTTP/2 connection still in
|
||||||
|
use by the client, it could result in the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
http2: Transport: cannot retry err [http2: Transport received Server's graceful shutdown GOAWAY] after Request.Body was written; define Request.GetBody to avoid this error
|
||||||
|
```
|
||||||
|
|
||||||
|
This issue has now been resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5018
|
||||||
|
https://forum.restic.net/t/receiving-http2-goaway-messages-with-windows-restic-v0-17-0/8367
|
12
changelog/0.17.2_2024-10-27/issue-4004
Normal file
12
changelog/0.17.2_2024-10-27/issue-4004
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Bugfix: Support container-level SAS/SAT tokens for Azure backend
|
||||||
|
|
||||||
|
Restic previously expected SAS/SAT tokens to be generated at the account level,
|
||||||
|
which prevented tokens created at the container level from being used to
|
||||||
|
initialize a repository. This caused an error when attempting to initialize a
|
||||||
|
repository with container-level tokens.
|
||||||
|
|
||||||
|
Restic now supports both account-level and container-level SAS/SAT tokens for
|
||||||
|
initializing a repository.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4004
|
||||||
|
https://github.com/restic/restic/pull/5093
|
7
changelog/0.17.2_2024-10-27/issue-5050
Normal file
7
changelog/0.17.2_2024-10-27/issue-5050
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Return error if `tag` fails to lock repository
|
||||||
|
|
||||||
|
Since restic 0.17.0, the `tag` command did not return an error when it failed
|
||||||
|
to open or lock the repository. This issue has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5050
|
||||||
|
https://github.com/restic/restic/pull/5056
|
12
changelog/0.17.2_2024-10-27/issue-5063
Normal file
12
changelog/0.17.2_2024-10-27/issue-5063
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Bugfix: Correctly `backup` extended metadata when using VSS on Windows
|
||||||
|
|
||||||
|
On Windows, when creating a backup with the `--use-fs-snapshot` option, restic
|
||||||
|
read extended metadata from the original filesystem path instead of from the
|
||||||
|
snapshot. This could result in errors if files were removed during the backup
|
||||||
|
process.
|
||||||
|
|
||||||
|
This issue has now been resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5063
|
||||||
|
https://github.com/restic/restic/pull/5097
|
||||||
|
https://github.com/restic/restic/pull/5099
|
8
changelog/0.17.2_2024-10-27/pull-5047
Normal file
8
changelog/0.17.2_2024-10-27/pull-5047
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: Resolve potential error during concurrent cache cleanup
|
||||||
|
|
||||||
|
When multiple restic processes ran concurrently, they could compete to remove
|
||||||
|
obsolete snapshots from the local backend cache, sometimes leading to a "no
|
||||||
|
such file or directory" error. Restic now suppresses this error to prevent
|
||||||
|
issues during cache cleanup.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5047
|
24
changelog/0.17.2_2024-10-27/pull-5057
Normal file
24
changelog/0.17.2_2024-10-27/pull-5057
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Bugfix: Exclude irregular files from backups
|
||||||
|
|
||||||
|
Since restic 0.17.1, files with the type `irregular` could mistakenly be included
|
||||||
|
in snapshots, especially when backing up special file types on Windows that
|
||||||
|
restic cannot process. This issue has now been fixed.
|
||||||
|
|
||||||
|
Previously, this bug caused the `check` command to report errors like the
|
||||||
|
following one:
|
||||||
|
|
||||||
|
```
|
||||||
|
tree 12345678[...]: node "example.zip" with invalid type "irregular"
|
||||||
|
```
|
||||||
|
|
||||||
|
To repair affected snapshots, upgrade to restic 0.17.2 and run:
|
||||||
|
|
||||||
|
```
|
||||||
|
restic repair snapshots --forget
|
||||||
|
```
|
||||||
|
|
||||||
|
This will remove the `irregular` files from the snapshots (creating
|
||||||
|
a new snapshot ID for each of the affected snapshots).
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5057
|
||||||
|
https://forum.restic.net/t/errors-found-by-check-1-invalid-type-irregular-2-ciphertext-verification-failed/8447/2
|
7
changelog/0.17.3_2024-11-08/issue-4971
Normal file
7
changelog/0.17.3_2024-11-08/issue-4971
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Fix unusable `mount` on macOS Sonoma
|
||||||
|
|
||||||
|
On macOS Sonoma when using FUSE-T, it was not possible to access files in
|
||||||
|
a mounted repository. This issue is now resolved.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4971
|
||||||
|
https://github.com/restic/restic/pull/5048
|
14
changelog/0.17.3_2024-11-08/issue-5003
Normal file
14
changelog/0.17.3_2024-11-08/issue-5003
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Bugfix: Fix metadata errors during backup of removable disks on Windows
|
||||||
|
|
||||||
|
Since restic 0.17.0, backing up removable disks on Windows could report
|
||||||
|
errors with retrieving metadata like shown below.
|
||||||
|
|
||||||
|
```
|
||||||
|
error: incomplete metadata for d:\filename: get named security info failed with: Access is denied.
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5003
|
||||||
|
https://github.com/restic/restic/pull/5123
|
||||||
|
https://forum.restic.net/t/backing-up-a-folder-from-a-veracrypt-volume-brings-up-errors-since-restic-v17-0/8444
|
15
changelog/0.17.3_2024-11-08/issue-5107
Normal file
15
changelog/0.17.3_2024-11-08/issue-5107
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Bugfix: Fix metadata error on Windows for backups using VSS
|
||||||
|
|
||||||
|
Since restic 0.17.2, when creating a backup on Windows using `--use-fs-snapshot`,
|
||||||
|
restic would report an error like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: incomplete metadata for C:\: get EA failed while opening file handle for path \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyXX\, with: The process cannot access the file because it is being used by another process.
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed by correctly handling paths that refer to volume
|
||||||
|
shadow copy snapshots.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5107
|
||||||
|
https://github.com/restic/restic/pull/5110
|
||||||
|
https://github.com/restic/restic/pull/5112
|
8
changelog/0.17.3_2024-11-08/pull-5096
Normal file
8
changelog/0.17.3_2024-11-08/pull-5096
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Allow `prune --dry-run` without lock
|
||||||
|
|
||||||
|
The `prune --dry-run --no-lock` now allows performing a dry-run
|
||||||
|
without locking the repository. Note that if the repository is
|
||||||
|
modified concurrently, `prune` may return inaccurate statistics
|
||||||
|
or errors.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5096
|
8
changelog/0.17.3_2024-11-08/pull-5101
Normal file
8
changelog/0.17.3_2024-11-08/pull-5101
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: Do not retry load/list operation if SFTP connection is broken
|
||||||
|
|
||||||
|
When using restic with the SFTP backend, backend operations that load a
|
||||||
|
file or list files were retried even if the SFTP connection was broken.
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5101
|
||||||
|
https://forum.restic.net/t/restic-hanging-on-backup/8559
|
7
changelog/0.18.0_2025-03-27/issue-1378
Normal file
7
changelog/0.18.0_2025-03-27/issue-1378
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Add JSON support to `check` command
|
||||||
|
|
||||||
|
The `check` command now supports the `--json` option to output all statistics in
|
||||||
|
JSON format.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/1378
|
||||||
|
https://github.com/restic/restic/pull/5194
|
7
changelog/0.18.0_2025-03-27/issue-1843
Normal file
7
changelog/0.18.0_2025-03-27/issue-1843
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Correctly restore long filepaths' timestamp on old Windows
|
||||||
|
|
||||||
|
The `restore` command now correctly restores timestamps for files with paths longer
|
||||||
|
than 256 characters on Windows versions prior to Windows 10 1607.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/1843
|
||||||
|
https://github.com/restic/restic/pull/5061
|
13
changelog/0.18.0_2025-03-27/issue-2165
Normal file
13
changelog/0.18.0_2025-03-27/issue-2165
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Bugfix: Ignore disappeared backup source files
|
||||||
|
|
||||||
|
The `backup` command now quietly skips files that are removed between directory
|
||||||
|
listing and backup, instead of printing errors like:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lstat /some/file/name: no such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2165
|
||||||
|
https://github.com/restic/restic/issues/3098
|
||||||
|
https://github.com/restic/restic/pull/5143
|
||||||
|
https://github.com/restic/restic/pull/5145
|
7
changelog/0.18.0_2025-03-27/issue-2511
Normal file
7
changelog/0.18.0_2025-03-27/issue-2511
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Support generating shell completions to stdout
|
||||||
|
|
||||||
|
The `generate` command now supports using `-` as the filename with the
|
||||||
|
`--[shell]-completion` option to write the generated output to stdout.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2511
|
||||||
|
https://github.com/restic/restic/pull/5053
|
11
changelog/0.18.0_2025-03-27/issue-3202
Normal file
11
changelog/0.18.0_2025-03-27/issue-3202
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Enhancement: Add experimental S3 cold storage support
|
||||||
|
|
||||||
|
Introduce S3 backend options for transitioning pack files from cold to hot storage
|
||||||
|
on S3 and S3-compatible providers. Note: this only works for the `prune`, `copy`
|
||||||
|
and `restore` commands for now.
|
||||||
|
|
||||||
|
This experimental feature is gated behind the "s3-restore" feature flag.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5173
|
||||||
|
https://github.com/restic/restic/issues/3202
|
||||||
|
https://github.com/restic/restic/issues/2504
|
12
changelog/0.18.0_2025-03-27/issue-3697
Normal file
12
changelog/0.18.0_2025-03-27/issue-3697
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Enhancement: Allow excluding online-only cloud files (e.g. OneDrive)
|
||||||
|
|
||||||
|
Restic treated files synced using OneDrive Files On-Demand as though they
|
||||||
|
were regular files. This caused issues with VSS and could cause OneDrive to
|
||||||
|
download all files.
|
||||||
|
|
||||||
|
Restic now allows the user to exclude these files when backing up with
|
||||||
|
the `--exclude-cloud-files` option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3697
|
||||||
|
https://github.com/restic/restic/issues/4935
|
||||||
|
https://github.com/restic/restic/pull/4990
|
8
changelog/0.18.0_2025-03-27/issue-4179
Normal file
8
changelog/0.18.0_2025-03-27/issue-4179
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Add `sort` option to `ls` command
|
||||||
|
|
||||||
|
The `ls -l` command output can now be sorted using the new `--sort <field>`
|
||||||
|
option for the fields `name`, `size`, `time` (same as `mtime`), `mtime`,
|
||||||
|
`atime`, `ctime` and `extension`. A `--reverse` option is also available.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4179
|
||||||
|
https://github.com/restic/restic/pull/5182
|
7
changelog/0.18.0_2025-03-27/issue-4433
Normal file
7
changelog/0.18.0_2025-03-27/issue-4433
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Change default sort order for `find` output
|
||||||
|
|
||||||
|
The `find` command now sorts snapshots from newest to oldest by default. The
|
||||||
|
previous oldest-to-newest order can be restored using the new `--reverse` option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4433
|
||||||
|
https://github.com/restic/restic/pull/5184
|
12
changelog/0.18.0_2025-03-27/issue-4521
Normal file
12
changelog/0.18.0_2025-03-27/issue-4521
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Enhancement: Add support for Microsoft Blob Storage access tiers
|
||||||
|
|
||||||
|
The new `-o azure.access-tier=<tier>` option allows specifying the access tier
|
||||||
|
(`Hot`, `Cool` or `Cold`) for objects created in Microsoft Blob Storage. If
|
||||||
|
unspecified, the storage account's default tier is used.
|
||||||
|
|
||||||
|
There is no official `Archive` storage support in restic, use this option at
|
||||||
|
your own risk. To restore any data, it is necessary to manually warm up the
|
||||||
|
required data in the `Archive` tier.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4521
|
||||||
|
https://github.com/restic/restic/pull/5046
|
11
changelog/0.18.0_2025-03-27/issue-4942
Normal file
11
changelog/0.18.0_2025-03-27/issue-4942
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Enhancement: Add snapshot summary statistics to rewritten snapshots
|
||||||
|
|
||||||
|
The `rewrite` command now supports a `--snapshot-summary` option to add
|
||||||
|
statistics data to snapshots. Only two fields in the summary will be non-zero:
|
||||||
|
`TotalFilesProcessed` and `TotalBytesProcessed`.
|
||||||
|
|
||||||
|
For snapshots rewritten using the `--exclude` options, the summary
|
||||||
|
statistics are updated accordingly.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4942
|
||||||
|
https://github.com/restic/restic/pull/5185
|
6
changelog/0.18.0_2025-03-27/issue-4948
Normal file
6
changelog/0.18.0_2025-03-27/issue-4948
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Format exit errors as JSON when requested
|
||||||
|
|
||||||
|
Restic now formats error messages as JSON when the `--json` flag is used.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4948
|
||||||
|
https://github.com/restic/restic/pull/4952
|
10
changelog/0.18.0_2025-03-27/issue-4983
Normal file
10
changelog/0.18.0_2025-03-27/issue-4983
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Enhancement: Add SLSA provenance to GHCR container images
|
||||||
|
|
||||||
|
Restic's GitHub Container Registry (GHCR) image build workflow now includes
|
||||||
|
SLSA (Supply-chain Levels for Software Artifacts) provenance generation.
|
||||||
|
|
||||||
|
Please see the restic documentation for more information about verifying SLSA
|
||||||
|
provenance.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4983
|
||||||
|
https://github.com/restic/restic/pull/4999
|
7
changelog/0.18.0_2025-03-27/issue-5081
Normal file
7
changelog/0.18.0_2025-03-27/issue-5081
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Add retry mechanism for loading repository config
|
||||||
|
|
||||||
|
Restic now retries loading the repository config file when opening a repository.
|
||||||
|
The `init` command now also retries backend operations.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5081
|
||||||
|
https://github.com/restic/restic/pull/5095
|
8
changelog/0.18.0_2025-03-27/issue-5089
Normal file
8
changelog/0.18.0_2025-03-27/issue-5089
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Allow including/excluding extended file attributes during `restore`
|
||||||
|
|
||||||
|
The `restore` command now supports the `--exclude-xattr` and `--include-xattr`
|
||||||
|
options to control which extended file attributes will be restored. By default,
|
||||||
|
all attributes are restored.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5089
|
||||||
|
https://github.com/restic/restic/pull/5129
|
7
changelog/0.18.0_2025-03-27/issue-5092
Normal file
7
changelog/0.18.0_2025-03-27/issue-5092
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Show count of deleted files and directories during `restore`
|
||||||
|
|
||||||
|
The `restore` command now reports the number of deleted files and directories,
|
||||||
|
both in the regular output and in the `files_deleted` field of the JSON output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5092
|
||||||
|
https://github.com/restic/restic/pull/5100
|
7
changelog/0.18.0_2025-03-27/issue-5109
Normal file
7
changelog/0.18.0_2025-03-27/issue-5109
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Make small pack size configurable for `prune`
|
||||||
|
|
||||||
|
The `prune` command now supports the `--repack-smaller-than` option that
|
||||||
|
allows repacking pack files smaller than a specified size.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5109
|
||||||
|
https://github.com/restic/restic/pull/5183
|
6
changelog/0.18.0_2025-03-27/issue-5131
Normal file
6
changelog/0.18.0_2025-03-27/issue-5131
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Add DragonFlyBSD support
|
||||||
|
|
||||||
|
Restic can now be compiled on DragonflyBSD.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5131
|
||||||
|
https://github.com/restic/restic/pull/5138
|
8
changelog/0.18.0_2025-03-27/issue-5137
Normal file
8
changelog/0.18.0_2025-03-27/issue-5137
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Make `tag` command print which snapshots were modified
|
||||||
|
|
||||||
|
The `tag` command now outputs which snapshots were modified along with their
|
||||||
|
new snapshot ID. The command supports the `--json` option for machine-readable
|
||||||
|
output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5137
|
||||||
|
https://github.com/restic/restic/pull/5144
|
7
changelog/0.18.0_2025-03-27/issue-5174
Normal file
7
changelog/0.18.0_2025-03-27/issue-5174
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Add xattr support for NetBSD 10+
|
||||||
|
|
||||||
|
Extended attribute support for `backup` and `restore` operations
|
||||||
|
is now available on NetBSD version 10 and later.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5174
|
||||||
|
https://github.com/restic/restic/pull/5180
|
16
changelog/0.18.0_2025-03-27/issue-5259
Normal file
16
changelog/0.18.0_2025-03-27/issue-5259
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Bugfix: Fix rare crash in command output
|
||||||
|
|
||||||
|
Some commands could in rare cases crash when trying to print status messages
|
||||||
|
and request retries at the same time, resulting in an error like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
panic: runtime error: slice bounds out of range [468:156]
|
||||||
|
[...]
|
||||||
|
github.com/restic/restic/internal/ui/termstatus.(*lineWriter).Write(...)
|
||||||
|
/restic/internal/ui/termstatus/stdio_wrapper.go:36 +0x136
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5259
|
||||||
|
https://github.com/restic/restic/pull/5300
|
8
changelog/0.18.0_2025-03-27/issue-5287
Normal file
8
changelog/0.18.0_2025-03-27/issue-5287
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Make `recover` automatically rebuild index when needed
|
||||||
|
|
||||||
|
When trying to recover data from an interrupted snapshot, it was previously
|
||||||
|
necessary to manually run `repair index` before runnning `recover`. This now
|
||||||
|
happens automatically so that only `recover` is necessary.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5287
|
||||||
|
https://github.com/restic/restic/pull/5296
|
24
changelog/0.18.0_2025-03-27/issue-5291
Normal file
24
changelog/0.18.0_2025-03-27/issue-5291
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Security: Mitigate attack on content-defined chunking algorithm
|
||||||
|
|
||||||
|
Restic uses [Rabin Fingerprints](https://restic.net/blog/2015-09-12/restic-foundation1-cdc/)
|
||||||
|
for its content-defined chunker. The algorithm relies on a secret polynomial
|
||||||
|
to split files into chunks.
|
||||||
|
|
||||||
|
As shown in the paper "[Chunking Attacks on File Backup Services using Content-Defined Chunking](https://eprint.iacr.org/2025/532.pdf)"
|
||||||
|
by Boris Alexeev, Colin Percival and Yan X Zhang, an
|
||||||
|
attacker that can observe chunk sizes for a known file can derive the secret
|
||||||
|
polynomial. Knowledge of the polynomial might in some cases allow an attacker
|
||||||
|
to check whether certain large files are stored in a repository.
|
||||||
|
|
||||||
|
A practical attack is nevertheless hard as restic merges multiple chunks into
|
||||||
|
opaque pack files and by default processes multiple files in parallel. This
|
||||||
|
likely prevents an attacker from matching pack files to the attacker-known file
|
||||||
|
and thereby prevents the attack.
|
||||||
|
|
||||||
|
Despite the low chances of a practical attack, restic now has added mitigation
|
||||||
|
that randomizes how chunks are assembled into pack files. This prevents attackers
|
||||||
|
from guessing which chunks are part of a pack file and thereby prevents learning
|
||||||
|
the chunk sizes.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5291
|
||||||
|
https://github.com/restic/restic/pull/5295
|
9
changelog/0.18.0_2025-03-27/pull-4938
Normal file
9
changelog/0.18.0_2025-03-27/pull-4938
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Change: Update dependencies and require Go 1.23 or newer
|
||||||
|
|
||||||
|
We have updated all dependencies. Restic now requires Go 1.23 or newer to build.
|
||||||
|
|
||||||
|
This also disables support for TLS versions older than TLS 1.2. On Windows,
|
||||||
|
restic now requires at least Windows 10 or Windows Server 2016. On macOS,
|
||||||
|
restic now requires at least macOS 11 Big Sur.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4938
|
6
changelog/0.18.0_2025-03-27/pull-5054
Normal file
6
changelog/0.18.0_2025-03-27/pull-5054
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Enable compression for ZIP archives in `dump` command
|
||||||
|
|
||||||
|
The `dump` command now compresses ZIP archives using the DEFLATE algorithm,
|
||||||
|
reducing the size of exported archives.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5054
|
6
changelog/0.18.0_2025-03-27/pull-5119
Normal file
6
changelog/0.18.0_2025-03-27/pull-5119
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Add start and end timestamps to `backup` JSON output
|
||||||
|
|
||||||
|
The JSON output of the `backup` command now includes `backup_start` and
|
||||||
|
`backup_end` timestamps, containing the start and end time of the backup.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5119
|
7
changelog/0.18.0_2025-03-27/pull-5141
Normal file
7
changelog/0.18.0_2025-03-27/pull-5141
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Provide clear error message if AZURE_ACCOUNT_NAME is not set
|
||||||
|
|
||||||
|
If `AZURE_ACCOUNT_NAME` was not set, commands related to an Azure repository
|
||||||
|
would result in a misleading networking error. Restic now detect this and
|
||||||
|
provides a clear warning that the variable is not defined.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5141
|
7
changelog/0.18.0_2025-03-27/pull-5153
Normal file
7
changelog/0.18.0_2025-03-27/pull-5153
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Include root tree when searching using `find --tree`
|
||||||
|
|
||||||
|
The `restic find --tree` command did not find trees referenced by
|
||||||
|
`restic snapshot --json`. It now correctly includes the root tree
|
||||||
|
when searching.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5153
|
8
changelog/0.18.0_2025-03-27/pull-5162
Normal file
8
changelog/0.18.0_2025-03-27/pull-5162
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Change: Graduate feature flags
|
||||||
|
|
||||||
|
The `deprecate-legacy-index`, `deprecate-s3-legacy-layout`,
|
||||||
|
`explicit-s3-anonymous-auth` and `safe-forget-keep-tags` features are
|
||||||
|
now stable and can no longer be disabled. The corresponding feature flags
|
||||||
|
will be removed in restic 0.19.0.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5162
|
22
changelog/0.18.0_2025-03-27/pull-5170
Normal file
22
changelog/0.18.0_2025-03-27/pull-5170
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Bugfix: Prevent Windows VSS event log 8194 warnings for backup with fs snapshot
|
||||||
|
|
||||||
|
When running `backup` with the `--use-fs-snapshot` option in Windows with admin rights, event logs like
|
||||||
|
|
||||||
|
```
|
||||||
|
Volume Shadow Copy Service error: Unexpected error querying for the IVssWriterCallback interface. hr = 0x80070005, Access is denied.
|
||||||
|
. This is often caused by incorrect security settings in either the writer or requester process.
|
||||||
|
|
||||||
|
Operation:
|
||||||
|
Gathering Writer Data
|
||||||
|
|
||||||
|
Context:
|
||||||
|
Writer Class Id: {e8132975-6f93-4464-a53e-1050253ae220}
|
||||||
|
Writer Name: System Writer
|
||||||
|
Writer Instance ID: {54b151ac-d27d-4628-9cb0-2bc40959f50f}
|
||||||
|
```
|
||||||
|
|
||||||
|
are created several times even though the backup itself succeeds. This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5169
|
||||||
|
https://github.com/restic/restic/pull/5170
|
||||||
|
https://forum.restic.net/t/windows-shadow-copy-snapshot-vss-unexpected-provider-error/3674/2
|
8
changelog/0.18.0_2025-03-27/pull-5212
Normal file
8
changelog/0.18.0_2025-03-27/pull-5212
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: Fix duplicate data handling in `prune --max-unused`
|
||||||
|
|
||||||
|
The `prune --max-unused size` command did not correctly account for duplicate
|
||||||
|
data. If a repository contained a large amount of duplicate data, this could
|
||||||
|
previously result in pruning too little data. This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5212
|
||||||
|
https://forum.restic.net/t/restic-not-obeying-max-unused-parameter-on-prune/8879
|
10
changelog/0.18.0_2025-03-27/pull-5249
Normal file
10
changelog/0.18.0_2025-03-27/pull-5249
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Bugfix: Fix creation of oversized index by `repair index --read-all-packs`
|
||||||
|
|
||||||
|
Since restic 0.17.0, the new index created by `repair index --read-all-packs` was
|
||||||
|
written as a single large index. This significantly increased memory usage while
|
||||||
|
loading the index.
|
||||||
|
|
||||||
|
The index is now correctly split into multiple smaller indexes, and `repair index`
|
||||||
|
now also automatically splits oversized indexes.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5249
|
9
changelog/0.18.0_2025-03-27/pull-5251
Normal file
9
changelog/0.18.0_2025-03-27/pull-5251
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Enhancement: Improve retry handling for flaky `rclone` backends
|
||||||
|
|
||||||
|
Since restic 0.17.0, the backend retry mechanisms rely on backends correctly
|
||||||
|
reporting when a file does not exist. This is not always the case for some
|
||||||
|
`rclone` backends, which caused restic to stop retrying after the first failure.
|
||||||
|
|
||||||
|
For rclone, failed requests are now retried up to 5 times before giving up.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5251
|
@@ -5,6 +5,8 @@ Enhancement: Allow custom bar in the foo command
|
|||||||
|
|
||||||
# Describe the problem in the past tense, the new behavior in the present
|
# Describe the problem in the past tense, the new behavior in the present
|
||||||
# tense. Mention the affected commands, backends, operating systems, etc.
|
# tense. Mention the affected commands, backends, operating systems, etc.
|
||||||
|
# If the problem description just says that a feature was missing, then
|
||||||
|
# only explain the new behavior.
|
||||||
# Focus on user-facing behavior, not the implementation.
|
# Focus on user-facing behavior, not the implementation.
|
||||||
# Use "Restic now ..." instead of "We have changed ...".
|
# Use "Restic now ..." instead of "We have changed ...".
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ Details
|
|||||||
{{ range $entry := .Entries }}{{ with $entry }}
|
{{ range $entry := .Entries }}{{ with $entry }}
|
||||||
* {{ .Type }} #{{ .PrimaryID }}: {{ .Title }}
|
* {{ .Type }} #{{ .PrimaryID }}: {{ .Title }}
|
||||||
{{ range $par := .Paragraphs }}
|
{{ range $par := .Paragraphs }}
|
||||||
{{ $par }}
|
{{ indent 3 $par }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ range $id := .Issues -}}
|
{{ range $id := .Issues -}}
|
||||||
{{ ` ` }}[#{{ $id }}](https://github.com/restic/restic/issues/{{ $id -}})
|
{{ ` ` }}[#{{ $id }}](https://github.com/restic/restic/issues/{{ $id -}})
|
||||||
|
7
changelog/unreleased/issue-4728
Normal file
7
changelog/unreleased/issue-4728
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Added support for zstd compression levels `fastest` and `better`
|
||||||
|
|
||||||
|
Restic now supports the zstd compression modes `fastest` and `better`. Set the
|
||||||
|
environment variable `RESTIC_COMPRESSION` to `fastest` or `better` to use these
|
||||||
|
compression levels. This can also be set with the `--compression` flag.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4728
|
14
changelog/unreleased/issue-4868
Normal file
14
changelog/unreleased/issue-4868
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Enhancement: Include repository id in filesystem name used by `mount`
|
||||||
|
|
||||||
|
The filesystem created by restic's `mount` command now includes the repository
|
||||||
|
id in the filesystem name. The repository id is printed by restic when opening
|
||||||
|
a repository or can be looked up using `restic cat config`.
|
||||||
|
|
||||||
|
```
|
||||||
|
[restic-user@hostname restic]$ df ./test-mount/
|
||||||
|
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||||
|
restic:d3b07384d1 0 0 0 - /mnt/my-restic-repo
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4868
|
||||||
|
https://github.com/restic/restic/pull/5243
|
8
changelog/unreleased/issue-5233
Normal file
8
changelog/unreleased/issue-5233
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: forget command returns exit code 3 on partial removal of snapshots
|
||||||
|
|
||||||
|
The `forget` command now returns exit code 3 when it fails to remove one or
|
||||||
|
more snapshots. Previously, it returned exit code 0, which could lead to
|
||||||
|
confusion if the command was used in a script.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5233
|
||||||
|
https://github.com/restic/restic/pull/5322
|
14
changelog/unreleased/issue-5324
Normal file
14
changelog/unreleased/issue-5324
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Bugfix: Correctly handle `backup --stdin-filename` with directories
|
||||||
|
|
||||||
|
In restic 0.18.0, the `backup` command failed if a filename that includes
|
||||||
|
a least a directory was passed to `--stdin-filename`. For example,
|
||||||
|
`--stdin-filename /foo/bar` resulted in the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
Fatal: unable to save snapshot: open /foo: no such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
This has been fixed now.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5324
|
||||||
|
https://github.com/restic/restic/pull/5356
|
7
changelog/unreleased/issue-5325
Normal file
7
changelog/unreleased/issue-5325
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Correctly handle `RESTIC_HOST` in `forget` command
|
||||||
|
|
||||||
|
The `forget` command did not use the host name from the `RESTIC_HOST`
|
||||||
|
environment variable. This has been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5325
|
||||||
|
https://github.com/restic/restic/pull/5327
|
7
changelog/unreleased/issue-5342
Normal file
7
changelog/unreleased/issue-5342
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Ignore "chmod not supported" errors when writing files
|
||||||
|
|
||||||
|
Restic 0.18.0 introduced a bug that caused "chmod xxx: operation not supported"
|
||||||
|
errors to appear when writing to a local file repository that did not support
|
||||||
|
chmod (like CIFS or WebDAV mounted via FUSE). Restic now ignores those errors.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5342
|
@@ -15,23 +15,29 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/archiver"
|
"github.com/restic/restic/internal/archiver"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/filter"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/textfile"
|
"github.com/restic/restic/internal/textfile"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/backup"
|
"github.com/restic/restic/internal/ui/backup"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui/termstatus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdBackup = &cobra.Command{
|
func newBackupCommand() *cobra.Command {
|
||||||
Use: "backup [flags] [FILE/DIR] ...",
|
var opts BackupOptions
|
||||||
Short: "Create a new backup of files and/or directories",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "backup [flags] [FILE/DIR] ...",
|
||||||
|
Short: "Create a new backup of files and/or directories",
|
||||||
|
Long: `
|
||||||
The "backup" command creates a new snapshot and saves the files and directories
|
The "backup" command creates a new snapshot and saves the files and directories
|
||||||
given as the arguments.
|
given as the arguments.
|
||||||
|
|
||||||
@@ -43,28 +49,34 @@ Exit status is 1 if there was a fatal error (no snapshot created).
|
|||||||
Exit status is 3 if some source data could not be read (incomplete snapshot created).
|
Exit status is 3 if some source data could not be read (incomplete snapshot created).
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
PreRun: func(_ *cobra.Command, _ []string) {
|
PreRun: func(_ *cobra.Command, _ []string) {
|
||||||
if backupOptions.Host == "" {
|
if opts.Host == "" {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.Log("os.Hostname() returned err: %v", err)
|
debug.Log("os.Hostname() returned err: %v", err)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
opts.Host = hostname
|
||||||
}
|
}
|
||||||
backupOptions.Host = hostname
|
},
|
||||||
}
|
GroupID: cmdGroupDefault,
|
||||||
},
|
DisableAutoGenTag: true,
|
||||||
DisableAutoGenTag: true,
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
term, cancel := setupTermstatus()
|
||||||
term, cancel := setupTermstatus()
|
defer cancel()
|
||||||
defer cancel()
|
return runBackup(cmd.Context(), opts, globalOptions, term, args)
|
||||||
return runBackup(cmd.Context(), backupOptions, globalOptions, term, args)
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackupOptions bundles all options for the backup command.
|
// BackupOptions bundles all options for the backup command.
|
||||||
type BackupOptions struct {
|
type BackupOptions struct {
|
||||||
excludePatternOptions
|
filter.ExcludePatternOptions
|
||||||
|
|
||||||
Parent string
|
Parent string
|
||||||
GroupBy restic.SnapshotGroupByOptions
|
GroupBy restic.SnapshotGroupByOptions
|
||||||
@@ -73,6 +85,7 @@ type BackupOptions struct {
|
|||||||
ExcludeIfPresent []string
|
ExcludeIfPresent []string
|
||||||
ExcludeCaches bool
|
ExcludeCaches bool
|
||||||
ExcludeLargerThan string
|
ExcludeLargerThan string
|
||||||
|
ExcludeCloudFiles bool
|
||||||
Stdin bool
|
Stdin bool
|
||||||
StdinFilename string
|
StdinFilename string
|
||||||
StdinCommand bool
|
StdinCommand bool
|
||||||
@@ -92,62 +105,60 @@ type BackupOptions struct {
|
|||||||
SkipIfUnchanged bool
|
SkipIfUnchanged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupOptions BackupOptions
|
func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.StringVar(&opts.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)")
|
||||||
|
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
||||||
|
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
|
||||||
|
f.BoolVarP(&opts.Force, "force", "f", false, `force re-reading the source files/directories (overrides the "parent" flag)`)
|
||||||
|
|
||||||
// ErrInvalidSourceData is used to report an incomplete backup
|
opts.ExcludePatternOptions.Add(f)
|
||||||
var ErrInvalidSourceData = errors.New("at least one source file could not be read")
|
|
||||||
|
|
||||||
func init() {
|
f.BoolVarP(&opts.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems, don't cross filesystem boundaries and subvolumes")
|
||||||
cmdRoot.AddCommand(cmdBackup)
|
f.StringArrayVar(&opts.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(&opts.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
|
||||||
f := cmdBackup.Flags()
|
f.StringVar(&opts.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
|
||||||
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)")
|
f.BoolVar(&opts.Stdin, "stdin", false, "read backup from stdin")
|
||||||
backupOptions.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
f.StringVar(&opts.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
|
||||||
f.VarP(&backupOptions.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
|
f.BoolVar(&opts.StdinCommand, "stdin-from-command", false, "interpret arguments as command to execute and store its stdout")
|
||||||
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the source files/directories (overrides the "parent" flag)`)
|
f.Var(&opts.Tags, "tag", "add `tags` for the new snapshot in the format `tag[,tag,...]` (can be specified multiple times)")
|
||||||
|
f.UintVar(&opts.ReadConcurrency, "read-concurrency", 0, "read `n` files concurrently (default: $RESTIC_READ_CONCURRENCY or 2)")
|
||||||
initExcludePatternOptions(f, &backupOptions.excludePatternOptions)
|
f.StringVarP(&opts.Host, "host", "H", "", "set the `hostname` for the snapshot manually (default: $RESTIC_HOST). To prevent an expensive rescan use the \"parent\" flag")
|
||||||
|
f.StringVar(&opts.Host, "hostname", "", "set the `hostname` for the snapshot manually")
|
||||||
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems, don't cross filesystem boundaries and subvolumes")
|
|
||||||
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes `filename[:header]`, exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
|
||||||
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
|
|
||||||
f.StringVar(&backupOptions.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
|
|
||||||
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
|
|
||||||
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
|
|
||||||
f.BoolVar(&backupOptions.StdinCommand, "stdin-from-command", false, "interpret arguments as command to execute and store its stdout")
|
|
||||||
f.Var(&backupOptions.Tags, "tag", "add `tags` for the new snapshot in the format `tag[,tag,...]` (can be specified multiple times)")
|
|
||||||
f.UintVar(&backupOptions.ReadConcurrency, "read-concurrency", 0, "read `n` files concurrently (default: $RESTIC_READ_CONCURRENCY or 2)")
|
|
||||||
f.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually (default: $RESTIC_HOST). To prevent an expensive rescan use the \"parent\" flag")
|
|
||||||
f.StringVar(&backupOptions.Host, "hostname", "", "set the `hostname` for the snapshot manually")
|
|
||||||
err := f.MarkDeprecated("hostname", "use --host")
|
err := f.MarkDeprecated("hostname", "use --host")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// MarkDeprecated only returns an error when the flag could not be found
|
// MarkDeprecated only returns an error when the flag could not be found
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
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.StringArrayVar(&opts.FilesFrom, "files-from", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
||||||
f.StringArrayVar(&backupOptions.FilesFromVerbatim, "files-from-verbatim", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
f.StringArrayVar(&opts.FilesFromVerbatim, "files-from-verbatim", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
||||||
f.StringArrayVar(&backupOptions.FilesFromRaw, "files-from-raw", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
f.StringArrayVar(&opts.FilesFromRaw, "files-from-raw", 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.StringVar(&opts.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(&opts.WithAtime, "with-atime", false, "store the atime for all files and directories")
|
||||||
f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files")
|
f.BoolVar(&opts.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files")
|
||||||
f.BoolVar(&backupOptions.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files")
|
f.BoolVar(&opts.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files")
|
||||||
f.BoolVarP(&backupOptions.DryRun, "dry-run", "n", false, "do not upload or write any data, just show what would be done")
|
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not upload or write any data, just show what would be done")
|
||||||
f.BoolVar(&backupOptions.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
|
f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
f.BoolVar(&backupOptions.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
|
f.BoolVar(&opts.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
|
||||||
|
f.BoolVar(&opts.ExcludeCloudFiles, "exclude-cloud-files", false, "excludes online-only cloud files (such as OneDrive Files On-Demand)")
|
||||||
}
|
}
|
||||||
f.BoolVar(&backupOptions.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot")
|
f.BoolVar(&opts.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot")
|
||||||
|
|
||||||
// parse read concurrency from env, on error the default value will be used
|
// parse read concurrency from env, on error the default value will be used
|
||||||
readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32)
|
readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32)
|
||||||
backupOptions.ReadConcurrency = uint(readConcurrency)
|
opts.ReadConcurrency = uint(readConcurrency)
|
||||||
|
|
||||||
// parse host from env, if not exists or empty the default value will be used
|
// parse host from env, if not exists or empty the default value will be used
|
||||||
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||||
backupOptions.Host = host
|
opts.Host = host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var backupFSTestHook func(fs fs.FS) fs.FS
|
||||||
|
|
||||||
|
// ErrInvalidSourceData is used to report an incomplete backup
|
||||||
|
var ErrInvalidSourceData = errors.New("at least one source file could not be read")
|
||||||
|
|
||||||
// filterExisting returns a slice of all existing items, or an error if no
|
// filterExisting returns a slice of all existing items, or an error if no
|
||||||
// items exist at all.
|
// items exist at all.
|
||||||
func filterExisting(items []string) (result []string, err error) {
|
func filterExisting(items []string) (result []string, err error) {
|
||||||
@@ -295,9 +306,9 @@ func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
||||||
// from being saved in a snapshot based on path only
|
// from being saved in a snapshot based on path only
|
||||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (fs []RejectByNameFunc, err error) {
|
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (fs []archiver.RejectByNameFunc, err error) {
|
||||||
// exclude restic cache
|
// exclude restic cache
|
||||||
if repo.Cache != nil {
|
if repo.Cache() != nil {
|
||||||
f, err := rejectResticCache(repo)
|
f, err := rejectResticCache(repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -306,23 +317,12 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
|
|||||||
fs = append(fs, f)
|
fs = append(fs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fsPatterns, err := opts.excludePatternOptions.CollectPatterns()
|
fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(Warnf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fs = append(fs, fsPatterns...)
|
for _, pat := range fsPatterns {
|
||||||
|
fs = append(fs, archiver.RejectByNameFunc(pat))
|
||||||
if opts.ExcludeCaches {
|
|
||||||
opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, spec := range opts.ExcludeIfPresent {
|
|
||||||
f, err := rejectIfPresent(spec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fs = append(fs, f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs, nil
|
return fs, nil
|
||||||
@@ -330,25 +330,54 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
|
|||||||
|
|
||||||
// collectRejectFuncs returns a list of all functions which may reject data
|
// collectRejectFuncs returns a list of all functions which may reject data
|
||||||
// from being saved in a snapshot based on path and file info
|
// from being saved in a snapshot based on path and file info
|
||||||
func collectRejectFuncs(opts BackupOptions, targets []string) (fs []RejectFunc, err error) {
|
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs []archiver.RejectFunc, err error) {
|
||||||
// allowed devices
|
// allowed devices
|
||||||
if opts.ExcludeOtherFS && !opts.Stdin {
|
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
|
||||||
f, err := rejectByDevice(targets)
|
f, err := archiver.RejectByDevice(targets, fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fs = append(fs, f)
|
funcs = append(funcs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opts.ExcludeLargerThan) != 0 && !opts.Stdin {
|
if len(opts.ExcludeLargerThan) != 0 && !opts.Stdin && !opts.StdinCommand {
|
||||||
f, err := rejectBySize(opts.ExcludeLargerThan)
|
maxSize, err := ui.ParseBytes(opts.ExcludeLargerThan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fs = append(fs, f)
|
|
||||||
|
f, err := archiver.RejectBySize(maxSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
funcs = append(funcs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs, nil
|
if opts.ExcludeCloudFiles && !opts.Stdin && !opts.StdinCommand {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
return nil, errors.Fatalf("exclude-cloud-files is only supported on Windows")
|
||||||
|
}
|
||||||
|
f, err := archiver.RejectCloudFiles(Warnf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
funcs = append(funcs, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ExcludeCaches {
|
||||||
|
opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, spec := range opts.ExcludeIfPresent {
|
||||||
|
f, err := archiver.RejectIfPresent(spec, Warnf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
funcs = append(funcs, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return funcs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectTargets returns a list of target files/dirs from several sources.
|
// collectTargets returns a list of target files/dirs from several sources.
|
||||||
@@ -503,12 +532,6 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
|
||||||
rejectFuncs, err := collectRejectFuncs(opts, targets)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var parentSnapshot *restic.Snapshot
|
var parentSnapshot *restic.Snapshot
|
||||||
if !opts.Stdin {
|
if !opts.Stdin {
|
||||||
parentSnapshot, err = findParentSnapshot(ctx, repo, opts, targets, timeStamp)
|
parentSnapshot, err = findParentSnapshot(ctx, repo, opts, targets, timeStamp)
|
||||||
@@ -530,30 +553,11 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
}
|
}
|
||||||
|
|
||||||
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
||||||
|
|
||||||
err = repo.LoadIndex(ctx, bar)
|
err = repo.LoadIndex(ctx, bar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetFS fs.FS = fs.Local{}
|
var targetFS fs.FS = fs.Local{}
|
||||||
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
|
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
|
||||||
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
|
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
|
||||||
@@ -587,15 +591,29 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
targetFS = &fs.Reader{
|
targetFS, err = fs.NewReader(filename, source, fs.ReaderOptions{
|
||||||
ModTime: timeStamp,
|
ModTime: timeStamp,
|
||||||
Name: filename,
|
Mode: 0644,
|
||||||
Mode: 0644,
|
})
|
||||||
ReadCloser: source,
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to backup from stdin: %w", err)
|
||||||
}
|
}
|
||||||
targets = []string{filename}
|
targets = []string{filename}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if backupFSTestHook != nil {
|
||||||
|
targetFS = backupFSTestHook(targetFS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
||||||
|
rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
selectByNameFilter := archiver.CombineRejectByNames(rejectByNameFuncs)
|
||||||
|
selectFilter := archiver.CombineRejects(rejectFuncs)
|
||||||
|
|
||||||
wg, wgCtx := errgroup.WithContext(ctx)
|
wg, wgCtx := errgroup.WithContext(ctx)
|
||||||
cancelCtx, cancel := context.WithCancel(wgCtx)
|
cancelCtx, cancel := context.WithCancel(wgCtx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
@@ -30,7 +31,7 @@ func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts
|
|||||||
|
|
||||||
func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) {
|
func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) {
|
||||||
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
|
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
|
||||||
rtest.Assert(t, err == nil, "Error while backing up")
|
rtest.Assert(t, err == nil, "Error while backing up: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackup(t *testing.T) {
|
func TestBackup(t *testing.T) {
|
||||||
@@ -51,14 +52,14 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
|||||||
opts := BackupOptions{UseFsSnapshot: useFsSnapshot}
|
opts := BackupOptions{UseFsSnapshot: useFsSnapshot}
|
||||||
|
|
||||||
// first backup
|
// first backup
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
stat1 := dirStats(env.repo)
|
stat1 := dirStats(env.repo)
|
||||||
|
|
||||||
// second backup, implicit incremental
|
// second backup, implicit incremental
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
snapshotIDs := testListSnapshots(t, env.gopts, 2)
|
snapshotIDs := testListSnapshots(t, env.gopts, 2)
|
||||||
|
|
||||||
stat2 := dirStats(env.repo)
|
stat2 := dirStats(env.repo)
|
||||||
@@ -70,7 +71,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
|||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
// third backup, explicit incremental
|
// third backup, explicit incremental
|
||||||
opts.Parent = snapshotIDs[0].String()
|
opts.Parent = snapshotIDs[0].String()
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
snapshotIDs = testListSnapshots(t, env.gopts, 3)
|
snapshotIDs = testListSnapshots(t, env.gopts, 3)
|
||||||
|
|
||||||
stat3 := dirStats(env.repo)
|
stat3 := dirStats(env.repo)
|
||||||
@@ -83,7 +84,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
|||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
testRunRestore(t, env.gopts, restoredir, snapshotID.String()+":"+toPathInSnapshot(filepath.Dir(env.testdata)))
|
||||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
||||||
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
|
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
|
||||||
}
|
}
|
||||||
@@ -91,6 +92,20 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
|||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toPathInSnapshot(path string) string {
|
||||||
|
// use path as is on most platforms, but convert it on windows
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// the path generated by the test is always local so take the shortcut
|
||||||
|
vol := filepath.VolumeName(path)
|
||||||
|
if vol[len(vol)-1] != ':' {
|
||||||
|
panic(fmt.Sprintf("unexpected path: %q", path))
|
||||||
|
}
|
||||||
|
path = vol[:len(vol)-1] + string(filepath.Separator) + path[len(vol)+1:]
|
||||||
|
path = filepath.ToSlash(path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
func TestBackupWithRelativePath(t *testing.T) {
|
func TestBackupWithRelativePath(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -111,6 +126,63 @@ func TestBackupWithRelativePath(t *testing.T) {
|
|||||||
rtest.Assert(t, latestSn.Parent != nil && latestSn.Parent.Equal(firstSnapshotID), "second snapshot selected unexpected parent %v instead of %v", latestSn.Parent, firstSnapshotID)
|
rtest.Assert(t, latestSn.Parent != nil && latestSn.Parent.Equal(firstSnapshotID), "second snapshot selected unexpected parent %v instead of %v", latestSn.Parent, firstSnapshotID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type vssDeleteOriginalFS struct {
|
||||||
|
fs.FS
|
||||||
|
testdata string
|
||||||
|
hasRemoved bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *vssDeleteOriginalFS) Lstat(name string) (*fs.ExtendedFileInfo, error) {
|
||||||
|
if !f.hasRemoved {
|
||||||
|
// call Lstat to trigger snapshot creation
|
||||||
|
_, _ = f.FS.Lstat(name)
|
||||||
|
// nuke testdata
|
||||||
|
var err error
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
// The CI sometimes runs into "The process cannot access the file because it is being used by another process" errors
|
||||||
|
// thus try a few times to remove the data
|
||||||
|
err = os.RemoveAll(f.testdata)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f.hasRemoved = true
|
||||||
|
}
|
||||||
|
return f.FS.Lstat(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupVSS(t *testing.T) {
|
||||||
|
if runtime.GOOS != "windows" || fs.HasSufficientPrivilegesForVSS() != nil {
|
||||||
|
t.Skip("vss fs test can only be run on windows with admin privileges")
|
||||||
|
}
|
||||||
|
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
testSetupBackupData(t, env)
|
||||||
|
opts := BackupOptions{UseFsSnapshot: true}
|
||||||
|
|
||||||
|
var testFS *vssDeleteOriginalFS
|
||||||
|
backupFSTestHook = func(fs fs.FS) fs.FS {
|
||||||
|
testFS = &vssDeleteOriginalFS{
|
||||||
|
FS: fs,
|
||||||
|
testdata: env.testdata,
|
||||||
|
}
|
||||||
|
return testFS
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
backupFSTestHook = nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
||||||
|
testListSnapshots(t, env.gopts, 1)
|
||||||
|
rtest.Equals(t, true, testFS.hasRemoved, "testdata was not removed")
|
||||||
|
}
|
||||||
|
|
||||||
func TestBackupParentSelection(t *testing.T) {
|
func TestBackupParentSelection(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -293,12 +365,7 @@ func TestBackupExclude(t *testing.T) {
|
|||||||
for _, filename := range backupExcludeFilenames {
|
for _, filename := range backupExcludeFilenames {
|
||||||
fp := filepath.Join(datadir, filename)
|
fp := filepath.Join(datadir, filename)
|
||||||
rtest.OK(t, os.MkdirAll(filepath.Dir(fp), 0755))
|
rtest.OK(t, os.MkdirAll(filepath.Dir(fp), 0755))
|
||||||
|
rtest.OK(t, os.WriteFile(fp, []byte(filename), 0o666))
|
||||||
f, err := os.Create(fp)
|
|
||||||
rtest.OK(t, err)
|
|
||||||
|
|
||||||
fmt.Fprint(f, filename)
|
|
||||||
rtest.OK(t, f.Close())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshots := make(map[string]struct{})
|
snapshots := make(map[string]struct{})
|
||||||
@@ -499,7 +566,7 @@ func TestHardLink(t *testing.T) {
|
|||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
testRunRestore(t, env.gopts, restoredir, snapshotID.String())
|
||||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
||||||
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
||||||
|
|
||||||
@@ -565,12 +632,15 @@ func TestStdinFromCommand(t *testing.T) {
|
|||||||
|
|
||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
opts := BackupOptions{
|
opts := BackupOptions{
|
||||||
StdinCommand: true,
|
StdinCommand: true,
|
||||||
StdinFilename: "stdin",
|
// test that subdirectories are handled correctly
|
||||||
|
StdinFilename: "stdin/subdir/file",
|
||||||
}
|
}
|
||||||
|
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"python", "-c", "import sys; print('something'); sys.exit(0)"}, opts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"python", "-c", "import sys; print('something'); sys.exit(0)"}, opts, env.gopts)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
files := testRunLs(t, env.gopts, snapshots[0].String())
|
||||||
|
rtest.Assert(t, includes(files, "/stdin/subdir/file"), "file %q missing from snapshot, got %v", "stdin/subdir/file", files)
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
@@ -39,21 +39,24 @@ func TestCollectTargets(t *testing.T) {
|
|||||||
f1, err := os.Create(filepath.Join(dir, "fromfile"))
|
f1, err := os.Create(filepath.Join(dir, "fromfile"))
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
// Empty lines should be ignored. A line starting with '#' is a comment.
|
// Empty lines should be ignored. A line starting with '#' is a comment.
|
||||||
fmt.Fprintf(f1, "\n%s*\n # here's a comment\n", f1.Name())
|
_, err = fmt.Fprintf(f1, "\n%s*\n # here's a comment\n", f1.Name())
|
||||||
|
rtest.OK(t, err)
|
||||||
rtest.OK(t, f1.Close())
|
rtest.OK(t, f1.Close())
|
||||||
|
|
||||||
f2, err := os.Create(filepath.Join(dir, "fromfile-verbatim"))
|
f2, err := os.Create(filepath.Join(dir, "fromfile-verbatim"))
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
for _, filename := range []string{fooSpace, barStar} {
|
for _, filename := range []string{fooSpace, barStar} {
|
||||||
// Empty lines should be ignored. CR+LF is allowed.
|
// Empty lines should be ignored. CR+LF is allowed.
|
||||||
fmt.Fprintf(f2, "%s\r\n\n", filepath.Join(dir, filename))
|
_, err = fmt.Fprintf(f2, "%s\r\n\n", filepath.Join(dir, filename))
|
||||||
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
rtest.OK(t, f2.Close())
|
rtest.OK(t, f2.Close())
|
||||||
|
|
||||||
f3, err := os.Create(filepath.Join(dir, "fromfile-raw"))
|
f3, err := os.Create(filepath.Join(dir, "fromfile-raw"))
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
for _, filename := range []string{"baz", "quux"} {
|
for _, filename := range []string{"baz", "quux"} {
|
||||||
fmt.Fprintf(f3, "%s\x00", filepath.Join(dir, filename))
|
_, err = fmt.Fprintf(f3, "%s\x00", filepath.Join(dir, filename))
|
||||||
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
rtest.OK(t, f3.Close())
|
rtest.OK(t, f3.Close())
|
||||||
|
@@ -10,16 +10,19 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/backend/cache"
|
"github.com/restic/restic/internal/backend/cache"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
"github.com/restic/restic/internal/ui/table"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdCache = &cobra.Command{
|
func newCacheCommand() *cobra.Command {
|
||||||
Use: "cache",
|
var opts CacheOptions
|
||||||
Short: "Operate on local cache directories",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "cache",
|
||||||
|
Short: "Operate on local cache directories",
|
||||||
|
Long: `
|
||||||
The "cache" command allows listing and cleaning local cache directories.
|
The "cache" command allows listing and cleaning local cache directories.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
@@ -28,10 +31,15 @@ EXIT STATUS
|
|||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful.
|
||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(_ *cobra.Command, args []string) error {
|
DisableAutoGenTag: true,
|
||||||
return runCache(cacheOptions, globalOptions, args)
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
},
|
return runCache(opts, globalOptions, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheOptions bundles all options for the snapshots command.
|
// CacheOptions bundles all options for the snapshots command.
|
||||||
@@ -41,15 +49,10 @@ type CacheOptions struct {
|
|||||||
NoSize bool
|
NoSize bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheOptions CacheOptions
|
func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&opts.Cleanup, "cleanup", false, "remove old cache directories")
|
||||||
func init() {
|
f.UintVar(&opts.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old")
|
||||||
cmdRoot.AddCommand(cmdCache)
|
f.BoolVar(&opts.NoSize, "no-size", false, "do not output the size of the cache directories")
|
||||||
|
|
||||||
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 {
|
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
||||||
@@ -88,7 +91,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
for _, item := range oldDirs {
|
for _, item := range oldDirs {
|
||||||
dir := filepath.Join(cachedir, item.Name())
|
dir := filepath.Join(cachedir, item.Name())
|
||||||
err = fs.RemoveAll(dir)
|
err = os.RemoveAll(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("unable to remove %v: %v\n", dir, err)
|
Warnf("unable to remove %v: %v\n", dir, err)
|
||||||
}
|
}
|
||||||
|
@@ -12,10 +12,13 @@ import (
|
|||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdCat = &cobra.Command{
|
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
|
||||||
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
|
|
||||||
Short: "Print internal objects to stdout",
|
func newCatCommand() *cobra.Command {
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
|
||||||
|
Short: "Print internal objects to stdout",
|
||||||
|
Long: `
|
||||||
The "cat" command is used to print internal objects to stdout.
|
The "cat" command is used to print internal objects to stdout.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
@@ -25,33 +28,32 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
DisableAutoGenTag: true,
|
||||||
return runCat(cmd.Context(), globalOptions, args)
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
},
|
return runCat(cmd.Context(), globalOptions, args)
|
||||||
}
|
},
|
||||||
|
ValidArgs: catAllowedCmds,
|
||||||
func init() {
|
}
|
||||||
cmdRoot.AddCommand(cmdCat)
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCatArgs(args []string) error {
|
func validateCatArgs(args []string) error {
|
||||||
var allowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
|
|
||||||
|
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return errors.Fatal("type not specified")
|
return errors.Fatal("type not specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
validType := false
|
validType := false
|
||||||
for _, v := range allowedCmds {
|
for _, v := range catAllowedCmds {
|
||||||
if v == args[0] {
|
if v == args[0] {
|
||||||
validType = true
|
validType = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !validType {
|
if !validType {
|
||||||
return errors.Fatalf("invalid type %q, must be one of [%s]", args[0], strings.Join(allowedCmds, "|"))
|
return errors.Fatalf("invalid type %q, must be one of [%s]", args[0], strings.Join(catAllowedCmds, "|"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if args[0] != "masterkey" && args[0] != "config" && len(args) != 2 {
|
if args[0] != "masterkey" && args[0] != "config" && len(args) != 2 {
|
||||||
|
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -10,11 +11,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend/cache"
|
"github.com/restic/restic/internal/backend/cache"
|
||||||
"github.com/restic/restic/internal/checker"
|
"github.com/restic/restic/internal/checker"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
@@ -22,10 +23,12 @@ import (
|
|||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui/termstatus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdCheck = &cobra.Command{
|
func newCheckCommand() *cobra.Command {
|
||||||
Use: "check [flags]",
|
var opts CheckOptions
|
||||||
Short: "Check the repository for errors",
|
cmd := &cobra.Command{
|
||||||
Long: `
|
Use: "check [flags]",
|
||||||
|
Short: "Check the repository for errors",
|
||||||
|
Long: `
|
||||||
The "check" command tests the repository for errors and reports any errors it
|
The "check" command tests the repository for errors and reports any errors it
|
||||||
finds. It can also be used to read all data and therefore simulate a restore.
|
finds. It can also be used to read all data and therefore simulate a restore.
|
||||||
|
|
||||||
@@ -39,16 +42,29 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
DisableAutoGenTag: true,
|
||||||
term, cancel := setupTermstatus()
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
defer cancel()
|
term, cancel := setupTermstatus()
|
||||||
return runCheck(cmd.Context(), checkOptions, globalOptions, args, term)
|
defer cancel()
|
||||||
},
|
summary, err := runCheck(cmd.Context(), opts, globalOptions, args, term)
|
||||||
PreRunE: func(_ *cobra.Command, _ []string) error {
|
if globalOptions.JSON {
|
||||||
return checkFlags(checkOptions)
|
if err != nil && summary.NumErrors == 0 {
|
||||||
},
|
summary.NumErrors = 1
|
||||||
|
}
|
||||||
|
term.Print(ui.ToJSONString(summary))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
PreRunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
return checkFlags(opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckOptions bundles all options for the 'check' command.
|
// CheckOptions bundles all options for the 'check' command.
|
||||||
@@ -59,14 +75,9 @@ type CheckOptions struct {
|
|||||||
WithCache bool
|
WithCache bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkOptions CheckOptions
|
func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&opts.ReadData, "read-data", false, "read all data blobs")
|
||||||
func init() {
|
f.StringVar(&opts.ReadDataSubset, "read-data-subset", "", "read a `subset` of data packs, specified as 'n/t' for specific part, or either 'x%' or 'x.y%' or a size in bytes with suffixes k/K, m/M, g/G, t/T for a random subset")
|
||||||
cmdRoot.AddCommand(cmdCheck)
|
|
||||||
|
|
||||||
f := cmdCheck.Flags()
|
|
||||||
f.BoolVar(&checkOptions.ReadData, "read-data", false, "read all data blobs")
|
|
||||||
f.StringVar(&checkOptions.ReadDataSubset, "read-data-subset", "", "read a `subset` of data packs, specified as 'n/t' for specific part, or either 'x%' or 'x.y%' or a size in bytes with suffixes k/K, m/M, g/G, t/T for a random subset")
|
|
||||||
var ignored bool
|
var ignored bool
|
||||||
f.BoolVar(&ignored, "check-unused", false, "find unused blobs")
|
f.BoolVar(&ignored, "check-unused", false, "find unused blobs")
|
||||||
err := f.MarkDeprecated("check-unused", "`--check-unused` is deprecated and will be ignored")
|
err := f.MarkDeprecated("check-unused", "`--check-unused` is deprecated and will be ignored")
|
||||||
@@ -74,7 +85,7 @@ func init() {
|
|||||||
// MarkDeprecated only returns an error when the flag is not found
|
// MarkDeprecated only returns an error when the flag is not found
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
f.BoolVar(&checkOptions.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository")
|
f.BoolVar(&opts.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkFlags(opts CheckOptions) error {
|
func checkFlags(opts CheckOptions) error {
|
||||||
@@ -200,7 +211,7 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
|
|||||||
printer.P("using temporary cache in %v\n", tempdir)
|
printer.P("using temporary cache in %v\n", tempdir)
|
||||||
|
|
||||||
cleanup = func() {
|
cleanup = func() {
|
||||||
err := fs.RemoveAll(tempdir)
|
err := os.RemoveAll(tempdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printer.E("error removing temporary cache directory: %v\n", err)
|
printer.E("error removing temporary cache directory: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -209,12 +220,18 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
|
|||||||
return cleanup
|
return cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args []string, term *termstatus.Terminal) error {
|
func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args []string, term *termstatus.Terminal) (checkSummary, error) {
|
||||||
|
summary := checkSummary{MessageType: "summary"}
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return errors.Fatal("the check command expects no arguments, only options - please see `restic help check` for usage and flags")
|
return summary, errors.Fatal("the check command expects no arguments, only options - please see `restic help check` for usage and flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
printer := newTerminalProgressPrinter(gopts.verbosity, term)
|
var printer progress.Printer
|
||||||
|
if !gopts.JSON {
|
||||||
|
printer = newTerminalProgressPrinter(gopts.verbosity, term)
|
||||||
|
} else {
|
||||||
|
printer = newJSONErrorPrinter(term)
|
||||||
|
}
|
||||||
|
|
||||||
cleanup := prepareCheckCache(opts, &gopts, printer)
|
cleanup := prepareCheckCache(opts, &gopts, printer)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -224,53 +241,43 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
}
|
}
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, gopts.NoLock)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, gopts.NoLock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return summary, err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
chkr := checker.New(repo, opts.CheckUnused)
|
chkr := checker.New(repo, opts.CheckUnused)
|
||||||
err = chkr.LoadSnapshots(ctx)
|
err = chkr.LoadSnapshots(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return summary, err
|
||||||
}
|
}
|
||||||
|
|
||||||
printer.P("load indexes\n")
|
printer.P("load indexes\n")
|
||||||
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
||||||
hints, errs := chkr.LoadIndex(ctx, bar)
|
hints, errs := chkr.LoadIndex(ctx, bar)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
errorsFound := false
|
errorsFound := false
|
||||||
suggestIndexRebuild := false
|
|
||||||
suggestLegacyIndexRebuild := false
|
|
||||||
mixedFound := false
|
|
||||||
for _, hint := range hints {
|
for _, hint := range hints {
|
||||||
switch hint.(type) {
|
switch hint.(type) {
|
||||||
case *checker.ErrDuplicatePacks:
|
case *checker.ErrDuplicatePacks:
|
||||||
term.Print(hint.Error())
|
printer.S("%s", hint.Error())
|
||||||
suggestIndexRebuild = true
|
summary.HintRepairIndex = true
|
||||||
case *checker.ErrOldIndexFormat:
|
|
||||||
printer.E("error: %v\n", hint)
|
|
||||||
suggestLegacyIndexRebuild = true
|
|
||||||
errorsFound = true
|
|
||||||
case *checker.ErrMixedPack:
|
case *checker.ErrMixedPack:
|
||||||
term.Print(hint.Error())
|
printer.S("%s", hint.Error())
|
||||||
mixedFound = true
|
summary.HintPrune = true
|
||||||
default:
|
default:
|
||||||
printer.E("error: %v\n", hint)
|
printer.E("error: %v\n", hint)
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if suggestIndexRebuild {
|
if summary.HintRepairIndex {
|
||||||
term.Print("Duplicate packs are non-critical, you can run `restic repair index' to correct this.\n")
|
printer.S("Duplicate packs are non-critical, you can run `restic repair index' to correct this.\n")
|
||||||
}
|
}
|
||||||
if suggestLegacyIndexRebuild {
|
if summary.HintPrune {
|
||||||
printer.E("error: Found indexes using the legacy format, you must run `restic repair index' to correct this.\n")
|
printer.S("Mixed packs with tree and data blobs are non-critical, you can run `restic prune` to correct this.\n")
|
||||||
}
|
|
||||||
if mixedFound {
|
|
||||||
term.Print("Mixed packs with tree and data blobs are non-critical, you can run `restic prune` to correct this.\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
@@ -278,8 +285,10 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
printer.E("error: %v\n", err)
|
printer.E("error: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summary.NumErrors += len(errs)
|
||||||
|
summary.HintRepairIndex = true
|
||||||
printer.E("\nThe repository index is damaged and must be repaired. You must run `restic repair index' to correct this.\n\n")
|
printer.E("\nThe repository index is damaged and must be repaired. You must run `restic repair index' to correct this.\n\n")
|
||||||
return errors.Fatal("repository contains errors")
|
return summary, errors.Fatal("repository contains errors")
|
||||||
}
|
}
|
||||||
|
|
||||||
orphanedPacks := 0
|
orphanedPacks := 0
|
||||||
@@ -300,23 +309,24 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
salvagePacks.Insert(packErr.ID)
|
salvagePacks.Insert(packErr.ID)
|
||||||
}
|
}
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
|
summary.NumErrors++
|
||||||
printer.E("%v\n", err)
|
printer.E("%v\n", err)
|
||||||
}
|
}
|
||||||
} else if err == checker.ErrLegacyLayout {
|
|
||||||
errorsFound = true
|
|
||||||
printer.E("error: repository still uses the S3 legacy layout\nYou must run `restic migrate s3legacy` to correct this.\n")
|
|
||||||
} else {
|
} else {
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
printer.E("%v\n", err)
|
printer.E("%v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if orphanedPacks > 0 && !errorsFound {
|
if orphanedPacks > 0 {
|
||||||
// hide notice if repository is damaged
|
summary.HintPrune = true
|
||||||
printer.P("%d additional files were found in the repo, which likely contain duplicate data.\nThis is non-critical, you can run `restic prune` to correct this.\n", orphanedPacks)
|
if !errorsFound {
|
||||||
|
// hide notice if repository is damaged
|
||||||
|
printer.P("%d additional files were found in the repo, which likely contain duplicate data.\nThis is non-critical, you can run `restic prune` to correct this.\n", orphanedPacks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
printer.P("check snapshots, trees and blobs\n")
|
printer.P("check snapshots, trees and blobs\n")
|
||||||
@@ -326,7 +336,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
bar := newTerminalProgressMax(!gopts.Quiet, 0, "snapshots", term)
|
bar := printer.NewCounter("snapshots")
|
||||||
defer bar.Done()
|
defer bar.Done()
|
||||||
chkr.Structure(ctx, bar, errChan)
|
chkr.Structure(ctx, bar, errChan)
|
||||||
}()
|
}()
|
||||||
@@ -336,9 +346,11 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
if e, ok := err.(*checker.TreeError); ok {
|
if e, ok := err.(*checker.TreeError); ok {
|
||||||
printer.E("error for tree %v:\n", e.ID.Str())
|
printer.E("error for tree %v:\n", e.ID.Str())
|
||||||
for _, treeErr := range e.Errors {
|
for _, treeErr := range e.Errors {
|
||||||
|
summary.NumErrors++
|
||||||
printer.E(" %v\n", treeErr)
|
printer.E(" %v\n", treeErr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
summary.NumErrors++
|
||||||
printer.E("error: %v\n", err)
|
printer.E("error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,13 +360,13 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
// deadlocking in the case of errors.
|
// deadlocking in the case of errors.
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.CheckUnused {
|
if opts.CheckUnused {
|
||||||
unused, err := chkr.UnusedBlobs(ctx)
|
unused, err := chkr.UnusedBlobs(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return summary, err
|
||||||
}
|
}
|
||||||
for _, id := range unused {
|
for _, id := range unused {
|
||||||
printer.P("unused blob %v\n", id)
|
printer.P("unused blob %v\n", id)
|
||||||
@@ -363,15 +375,15 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
}
|
}
|
||||||
|
|
||||||
doReadData := func(packs map[restic.ID]int64) {
|
doReadData := func(packs map[restic.ID]int64) {
|
||||||
packCount := uint64(len(packs))
|
p := printer.NewCounter("packs")
|
||||||
|
p.SetMax(uint64(len(packs)))
|
||||||
p := newTerminalProgressMax(!gopts.Quiet, packCount, "packs", term)
|
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
|
|
||||||
go chkr.ReadPacks(ctx, packs, p, errChan)
|
go chkr.ReadPacks(ctx, packs, p, errChan)
|
||||||
|
|
||||||
for err := range errChan {
|
for err := range errChan {
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
|
summary.NumErrors++
|
||||||
printer.E("%v\n", err)
|
printer.E("%v\n", err)
|
||||||
if err, ok := err.(*repository.ErrPackData); ok {
|
if err, ok := err.(*repository.ErrPackData); ok {
|
||||||
salvagePacks.Insert(err.PackID)
|
salvagePacks.Insert(err.PackID)
|
||||||
@@ -406,44 +418,43 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
repoSize += size
|
repoSize += size
|
||||||
}
|
}
|
||||||
if repoSize == 0 {
|
if repoSize == 0 {
|
||||||
return errors.Fatal("Cannot read from a repository having size 0")
|
return summary, errors.Fatal("Cannot read from a repository having size 0")
|
||||||
}
|
}
|
||||||
subsetSize, _ := ui.ParseBytes(opts.ReadDataSubset)
|
subsetSize, _ := ui.ParseBytes(opts.ReadDataSubset)
|
||||||
if subsetSize > repoSize {
|
if subsetSize > repoSize {
|
||||||
subsetSize = repoSize
|
subsetSize = repoSize
|
||||||
}
|
}
|
||||||
packs = selectRandomPacksByFileSize(chkr.GetPacks(), subsetSize, repoSize)
|
packs = selectRandomPacksByFileSize(chkr.GetPacks(), subsetSize, repoSize)
|
||||||
printer.P("read %d bytes of data packs\n", subsetSize)
|
percentage := float64(subsetSize) / float64(repoSize) * 100.0
|
||||||
|
printer.P("read %d bytes (%.1f%%) of data packs\n", subsetSize, percentage)
|
||||||
}
|
}
|
||||||
if packs == nil {
|
if packs == nil {
|
||||||
return errors.Fatal("internal error: failed to select packs to check")
|
return summary, errors.Fatal("internal error: failed to select packs to check")
|
||||||
}
|
}
|
||||||
doReadData(packs)
|
doReadData(packs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(salvagePacks) > 0 {
|
if len(salvagePacks) > 0 {
|
||||||
printer.E("\nThe repository contains damaged pack files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.\n\n")
|
printer.E("\nThe repository contains damaged pack files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.\n\n")
|
||||||
var strIDs []string
|
|
||||||
for id := range salvagePacks {
|
for id := range salvagePacks {
|
||||||
strIDs = append(strIDs, id.String())
|
summary.BrokenPacks = append(summary.BrokenPacks, id.String())
|
||||||
}
|
}
|
||||||
printer.E("restic repair packs %v\nrestic repair snapshots --forget\n\n", strings.Join(strIDs, " "))
|
printer.E("restic repair packs %v\nrestic repair snapshots --forget\n\n", strings.Join(summary.BrokenPacks, " "))
|
||||||
printer.E("Damaged pack files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!\n")
|
printer.E("Damaged pack files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorsFound {
|
if errorsFound {
|
||||||
if len(salvagePacks) == 0 {
|
if len(salvagePacks) == 0 {
|
||||||
printer.E("\nThe repository is damaged and must be repaired. Please follow the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html .\n\n")
|
printer.E("\nThe repository is damaged and must be repaired. Please follow the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html .\n\n")
|
||||||
}
|
}
|
||||||
return errors.Fatal("repository contains errors")
|
return summary, errors.Fatal("repository contains errors")
|
||||||
}
|
}
|
||||||
printer.P("no errors were found\n")
|
printer.P("no errors were found\n")
|
||||||
|
return summary, nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectPacksByBucket selects subsets of packs by ranges of buckets.
|
// selectPacksByBucket selects subsets of packs by ranges of buckets.
|
||||||
@@ -489,3 +500,42 @@ func selectRandomPacksByFileSize(allPacks map[restic.ID]int64, subsetSize int64,
|
|||||||
packs := selectRandomPacksByPercentage(allPacks, subsetPercentage)
|
packs := selectRandomPacksByPercentage(allPacks, subsetPercentage)
|
||||||
return packs
|
return packs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type checkSummary struct {
|
||||||
|
MessageType string `json:"message_type"` // "summary"
|
||||||
|
NumErrors int `json:"num_errors"`
|
||||||
|
BrokenPacks []string `json:"broken_packs"` // run "restic repair packs ID..." and "restic repair snapshots --forget" to remove damaged files
|
||||||
|
HintRepairIndex bool `json:"suggest_repair_index"` // run "restic repair index"
|
||||||
|
HintPrune bool `json:"suggest_prune"` // run "restic prune"
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkError struct {
|
||||||
|
MessageType string `json:"message_type"` // "error"
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonErrorPrinter struct {
|
||||||
|
term ui.Terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJSONErrorPrinter(term ui.Terminal) *jsonErrorPrinter {
|
||||||
|
return &jsonErrorPrinter{
|
||||||
|
term: term,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
||||||
|
status := checkError{
|
||||||
|
MessageType: "error",
|
||||||
|
Message: fmt.Sprintf(msg, args...),
|
||||||
|
}
|
||||||
|
p.term.Error(ui.ToJSONString(status))
|
||||||
|
}
|
||||||
|
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
|
||||||
|
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
|
||||||
|
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
|
||||||
|
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
|
||||||
|
@@ -32,7 +32,8 @@ func testRunCheckOutput(gopts GlobalOptions, checkUnused bool) (string, error) {
|
|||||||
ReadData: true,
|
ReadData: true,
|
||||||
CheckUnused: checkUnused,
|
CheckUnused: checkUnused,
|
||||||
}
|
}
|
||||||
return runCheck(context.TODO(), opts, gopts, nil, term)
|
_, err := runCheck(context.TODO(), opts, gopts, nil, term)
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
return buf.String(), err
|
return buf.String(), err
|
||||||
}
|
}
|
||||||
|
@@ -11,12 +11,15 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdCopy = &cobra.Command{
|
func newCopyCommand() *cobra.Command {
|
||||||
Use: "copy [flags] [snapshotID ...]",
|
var opts CopyOptions
|
||||||
Short: "Copy snapshots from one repository to another",
|
cmd := &cobra.Command{
|
||||||
Long: `
|
Use: "copy [flags] [snapshotID ...]",
|
||||||
|
Short: "Copy snapshots from one repository to another",
|
||||||
|
Long: `
|
||||||
The "copy" command copies one or more snapshots from one repository to another.
|
The "copy" command copies one or more snapshots from one repository to another.
|
||||||
|
|
||||||
NOTE: This process will have to both download (read) and upload (write) the
|
NOTE: This process will have to both download (read) and upload (write) the
|
||||||
@@ -38,10 +41,17 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
GroupID: cmdGroupDefault,
|
||||||
return runCopy(cmd.Context(), copyOptions, globalOptions, args)
|
DisableAutoGenTag: true,
|
||||||
},
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runCopy(cmd.Context(), opts, globalOptions, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyOptions bundles all options for the copy command.
|
// CopyOptions bundles all options for the copy command.
|
||||||
@@ -50,14 +60,9 @@ type CopyOptions struct {
|
|||||||
restic.SnapshotFilter
|
restic.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
var copyOptions CopyOptions
|
func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
opts.secondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from")
|
||||||
func init() {
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||||
cmdRoot.AddCommand(cmdCopy)
|
|
||||||
|
|
||||||
f := cmdCopy.Flags()
|
|
||||||
initSecondaryRepoOptions(f, ©Options.secondaryRepoOptions, "destination", "to copy snapshots from")
|
|
||||||
initMultiSnapshotFilter(f, ©Options.SnapshotFilter, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error {
|
func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error {
|
||||||
@@ -234,7 +239,15 @@ func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Rep
|
|||||||
}
|
}
|
||||||
|
|
||||||
bar := newProgressMax(!quiet, uint64(len(packList)), "packs copied")
|
bar := newProgressMax(!quiet, uint64(len(packList)), "packs copied")
|
||||||
_, err = repository.Repack(ctx, srcRepo, dstRepo, packList, copyBlobs, bar)
|
_, err = repository.Repack(
|
||||||
|
ctx,
|
||||||
|
srcRepo,
|
||||||
|
dstRepo,
|
||||||
|
packList,
|
||||||
|
copyBlobs,
|
||||||
|
bar,
|
||||||
|
func(msg string, args ...interface{}) { fmt.Printf(msg+"\n", args...) },
|
||||||
|
)
|
||||||
bar.Done()
|
bar.Done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatal(err.Error())
|
return errors.Fatal(err.Error())
|
||||||
|
@@ -62,11 +62,11 @@ func TestCopy(t *testing.T) {
|
|||||||
for i, snapshotID := range snapshotIDs {
|
for i, snapshotID := range snapshotIDs {
|
||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
origRestores[restoredir] = struct{}{}
|
origRestores[restoredir] = struct{}{}
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID)
|
testRunRestore(t, env.gopts, restoredir, snapshotID.String())
|
||||||
}
|
}
|
||||||
for i, snapshotID := range copiedSnapshotIDs {
|
for i, snapshotID := range copiedSnapshotIDs {
|
||||||
restoredir := filepath.Join(env2.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env2.base, fmt.Sprintf("restore%d", i))
|
||||||
testRunRestore(t, env2.gopts, restoredir, snapshotID)
|
testRunRestore(t, env2.gopts, restoredir, snapshotID.String())
|
||||||
foundMatch := false
|
foundMatch := false
|
||||||
for cmpdir := range origRestores {
|
for cmpdir := range origRestores {
|
||||||
diff := directoriesContentsDiff(restoredir, cmpdir)
|
diff := directoriesContentsDiff(restoredir, cmpdir)
|
||||||
|
@@ -18,6 +18,7 @@ import (
|
|||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/crypto"
|
"github.com/restic/restic/internal/crypto"
|
||||||
@@ -28,15 +29,29 @@ import (
|
|||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdDebug = &cobra.Command{
|
func registerDebugCommand(cmd *cobra.Command) {
|
||||||
Use: "debug",
|
cmd.AddCommand(
|
||||||
Short: "Debug commands",
|
newDebugCommand(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdDebugDump = &cobra.Command{
|
func newDebugCommand() *cobra.Command {
|
||||||
Use: "dump [indexes|snapshots|all|packs]",
|
cmd := &cobra.Command{
|
||||||
Short: "Dump data structures",
|
Use: "debug",
|
||||||
Long: `
|
Short: "Debug commands",
|
||||||
|
GroupID: cmdGroupDefault,
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
}
|
||||||
|
cmd.AddCommand(newDebugDumpCommand())
|
||||||
|
cmd.AddCommand(newDebugExamineCommand())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDebugDumpCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "dump [indexes|snapshots|all|packs]",
|
||||||
|
Short: "Dump data structures",
|
||||||
|
Long: `
|
||||||
The "dump" command dumps data structures from the repository as JSON objects. It
|
The "dump" command dumps data structures from the repository as JSON objects. It
|
||||||
is used for debugging purposes only.
|
is used for debugging purposes only.
|
||||||
|
|
||||||
@@ -47,11 +62,30 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDebugDump(cmd.Context(), globalOptions, args)
|
return runDebugDump(cmd.Context(), globalOptions, args)
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDebugExamineCommand() *cobra.Command {
|
||||||
|
var opts DebugExamineOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "examine pack-ID...",
|
||||||
|
Short: "Examine a pack file",
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runDebugExamine(cmd.Context(), globalOptions, opts, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type DebugExamineOptions struct {
|
type DebugExamineOptions struct {
|
||||||
@@ -61,16 +95,11 @@ type DebugExamineOptions struct {
|
|||||||
ReuploadBlobs bool
|
ReuploadBlobs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugExamineOpts DebugExamineOptions
|
func (opts *DebugExamineOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&opts.ExtractPack, "extract-pack", false, "write blobs to the current directory")
|
||||||
func init() {
|
f.BoolVar(&opts.ReuploadBlobs, "reupload-blobs", false, "reupload blobs to the repository")
|
||||||
cmdRoot.AddCommand(cmdDebug)
|
f.BoolVar(&opts.TryRepair, "try-repair", false, "try to repair broken blobs with single bit flips")
|
||||||
cmdDebug.AddCommand(cmdDebugDump)
|
f.BoolVar(&opts.RepairByte, "repair-byte", false, "try to repair broken blobs by trying bytes")
|
||||||
cmdDebug.AddCommand(cmdDebugExamine)
|
|
||||||
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.ExtractPack, "extract-pack", false, "write blobs to the current directory")
|
|
||||||
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.ReuploadBlobs, "reupload-blobs", false, "reupload blobs to the repository")
|
|
||||||
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.TryRepair, "try-repair", false, "try to repair broken blobs with single bit flips")
|
|
||||||
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.RepairByte, "repair-byte", false, "try to repair broken blobs by trying bytes")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prettyPrintJSON(wr io.Writer, item interface{}) error {
|
func prettyPrintJSON(wr io.Writer, item interface{}) error {
|
||||||
@@ -89,7 +118,9 @@ func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(wr, "snapshot_id: %v\n", id)
|
if _, err := fmt.Fprintf(wr, "snapshot_id: %v\n", id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return prettyPrintJSON(wr, snapshot)
|
return prettyPrintJSON(wr, snapshot)
|
||||||
})
|
})
|
||||||
@@ -140,7 +171,7 @@ func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer) error {
|
func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer) error {
|
||||||
return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, oldFormat bool, err error) error {
|
return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error {
|
||||||
Printf("index_id: %v\n", id)
|
Printf("index_id: %v\n", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -189,16 +220,7 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdDebugExamine = &cobra.Command{
|
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
||||||
Use: "examine pack-ID...",
|
|
||||||
Short: "Examine a pack file",
|
|
||||||
DisableAutoGenTag: true,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return runDebugExamine(cmd.Context(), globalOptions, debugExamineOpts, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, bytewise bool) []byte {
|
|
||||||
if bytewise {
|
if bytewise {
|
||||||
Printf(" trying to repair blob by finding a broken byte\n")
|
Printf(" trying to repair blob by finding a broken byte\n")
|
||||||
} else {
|
} else {
|
||||||
@@ -297,7 +319,7 @@ func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, by
|
|||||||
return fixed
|
return fixed
|
||||||
}
|
}
|
||||||
|
|
||||||
func decryptUnsigned(ctx context.Context, k *crypto.Key, buf []byte) []byte {
|
func decryptUnsigned(k *crypto.Key, buf []byte) []byte {
|
||||||
// strip signature at the end
|
// strip signature at the end
|
||||||
l := len(buf)
|
l := len(buf)
|
||||||
nonce, ct := buf[:16], buf[16:l-16]
|
nonce, ct := buf[:16], buf[16:l-16]
|
||||||
@@ -348,13 +370,13 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error decrypting blob: %v\n", err)
|
Warnf("error decrypting blob: %v\n", err)
|
||||||
if opts.TryRepair || opts.RepairByte {
|
if opts.TryRepair || opts.RepairByte {
|
||||||
plaintext = tryRepairWithBitflip(ctx, key, buf, opts.RepairByte)
|
plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte)
|
||||||
}
|
}
|
||||||
if plaintext != nil {
|
if plaintext != nil {
|
||||||
outputPrefix = "repaired "
|
outputPrefix = "repaired "
|
||||||
filePrefix = "repaired-"
|
filePrefix = "repaired-"
|
||||||
} else {
|
} else {
|
||||||
plaintext = decryptUnsigned(ctx, key, buf)
|
plaintext = decryptUnsigned(key, buf)
|
||||||
err = storePlainBlob(blob.ID, "damaged-", plaintext)
|
err = storePlainBlob(blob.ID, "damaged-", plaintext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
9
cmd/restic/cmd_debug_disabled.go
Normal file
9
cmd/restic/cmd_debug_disabled.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !debug
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
func registerDebugCommand(_ *cobra.Command) {
|
||||||
|
// No commands to register in non-debug mode
|
||||||
|
}
|
@@ -12,12 +12,16 @@ import (
|
|||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdDiff = &cobra.Command{
|
func newDiffCommand() *cobra.Command {
|
||||||
Use: "diff [flags] snapshotID snapshotID",
|
var opts DiffOptions
|
||||||
Short: "Show differences between two snapshots",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "diff [flags] snapshotID snapshotID",
|
||||||
|
Short: "Show differences between two snapshots",
|
||||||
|
Long: `
|
||||||
The "diff" command shows differences from the first to the second snapshot. The
|
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
|
first characters in each line display what has happened to a particular file or
|
||||||
directory:
|
directory:
|
||||||
@@ -43,11 +47,17 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
DisableAutoGenTag: true,
|
||||||
return runDiff(cmd.Context(), diffOptions, globalOptions, args)
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
},
|
return runDiff(cmd.Context(), opts, globalOptions, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiffOptions collects all options for the diff command.
|
// DiffOptions collects all options for the diff command.
|
||||||
@@ -55,13 +65,8 @@ type DiffOptions struct {
|
|||||||
ShowMetadata bool
|
ShowMetadata bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var diffOptions DiffOptions
|
func (opts *DiffOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&opts.ShowMetadata, "metadata", false, "print changes in metadata")
|
||||||
func init() {
|
|
||||||
cmdRoot.AddCommand(cmdDiff)
|
|
||||||
|
|
||||||
f := cmdDiff.Flags()
|
|
||||||
f.BoolVar(&diffOptions.ShowMetadata, "metadata", false, "print changes in metadata")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*restic.Snapshot, string, error) {
|
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*restic.Snapshot, string, error) {
|
||||||
@@ -106,9 +111,9 @@ func (s *DiffStat) Add(node *restic.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch node.Type {
|
switch node.Type {
|
||||||
case "file":
|
case restic.NodeTypeFile:
|
||||||
s.Files++
|
s.Files++
|
||||||
case "dir":
|
case restic.NodeTypeDir:
|
||||||
s.Dirs++
|
s.Dirs++
|
||||||
default:
|
default:
|
||||||
s.Others++
|
s.Others++
|
||||||
@@ -122,7 +127,7 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch node.Type {
|
switch node.Type {
|
||||||
case "file":
|
case restic.NodeTypeFile:
|
||||||
for _, blob := range node.Content {
|
for _, blob := range node.Content {
|
||||||
h := restic.BlobHandle{
|
h := restic.BlobHandle{
|
||||||
ID: blob,
|
ID: blob,
|
||||||
@@ -130,7 +135,7 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
|||||||
}
|
}
|
||||||
bs.Insert(h)
|
bs.Insert(h)
|
||||||
}
|
}
|
||||||
case "dir":
|
case restic.NodeTypeDir:
|
||||||
h := restic.BlobHandle{
|
h := restic.BlobHandle{
|
||||||
ID: *node.Subtree,
|
ID: *node.Subtree,
|
||||||
Type: restic.TreeBlob,
|
Type: restic.TreeBlob,
|
||||||
@@ -177,23 +182,27 @@ func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, b
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, node := range tree.Nodes {
|
for _, node := range tree.Nodes {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
name := path.Join(prefix, node.Name)
|
name := path.Join(prefix, node.Name)
|
||||||
if node.Type == "dir" {
|
if node.Type == restic.NodeTypeDir {
|
||||||
name += "/"
|
name += "/"
|
||||||
}
|
}
|
||||||
c.printChange(NewChange(name, mode))
|
c.printChange(NewChange(name, mode))
|
||||||
stats.Add(node)
|
stats.Add(node)
|
||||||
addBlobs(blobs, node)
|
addBlobs(blobs, node)
|
||||||
|
|
||||||
if node.Type == "dir" {
|
if node.Type == restic.NodeTypeDir {
|
||||||
err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree)
|
err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree)
|
||||||
if err != nil {
|
if err != nil && err != context.Canceled {
|
||||||
Warnf("error: %v\n", err)
|
Warnf("error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id restic.ID) error {
|
func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id restic.ID) error {
|
||||||
@@ -204,17 +213,21 @@ func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id rest
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, node := range tree.Nodes {
|
for _, node := range tree.Nodes {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
addBlobs(blobs, node)
|
addBlobs(blobs, node)
|
||||||
|
|
||||||
if node.Type == "dir" {
|
if node.Type == restic.NodeTypeDir {
|
||||||
err := c.collectDir(ctx, blobs, *node.Subtree)
|
err := c.collectDir(ctx, blobs, *node.Subtree)
|
||||||
if err != nil {
|
if err != nil && err != context.Canceled {
|
||||||
Warnf("error: %v\n", err)
|
Warnf("error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[string]*restic.Node, uniqueNames []string) {
|
func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[string]*restic.Node, uniqueNames []string) {
|
||||||
@@ -255,6 +268,10 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
|||||||
tree1Nodes, tree2Nodes, names := uniqueNodeNames(tree1, tree2)
|
tree1Nodes, tree2Nodes, names := uniqueNodeNames(tree1, tree2)
|
||||||
|
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
node1, t1 := tree1Nodes[name]
|
node1, t1 := tree1Nodes[name]
|
||||||
node2, t2 := tree2Nodes[name]
|
node2, t2 := tree2Nodes[name]
|
||||||
|
|
||||||
@@ -270,12 +287,12 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
|||||||
mod += "T"
|
mod += "T"
|
||||||
}
|
}
|
||||||
|
|
||||||
if node2.Type == "dir" {
|
if node2.Type == restic.NodeTypeDir {
|
||||||
name += "/"
|
name += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
if node1.Type == "file" &&
|
if node1.Type == restic.NodeTypeFile &&
|
||||||
node2.Type == "file" &&
|
node2.Type == restic.NodeTypeFile &&
|
||||||
!reflect.DeepEqual(node1.Content, node2.Content) {
|
!reflect.DeepEqual(node1.Content, node2.Content) {
|
||||||
mod += "M"
|
mod += "M"
|
||||||
stats.ChangedFiles++
|
stats.ChangedFiles++
|
||||||
@@ -297,49 +314,49 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
|||||||
c.printChange(NewChange(name, mod))
|
c.printChange(NewChange(name, mod))
|
||||||
}
|
}
|
||||||
|
|
||||||
if node1.Type == "dir" && node2.Type == "dir" {
|
if node1.Type == restic.NodeTypeDir && node2.Type == restic.NodeTypeDir {
|
||||||
var err error
|
var err error
|
||||||
if (*node1.Subtree).Equal(*node2.Subtree) {
|
if (*node1.Subtree).Equal(*node2.Subtree) {
|
||||||
err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree)
|
err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree)
|
||||||
} else {
|
} else {
|
||||||
err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil && err != context.Canceled {
|
||||||
Warnf("error: %v\n", err)
|
Warnf("error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case t1 && !t2:
|
case t1 && !t2:
|
||||||
prefix := path.Join(prefix, name)
|
prefix := path.Join(prefix, name)
|
||||||
if node1.Type == "dir" {
|
if node1.Type == restic.NodeTypeDir {
|
||||||
prefix += "/"
|
prefix += "/"
|
||||||
}
|
}
|
||||||
c.printChange(NewChange(prefix, "-"))
|
c.printChange(NewChange(prefix, "-"))
|
||||||
stats.Removed.Add(node1)
|
stats.Removed.Add(node1)
|
||||||
|
|
||||||
if node1.Type == "dir" {
|
if node1.Type == restic.NodeTypeDir {
|
||||||
err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree)
|
err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree)
|
||||||
if err != nil {
|
if err != nil && err != context.Canceled {
|
||||||
Warnf("error: %v\n", err)
|
Warnf("error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case !t1 && t2:
|
case !t1 && t2:
|
||||||
prefix := path.Join(prefix, name)
|
prefix := path.Join(prefix, name)
|
||||||
if node2.Type == "dir" {
|
if node2.Type == restic.NodeTypeDir {
|
||||||
prefix += "/"
|
prefix += "/"
|
||||||
}
|
}
|
||||||
c.printChange(NewChange(prefix, "+"))
|
c.printChange(NewChange(prefix, "+"))
|
||||||
stats.Added.Add(node2)
|
stats.Added.Add(node2)
|
||||||
|
|
||||||
if node2.Type == "dir" {
|
if node2.Type == restic.NodeTypeDir {
|
||||||
err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree)
|
err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree)
|
||||||
if err != nil {
|
if err != nil && err != context.Canceled {
|
||||||
Warnf("error: %v\n", err)
|
Warnf("error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []string) error {
|
func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []string) error {
|
||||||
@@ -448,8 +465,8 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
|||||||
Printf("Others: %5d new, %5d removed\n", stats.Added.Others, stats.Removed.Others)
|
Printf("Others: %5d new, %5d removed\n", stats.Added.Others, stats.Removed.Others)
|
||||||
Printf("Data Blobs: %5d new, %5d removed\n", stats.Added.DataBlobs, stats.Removed.DataBlobs)
|
Printf("Data Blobs: %5d new, %5d removed\n", stats.Added.DataBlobs, stats.Removed.DataBlobs)
|
||||||
Printf("Tree Blobs: %5d new, %5d removed\n", stats.Added.TreeBlobs, stats.Removed.TreeBlobs)
|
Printf("Tree Blobs: %5d new, %5d removed\n", stats.Added.TreeBlobs, stats.Removed.TreeBlobs)
|
||||||
Printf(" Added: %-5s\n", ui.FormatBytes(uint64(stats.Added.Bytes)))
|
Printf(" Added: %-5s\n", ui.FormatBytes(stats.Added.Bytes))
|
||||||
Printf(" Removed: %-5s\n", ui.FormatBytes(uint64(stats.Removed.Bytes)))
|
Printf(" Removed: %-5s\n", ui.FormatBytes(stats.Removed.Bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@@ -13,12 +13,15 @@ import (
|
|||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdDump = &cobra.Command{
|
func newDumpCommand() *cobra.Command {
|
||||||
Use: "dump [flags] snapshotID file",
|
var opts DumpOptions
|
||||||
Short: "Print a backed-up file to stdout",
|
cmd := &cobra.Command{
|
||||||
Long: `
|
Use: "dump [flags] snapshotID file",
|
||||||
|
Short: "Print a backed-up file to stdout",
|
||||||
|
Long: `
|
||||||
The "dump" command extracts files from a snapshot from the repository. If a
|
The "dump" command extracts files from a snapshot from the repository. If a
|
||||||
single file is selected, it prints its contents to stdout. Folders are output
|
single file is selected, it prints its contents to stdout. Folders are output
|
||||||
as a tar (default) or zip file containing the contents of the specified folder.
|
as a tar (default) or zip file containing the contents of the specified folder.
|
||||||
@@ -38,11 +41,17 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
DisableAutoGenTag: true,
|
||||||
return runDump(cmd.Context(), dumpOptions, globalOptions, args)
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
},
|
return runDump(cmd.Context(), opts, globalOptions, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// DumpOptions collects all options for the dump command.
|
// DumpOptions collects all options for the dump command.
|
||||||
@@ -52,15 +61,10 @@ type DumpOptions struct {
|
|||||||
Target string
|
Target string
|
||||||
}
|
}
|
||||||
|
|
||||||
var dumpOptions DumpOptions
|
func (opts *DumpOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
initSingleSnapshotFilter(f, &opts.SnapshotFilter)
|
||||||
func init() {
|
f.StringVarP(&opts.Archive, "archive", "a", "tar", "set archive `format` as \"tar\" or \"zip\"")
|
||||||
cmdRoot.AddCommand(cmdDump)
|
f.StringVarP(&opts.Target, "target", "t", "", "write the output to target `path`")
|
||||||
|
|
||||||
flags := cmdDump.Flags()
|
|
||||||
initSingleSnapshotFilter(flags, &dumpOptions.SnapshotFilter)
|
|
||||||
flags.StringVarP(&dumpOptions.Archive, "archive", "a", "tar", "set archive `format` as \"tar\" or \"zip\"")
|
|
||||||
flags.StringVarP(&dumpOptions.Target, "target", "t", "", "write the output to target `path`")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitPath(p string) []string {
|
func splitPath(p string) []string {
|
||||||
@@ -85,19 +89,23 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
|
|||||||
item := filepath.Join(prefix, pathComponents[0])
|
item := filepath.Join(prefix, pathComponents[0])
|
||||||
l := len(pathComponents)
|
l := len(pathComponents)
|
||||||
for _, node := range tree.Nodes {
|
for _, node := range tree.Nodes {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// If dumping something in the highest level it will just take the
|
// If dumping something in the highest level it will just take the
|
||||||
// first item it finds and dump that according to the switch case below.
|
// first item it finds and dump that according to the switch case below.
|
||||||
if node.Name == pathComponents[0] {
|
if node.Name == pathComponents[0] {
|
||||||
switch {
|
switch {
|
||||||
case l == 1 && dump.IsFile(node):
|
case l == 1 && node.Type == restic.NodeTypeFile:
|
||||||
return d.WriteNode(ctx, node)
|
return d.WriteNode(ctx, node)
|
||||||
case l > 1 && dump.IsDir(node):
|
case l > 1 && node.Type == restic.NodeTypeDir:
|
||||||
subtree, err := restic.LoadTree(ctx, repo, *node.Subtree)
|
subtree, err := restic.LoadTree(ctx, repo, *node.Subtree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
||||||
}
|
}
|
||||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], d, canWriteArchiveFunc)
|
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], d, canWriteArchiveFunc)
|
||||||
case dump.IsDir(node):
|
case node.Type == restic.NodeTypeDir:
|
||||||
if err := canWriteArchiveFunc(); err != nil {
|
if err := canWriteArchiveFunc(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -108,7 +116,7 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
|
|||||||
return d.DumpTree(ctx, subtree, item)
|
return d.DumpTree(ctx, subtree, item)
|
||||||
case l > 1:
|
case l > 1:
|
||||||
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
||||||
case !dump.IsFile(node):
|
case node.Type != restic.NodeTypeFile:
|
||||||
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,11 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var featuresCmd = &cobra.Command{
|
func newFeaturesCommand() *cobra.Command {
|
||||||
Use: "features",
|
cmd := &cobra.Command{
|
||||||
Short: "Print list of feature flags",
|
Use: "features",
|
||||||
Long: `
|
Short: "Print list of feature flags",
|
||||||
|
Long: `
|
||||||
The "features" command prints a list of supported feature flags.
|
The "features" command prints a list of supported feature flags.
|
||||||
|
|
||||||
To pass feature flags to restic, set the RESTIC_FEATURES environment variable
|
To pass feature flags to restic, set the RESTIC_FEATURES environment variable
|
||||||
@@ -31,29 +32,28 @@ EXIT STATUS
|
|||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful.
|
||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
`,
|
`,
|
||||||
Hidden: true,
|
GroupID: cmdGroupAdvanced,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(_ *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return errors.Fatal("the feature command expects no arguments")
|
return errors.Fatal("the feature command expects no arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("All Feature Flags:\n")
|
fmt.Printf("All Feature Flags:\n")
|
||||||
flags := feature.Flag.List()
|
flags := feature.Flag.List()
|
||||||
|
|
||||||
tab := table.New()
|
tab := table.New()
|
||||||
tab.AddColumn("Name", "{{ .Name }}")
|
tab.AddColumn("Name", "{{ .Name }}")
|
||||||
tab.AddColumn("Type", "{{ .Type }}")
|
tab.AddColumn("Type", "{{ .Type }}")
|
||||||
tab.AddColumn("Default", "{{ .Default }}")
|
tab.AddColumn("Default", "{{ .Default }}")
|
||||||
tab.AddColumn("Description", "{{ .Description }}")
|
tab.AddColumn("Description", "{{ .Description }}")
|
||||||
|
|
||||||
for _, flag := range flags {
|
for _, flag := range flags {
|
||||||
tab.AddRow(flag)
|
tab.AddRow(flag)
|
||||||
}
|
}
|
||||||
return tab.Write(globalOptions.stdout)
|
return tab.Write(globalOptions.stdout)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
return cmd
|
||||||
cmdRoot.AddCommand(featuresCmd)
|
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
@@ -16,14 +17,19 @@ import (
|
|||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdFind = &cobra.Command{
|
func newFindCommand() *cobra.Command {
|
||||||
Use: "find [flags] PATTERN...",
|
var opts FindOptions
|
||||||
Short: "Find a file, a directory or restic IDs",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
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
|
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.`,
|
It can also be used to search for restic blobs or trees for troubleshooting.
|
||||||
Example: `restic find config.json
|
The default sort option for the snapshots is youngest to oldest. To sort the
|
||||||
|
output from oldest to youngest specify --reverse.`,
|
||||||
|
Example: `restic find config.json
|
||||||
restic find --json "*.yml" "*.json"
|
restic find --json "*.yml" "*.json"
|
||||||
restic find --json --blob 420f620f b46ebe8a ddd38656
|
restic find --json --blob 420f620f b46ebe8a ddd38656
|
||||||
restic find --show-pack-id --blob 420f620f
|
restic find --show-pack-id --blob 420f620f
|
||||||
@@ -37,11 +43,17 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
DisableAutoGenTag: true,
|
||||||
return runFind(cmd.Context(), findOptions, globalOptions, args)
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
},
|
return runFind(cmd.Context(), opts, globalOptions, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindOptions bundles all options for the find command.
|
// FindOptions bundles all options for the find command.
|
||||||
@@ -54,27 +66,24 @@ type FindOptions struct {
|
|||||||
CaseInsensitive bool
|
CaseInsensitive bool
|
||||||
ListLong bool
|
ListLong bool
|
||||||
HumanReadable bool
|
HumanReadable bool
|
||||||
|
Reverse bool
|
||||||
restic.SnapshotFilter
|
restic.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
var findOptions FindOptions
|
func (opts *FindOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.StringVarP(&opts.Oldest, "oldest", "O", "", "oldest modification date/time")
|
||||||
|
f.StringVarP(&opts.Newest, "newest", "N", "", "newest modification date/time")
|
||||||
|
f.StringArrayVarP(&opts.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
|
||||||
|
f.BoolVar(&opts.BlobID, "blob", false, "pattern is a blob-ID")
|
||||||
|
f.BoolVar(&opts.TreeID, "tree", false, "pattern is a tree-ID")
|
||||||
|
f.BoolVar(&opts.PackID, "pack", false, "pattern is a pack-ID")
|
||||||
|
f.BoolVar(&opts.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)")
|
||||||
|
f.BoolVarP(&opts.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
|
||||||
|
f.BoolVarP(&opts.Reverse, "reverse", "R", false, "reverse sort order oldest to newest")
|
||||||
|
f.BoolVarP(&opts.ListLong, "long", "l", false, "use a long listing format showing size and mode")
|
||||||
|
f.BoolVar(&opts.HumanReadable, "human-readable", false, "print sizes in human readable format")
|
||||||
|
|
||||||
func init() {
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||||
cmdRoot.AddCommand(cmdFind)
|
|
||||||
|
|
||||||
f := cmdFind.Flags()
|
|
||||||
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
|
|
||||||
f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time")
|
|
||||||
f.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")
|
|
||||||
f.BoolVar(&findOptions.HumanReadable, "human-readable", false, "print sizes in human readable format")
|
|
||||||
|
|
||||||
initMultiSnapshotFilter(f, &findOptions.SnapshotFilter, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type findPattern struct {
|
type findPattern struct {
|
||||||
@@ -296,7 +305,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
var errIfNoMatch error
|
var errIfNoMatch error
|
||||||
if node.Type == "dir" {
|
if node.Type == restic.NodeTypeDir {
|
||||||
var childMayMatch bool
|
var childMayMatch bool
|
||||||
for _, pat := range f.pat.pattern {
|
for _, pat := range f.pat.pattern {
|
||||||
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
|
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
|
||||||
@@ -334,6 +343,26 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Finder) findTree(treeID restic.ID, nodepath string) error {
|
||||||
|
found := false
|
||||||
|
if _, ok := f.treeIDs[treeID.String()]; ok {
|
||||||
|
found = true
|
||||||
|
} else if _, ok := f.treeIDs[treeID.Str()]; ok {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
f.out.PrintObject("tree", treeID.String(), nodepath, "", f.out.newsn)
|
||||||
|
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 errors.New("OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
||||||
debug.Log("searching IDs in snapshot %s", sn.ID())
|
debug.Log("searching IDs in snapshot %s", sn.ID())
|
||||||
|
|
||||||
@@ -352,31 +381,26 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
|
if nodepath == "/" {
|
||||||
|
if err := f.findTree(parentTreeID, "/"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == "dir" && f.treeIDs != nil {
|
if node.Type == "dir" && f.treeIDs != nil {
|
||||||
treeID := node.Subtree
|
if err := f.findTree(*node.Subtree, nodepath); err != nil {
|
||||||
found := false
|
return err
|
||||||
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 errors.New("OK")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == "file" && f.blobIDs != nil {
|
if node.Type == restic.NodeTypeFile && f.blobIDs != nil {
|
||||||
for _, id := range node.Content {
|
for _, id := range node.Content {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
idStr := id.String()
|
idStr := id.String()
|
||||||
if _, ok := f.blobIDs[idStr]; !ok {
|
if _, ok := f.blobIDs[idStr]; !ok {
|
||||||
// Look for short ID form
|
// Look for short ID form
|
||||||
@@ -620,7 +644,10 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(filteredSnapshots, func(i, j int) bool {
|
sort.Slice(filteredSnapshots, func(i, j int) bool {
|
||||||
return filteredSnapshots[i].Time.Before(filteredSnapshots[j].Time)
|
if opts.Reverse {
|
||||||
|
return filteredSnapshots[i].Time.Before(filteredSnapshots[j].Time)
|
||||||
|
}
|
||||||
|
return filteredSnapshots[i].Time.After(filteredSnapshots[j].Time)
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, sn := range filteredSnapshots {
|
for _, sn := range filteredSnapshots {
|
||||||
|
@@ -10,11 +10,10 @@ import (
|
|||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunFind(t testing.TB, wantJSON bool, gopts GlobalOptions, pattern string) []byte {
|
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts GlobalOptions, pattern string) []byte {
|
||||||
buf, err := withCaptureStdout(func() error {
|
buf, err := withCaptureStdout(func() error {
|
||||||
gopts.JSON = wantJSON
|
gopts.JSON = wantJSON
|
||||||
|
|
||||||
opts := FindOptions{}
|
|
||||||
return runFind(context.TODO(), opts, gopts, []string{pattern})
|
return runFind(context.TODO(), opts, gopts, []string{pattern})
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
@@ -29,16 +28,15 @@ func TestFind(t *testing.T) {
|
|||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
|
||||||
|
|
||||||
results := testRunFind(t, false, env.gopts, "unexistingfile")
|
results := testRunFind(t, false, FindOptions{}, env.gopts, "unexistingfile")
|
||||||
rtest.Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile)
|
rtest.Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, false, env.gopts, "testfile")
|
results = testRunFind(t, false, FindOptions{}, env.gopts, "testfile")
|
||||||
lines := strings.Split(string(results), "\n")
|
lines := strings.Split(string(results), "\n")
|
||||||
rtest.Assert(t, len(lines) == 2, "expected one file found in repo (%v)", datafile)
|
rtest.Assert(t, len(lines) == 2, "expected one file found in repo (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, false, env.gopts, "testfile*")
|
results = testRunFind(t, false, FindOptions{}, env.gopts, "testfile*")
|
||||||
lines = strings.Split(string(results), "\n")
|
lines = strings.Split(string(results), "\n")
|
||||||
rtest.Assert(t, len(lines) == 4, "expected three files found in repo (%v)", datafile)
|
rtest.Assert(t, len(lines) == 4, "expected three files found in repo (%v)", datafile)
|
||||||
}
|
}
|
||||||
@@ -67,21 +65,69 @@ func TestFindJSON(t *testing.T) {
|
|||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
|
snapshot, _ := testRunSnapshots(t, env.gopts)
|
||||||
|
|
||||||
results := testRunFind(t, true, env.gopts, "unexistingfile")
|
results := testRunFind(t, true, FindOptions{}, env.gopts, "unexistingfile")
|
||||||
matches := []testMatches{}
|
matches := []testMatches{}
|
||||||
rtest.OK(t, json.Unmarshal(results, &matches))
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
rtest.Assert(t, len(matches) == 0, "expected no match in repo (%v)", datafile)
|
rtest.Assert(t, len(matches) == 0, "expected no match in repo (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, true, env.gopts, "testfile")
|
results = testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
|
||||||
rtest.OK(t, json.Unmarshal(results, &matches))
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
||||||
rtest.Assert(t, len(matches[0].Matches) == 1, "expected a single file to match (%v)", datafile)
|
rtest.Assert(t, len(matches[0].Matches) == 1, "expected a single file to match (%v)", datafile)
|
||||||
rtest.Assert(t, matches[0].Hits == 1, "expected hits to show 1 match (%v)", datafile)
|
rtest.Assert(t, matches[0].Hits == 1, "expected hits to show 1 match (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, true, env.gopts, "testfile*")
|
results = testRunFind(t, true, FindOptions{}, env.gopts, "testfile*")
|
||||||
rtest.OK(t, json.Unmarshal(results, &matches))
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
||||||
rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", datafile)
|
rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", datafile)
|
||||||
rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
|
rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
|
||||||
|
|
||||||
|
results = testRunFind(t, true, FindOptions{TreeID: true}, env.gopts, snapshot.Tree.String())
|
||||||
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
|
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", matches)
|
||||||
|
rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", matches[0].Matches)
|
||||||
|
rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSorting(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
datafile := testSetupBackupData(t, env)
|
||||||
|
opts := BackupOptions{}
|
||||||
|
|
||||||
|
// first backup
|
||||||
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
|
sn1 := testListSnapshots(t, env.gopts, 1)[0]
|
||||||
|
|
||||||
|
// second backup
|
||||||
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
|
snapshots := testListSnapshots(t, env.gopts, 2)
|
||||||
|
// get id of new snapshot without depending on file order returned by filesystem
|
||||||
|
sn2 := snapshots[0]
|
||||||
|
if sn1.Equal(sn2) {
|
||||||
|
sn2 = snapshots[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// first restic find - with default FindOptions{}
|
||||||
|
results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
|
||||||
|
lines := strings.Split(string(results), "\n")
|
||||||
|
rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines))
|
||||||
|
matches := []testMatches{}
|
||||||
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
|
|
||||||
|
// run second restic find with --reverse, sort oldest to newest
|
||||||
|
resultsReverse := testRunFind(t, true, FindOptions{Reverse: true}, env.gopts, "testfile")
|
||||||
|
lines = strings.Split(string(resultsReverse), "\n")
|
||||||
|
rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines))
|
||||||
|
matchesReverse := []testMatches{}
|
||||||
|
rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse))
|
||||||
|
|
||||||
|
// compare result sets
|
||||||
|
rtest.Assert(t, sn1.String() == matchesReverse[0].SnapshotID, "snapshot[0] must match old snapshot")
|
||||||
|
rtest.Assert(t, sn2.String() == matchesReverse[1].SnapshotID, "snapshot[1] must match new snapshot")
|
||||||
|
rtest.Assert(t, matches[0].SnapshotID == matchesReverse[1].SnapshotID, "matches should be sorted 1")
|
||||||
|
rtest.Assert(t, matches[1].SnapshotID == matchesReverse[0].SnapshotID, "matches should be sorted 2")
|
||||||
}
|
}
|
||||||
|
@@ -8,16 +8,20 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/feature"
|
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui/termstatus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdForget = &cobra.Command{
|
func newForgetCommand() *cobra.Command {
|
||||||
Use: "forget [flags] [snapshot ID] [...]",
|
var opts ForgetOptions
|
||||||
Short: "Remove snapshots from the repository",
|
var pruneOpts PruneOptions
|
||||||
Long: `
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "forget [flags] [snapshot ID] [...]",
|
||||||
|
Short: "Remove snapshots from the repository",
|
||||||
|
Long: `
|
||||||
The "forget" command removes snapshots according to a policy. All snapshots are
|
The "forget" command removes snapshots according to a policy. All snapshots are
|
||||||
first divided into groups according to "--group-by", and after that the policy
|
first divided into groups according to "--group-by", and after that the policy
|
||||||
specified by the "--keep-*" options is applied to each group individually.
|
specified by the "--keep-*" options is applied to each group individually.
|
||||||
@@ -37,20 +41,29 @@ EXIT STATUS
|
|||||||
|
|
||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful.
|
||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
|
Exit status is 3 if there was an error removing one or more snapshots.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
DisableAutoGenTag: true,
|
||||||
term, cancel := setupTermstatus()
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
defer cancel()
|
term, cancel := setupTermstatus()
|
||||||
return runForget(cmd.Context(), forgetOptions, forgetPruneOptions, globalOptions, term, args)
|
defer cancel()
|
||||||
},
|
return runForget(cmd.Context(), opts, pruneOpts, globalOptions, term, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
pruneOpts.AddLimitedFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForgetPolicyCount int
|
type ForgetPolicyCount int
|
||||||
|
|
||||||
var ErrNegativePolicyCount = errors.New("negative values not allowed, use 'unlimited' instead")
|
var ErrNegativePolicyCount = errors.New("negative values not allowed, use 'unlimited' instead")
|
||||||
|
var ErrFailedToRemoveOneOrMoreSnapshots = errors.New("failed to remove one or more snapshots")
|
||||||
|
|
||||||
func (c *ForgetPolicyCount) Set(s string) error {
|
func (c *ForgetPolicyCount) Set(s string) error {
|
||||||
switch s {
|
switch s {
|
||||||
@@ -110,44 +123,38 @@ type ForgetOptions struct {
|
|||||||
Prune bool
|
Prune bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var forgetOptions ForgetOptions
|
func (opts *ForgetOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
var forgetPruneOptions PruneOptions
|
f.VarP(&opts.Last, "keep-last", "l", "keep the last `n` snapshots (use 'unlimited' to keep all snapshots)")
|
||||||
|
f.VarP(&opts.Hourly, "keep-hourly", "H", "keep the last `n` hourly snapshots (use 'unlimited' to keep all hourly snapshots)")
|
||||||
|
f.VarP(&opts.Daily, "keep-daily", "d", "keep the last `n` daily snapshots (use 'unlimited' to keep all daily snapshots)")
|
||||||
|
f.VarP(&opts.Weekly, "keep-weekly", "w", "keep the last `n` weekly snapshots (use 'unlimited' to keep all weekly snapshots)")
|
||||||
|
f.VarP(&opts.Monthly, "keep-monthly", "m", "keep the last `n` monthly snapshots (use 'unlimited' to keep all monthly snapshots)")
|
||||||
|
f.VarP(&opts.Yearly, "keep-yearly", "y", "keep the last `n` yearly snapshots (use 'unlimited' to keep all yearly snapshots)")
|
||||||
|
f.VarP(&opts.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
|
f.VarP(&opts.WithinHourly, "keep-within-hourly", "", "keep hourly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
|
f.VarP(&opts.WithinDaily, "keep-within-daily", "", "keep daily snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
|
f.VarP(&opts.WithinWeekly, "keep-within-weekly", "", "keep weekly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
|
f.VarP(&opts.WithinMonthly, "keep-within-monthly", "", "keep monthly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
|
f.VarP(&opts.WithinYearly, "keep-within-yearly", "", "keep yearly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
||||||
|
f.Var(&opts.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
|
||||||
|
f.BoolVar(&opts.UnsafeAllowRemoveAll, "unsafe-allow-remove-all", false, "allow deleting all snapshots of a snapshot group")
|
||||||
|
|
||||||
func init() {
|
f.StringArrayVar(&opts.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)")
|
||||||
cmdRoot.AddCommand(cmdForget)
|
|
||||||
|
|
||||||
f := cmdForget.Flags()
|
|
||||||
f.VarP(&forgetOptions.Last, "keep-last", "l", "keep the last `n` snapshots (use 'unlimited' to keep all snapshots)")
|
|
||||||
f.VarP(&forgetOptions.Hourly, "keep-hourly", "H", "keep the last `n` hourly snapshots (use 'unlimited' to keep all hourly snapshots)")
|
|
||||||
f.VarP(&forgetOptions.Daily, "keep-daily", "d", "keep the last `n` daily snapshots (use 'unlimited' to keep all daily snapshots)")
|
|
||||||
f.VarP(&forgetOptions.Weekly, "keep-weekly", "w", "keep the last `n` weekly snapshots (use 'unlimited' to keep all weekly snapshots)")
|
|
||||||
f.VarP(&forgetOptions.Monthly, "keep-monthly", "m", "keep the last `n` monthly snapshots (use 'unlimited' to keep all monthly snapshots)")
|
|
||||||
f.VarP(&forgetOptions.Yearly, "keep-yearly", "y", "keep the last `n` yearly snapshots (use 'unlimited' to keep all yearly snapshots)")
|
|
||||||
f.VarP(&forgetOptions.Within, "keep-within", "", "keep snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
|
||||||
f.VarP(&forgetOptions.WithinHourly, "keep-within-hourly", "", "keep hourly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
|
||||||
f.VarP(&forgetOptions.WithinDaily, "keep-within-daily", "", "keep daily snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
|
||||||
f.VarP(&forgetOptions.WithinWeekly, "keep-within-weekly", "", "keep weekly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
|
||||||
f.VarP(&forgetOptions.WithinMonthly, "keep-within-monthly", "", "keep monthly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
|
||||||
f.VarP(&forgetOptions.WithinYearly, "keep-within-yearly", "", "keep yearly snapshots that are newer than `duration` (eg. 1y5m7d2h) relative to the latest snapshot")
|
|
||||||
f.Var(&forgetOptions.KeepTags, "keep-tag", "keep snapshots with this `taglist` (can be specified multiple times)")
|
|
||||||
f.BoolVar(&forgetOptions.UnsafeAllowRemoveAll, "unsafe-allow-remove-all", false, "allow deleting all snapshots of a snapshot group")
|
|
||||||
|
|
||||||
initMultiSnapshotFilter(f, &forgetOptions.SnapshotFilter, false)
|
|
||||||
f.StringArrayVar(&forgetOptions.Hosts, "hostname", nil, "only consider snapshots with the given `hostname` (can be specified multiple times)")
|
|
||||||
err := f.MarkDeprecated("hostname", "use --host")
|
err := f.MarkDeprecated("hostname", "use --host")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// MarkDeprecated only returns an error when the flag is not found
|
// MarkDeprecated only returns an error when the flag is not found
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
// must be defined after `--hostname` to not override the default value from the environment
|
||||||
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, false)
|
||||||
|
|
||||||
f.BoolVarP(&forgetOptions.Compact, "compact", "c", false, "use compact output format")
|
f.BoolVarP(&opts.Compact, "compact", "c", false, "use compact output format")
|
||||||
forgetOptions.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
||||||
f.VarP(&forgetOptions.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
|
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
|
||||||
f.BoolVarP(&forgetOptions.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
|
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
|
||||||
f.BoolVar(&forgetOptions.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
|
f.BoolVar(&opts.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
|
||||||
|
|
||||||
f.SortFlags = false
|
f.SortFlags = false
|
||||||
addPruneOptions(cmdForget, &forgetPruneOptions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyForgetOptions(opts *ForgetOptions) error {
|
func verifyForgetOptions(opts *ForgetOptions) error {
|
||||||
@@ -246,6 +253,10 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
printer.P("Applying Policy: %v\n", policy)
|
printer.P("Applying Policy: %v\n", policy)
|
||||||
|
|
||||||
for k, snapshotGroup := range snapshotGroups {
|
for k, snapshotGroup := range snapshotGroups {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
if gopts.Verbose >= 1 && !gopts.JSON {
|
if gopts.Verbose >= 1 && !gopts.JSON {
|
||||||
err = PrintSnapshotGroupHeader(globalOptions.stdout, k)
|
err = PrintSnapshotGroupHeader(globalOptions.stdout, k)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -265,7 +276,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
|
|
||||||
keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)
|
keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)
|
||||||
|
|
||||||
if feature.Flag.Enabled(feature.SafeForgetKeepTags) && !policy.Empty() && len(keep) == 0 {
|
if !policy.Empty() && len(keep) == 0 {
|
||||||
return fmt.Errorf("refusing to delete last snapshot of snapshot group \"%v\"", key.String())
|
return fmt.Errorf("refusing to delete last snapshot of snapshot group \"%v\"", key.String())
|
||||||
}
|
}
|
||||||
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
|
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
|
||||||
@@ -296,12 +307,15 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// these are the snapshots that failed to be removed
|
||||||
|
failedSnIDs := restic.NewIDSet()
|
||||||
if len(removeSnIDs) > 0 {
|
if len(removeSnIDs) > 0 {
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
bar := printer.NewCounter("files deleted")
|
bar := printer.NewCounter("files deleted")
|
||||||
err := restic.ParallelRemove(ctx, repo, removeSnIDs, restic.SnapshotFile, func(id restic.ID, err error) error {
|
err := restic.ParallelRemove(ctx, repo, removeSnIDs, restic.WriteableSnapshotFile, func(id restic.ID, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printer.E("unable to remove %v/%v from the repository\n", restic.SnapshotFile, id)
|
printer.E("unable to remove %v/%v from the repository\n", restic.SnapshotFile, id)
|
||||||
|
failedSnIDs.Insert(id)
|
||||||
} else {
|
} else {
|
||||||
printer.VV("removed %v/%v\n", restic.SnapshotFile, id)
|
printer.VV("removed %v/%v\n", restic.SnapshotFile, id)
|
||||||
}
|
}
|
||||||
@@ -323,6 +337,10 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(failedSnIDs) > 0 {
|
||||||
|
return ErrFailedToRemoveOneOrMoreSnapshots
|
||||||
|
}
|
||||||
|
|
||||||
if len(removeSnIDs) > 0 && opts.Prune {
|
if len(removeSnIDs) > 0 && opts.Prune {
|
||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
printer.P("%d snapshots would be removed, running prune dry run\n", len(removeSnIDs))
|
printer.P("%d snapshots would be removed, running prune dry run\n", len(removeSnIDs))
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestForgetPolicyValues(t *testing.T) {
|
func TestForgetPolicyValues(t *testing.T) {
|
||||||
@@ -92,3 +93,10 @@ func TestForgetOptionValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForgetHostnameDefaulting(t *testing.T) {
|
||||||
|
t.Setenv("RESTIC_HOST", "testhost")
|
||||||
|
opts := ForgetOptions{}
|
||||||
|
opts.AddFlags(pflag.NewFlagSet("test", pflag.ContinueOnError))
|
||||||
|
rtest.Equals(t, []string{"testhost"}, opts.Hosts)
|
||||||
|
}
|
||||||
|
@@ -1,17 +1,23 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/cobra/doc"
|
"github.com/spf13/cobra/doc"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdGenerate = &cobra.Command{
|
func newGenerateCommand() *cobra.Command {
|
||||||
Use: "generate [flags]",
|
var opts generateOptions
|
||||||
Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "generate [flags]",
|
||||||
|
Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)",
|
||||||
|
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, fish and zsh).
|
and the auto-completion files for bash, fish and zsh).
|
||||||
|
|
||||||
@@ -21,10 +27,13 @@ EXIT STATUS
|
|||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful.
|
||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(_ *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
return runGenerate(genOpts, args)
|
return runGenerate(opts, args)
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type generateOptions struct {
|
type generateOptions struct {
|
||||||
@@ -35,19 +44,15 @@ type generateOptions struct {
|
|||||||
PowerShellCompletionFile string
|
PowerShellCompletionFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
var genOpts generateOptions
|
func (opts *generateOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.StringVar(&opts.ManDir, "man", "", "write man pages to `directory`")
|
||||||
func init() {
|
f.StringVar(&opts.BashCompletionFile, "bash-completion", "", "write bash completion `file` (`-` for stdout)")
|
||||||
cmdRoot.AddCommand(cmdGenerate)
|
f.StringVar(&opts.FishCompletionFile, "fish-completion", "", "write fish completion `file` (`-` for stdout)")
|
||||||
fs := cmdGenerate.Flags()
|
f.StringVar(&opts.ZSHCompletionFile, "zsh-completion", "", "write zsh completion `file` (`-` for stdout)")
|
||||||
fs.StringVar(&genOpts.ManDir, "man", "", "write man pages to `directory`")
|
f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
|
||||||
fs.StringVar(&genOpts.BashCompletionFile, "bash-completion", "", "write bash completion `file`")
|
|
||||||
fs.StringVar(&genOpts.FishCompletionFile, "fish-completion", "", "write fish completion `file`")
|
|
||||||
fs.StringVar(&genOpts.ZSHCompletionFile, "zsh-completion", "", "write zsh completion `file`")
|
|
||||||
fs.StringVar(&genOpts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file`")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeManpages(dir string) error {
|
func writeManpages(root *cobra.Command, dir string) error {
|
||||||
// use a fixed date for the man pages so that generating them is deterministic
|
// use a fixed date for the man pages so that generating them is deterministic
|
||||||
date, err := time.Parse("Jan 2006", "Jan 2017")
|
date, err := time.Parse("Jan 2006", "Jan 2017")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -62,35 +67,47 @@ func writeManpages(dir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("writing man pages to directory %v\n", dir)
|
Verbosef("writing man pages to directory %v\n", dir)
|
||||||
return doc.GenManTree(cmdRoot, header, dir)
|
return doc.GenManTree(root, header, dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeBashCompletion(file string) error {
|
func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) {
|
||||||
if stdoutIsTerminal() {
|
if stdoutIsTerminal() {
|
||||||
Verbosef("writing bash completion file to %v\n", file)
|
Verbosef("writing %s completion file to %v\n", shell, filename)
|
||||||
}
|
}
|
||||||
return cmdRoot.GenBashCompletionFile(file)
|
var outWriter io.Writer
|
||||||
|
if filename != "-" {
|
||||||
|
var outFile *os.File
|
||||||
|
outFile, err = os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { err = outFile.Close() }()
|
||||||
|
outWriter = outFile
|
||||||
|
} else {
|
||||||
|
outWriter = globalOptions.stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
err = generate(outWriter)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeFishCompletion(file string) error {
|
func checkStdoutForSingleShell(opts generateOptions) error {
|
||||||
if stdoutIsTerminal() {
|
completionFileOpts := []string{
|
||||||
Verbosef("writing fish completion file to %v\n", file)
|
opts.BashCompletionFile,
|
||||||
|
opts.FishCompletionFile,
|
||||||
|
opts.ZSHCompletionFile,
|
||||||
|
opts.PowerShellCompletionFile,
|
||||||
}
|
}
|
||||||
return cmdRoot.GenFishCompletionFile(file, true)
|
seenIsStdout := false
|
||||||
}
|
for _, completionFileOpt := range completionFileOpts {
|
||||||
|
if completionFileOpt == "-" {
|
||||||
func writeZSHCompletion(file string) error {
|
if seenIsStdout {
|
||||||
if stdoutIsTerminal() {
|
return errors.Fatal("the generate command can generate shell completions to stdout for single shell only")
|
||||||
Verbosef("writing zsh completion file to %v\n", file)
|
}
|
||||||
|
seenIsStdout = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return cmdRoot.GenZshCompletionFile(file)
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
func writePowerShellCompletion(file string) error {
|
|
||||||
if stdoutIsTerminal() {
|
|
||||||
Verbosef("writing powershell completion file to %v\n", file)
|
|
||||||
}
|
|
||||||
return cmdRoot.GenPowerShellCompletionFile(file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runGenerate(opts generateOptions, args []string) error {
|
func runGenerate(opts generateOptions, args []string) error {
|
||||||
@@ -98,36 +115,43 @@ func runGenerate(opts generateOptions, args []string) error {
|
|||||||
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
|
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmdRoot := newRootCommand()
|
||||||
|
|
||||||
if opts.ManDir != "" {
|
if opts.ManDir != "" {
|
||||||
err := writeManpages(opts.ManDir)
|
err := writeManpages(cmdRoot, opts.ManDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := checkStdoutForSingleShell(opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if opts.BashCompletionFile != "" {
|
if opts.BashCompletionFile != "" {
|
||||||
err := writeBashCompletion(opts.BashCompletionFile)
|
err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.FishCompletionFile != "" {
|
if opts.FishCompletionFile != "" {
|
||||||
err := writeFishCompletion(opts.FishCompletionFile)
|
err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ZSHCompletionFile != "" {
|
if opts.ZSHCompletionFile != "" {
|
||||||
err := writeZSHCompletion(opts.ZSHCompletionFile)
|
err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.PowerShellCompletionFile != "" {
|
if opts.PowerShellCompletionFile != "" {
|
||||||
err := writePowerShellCompletion(opts.PowerShellCompletionFile)
|
err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
40
cmd/restic/cmd_generate_integration_test.go
Normal file
40
cmd/restic/cmd_generate_integration_test.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateStdout(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
opts generateOptions
|
||||||
|
}{
|
||||||
|
{"bash", generateOptions{BashCompletionFile: "-"}},
|
||||||
|
{"fish", generateOptions{FishCompletionFile: "-"}},
|
||||||
|
{"zsh", generateOptions{ZSHCompletionFile: "-"}},
|
||||||
|
{"powershell", generateOptions{PowerShellCompletionFile: "-"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
globalOptions.stdout = buf
|
||||||
|
err := runGenerate(tc.opts, []string{})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
completionString := buf.String()
|
||||||
|
rtest.Assert(t, strings.Contains(completionString, "# "+tc.name+" completion for restic"), "has no expected completion header")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Generate shell completions to stdout for two shells", func(t *testing.T) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
globalOptions.stdout = buf
|
||||||
|
opts := generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"}
|
||||||
|
err := runGenerate(opts, []string{})
|
||||||
|
rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails")
|
||||||
|
})
|
||||||
|
}
|
@@ -12,12 +12,16 @@ import (
|
|||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdInit = &cobra.Command{
|
func newInitCommand() *cobra.Command {
|
||||||
Use: "init",
|
var opts InitOptions
|
||||||
Short: "Initialize a new repository",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "Initialize a new repository",
|
||||||
|
Long: `
|
||||||
The "init" command initializes a new repository.
|
The "init" command initializes a new repository.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
@@ -26,10 +30,14 @@ EXIT STATUS
|
|||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful.
|
||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
DisableAutoGenTag: true,
|
||||||
return runInit(cmd.Context(), initOptions, globalOptions, args)
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
},
|
return runInit(cmd.Context(), opts, globalOptions, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitOptions bundles all options for the init command.
|
// InitOptions bundles all options for the init command.
|
||||||
@@ -39,15 +47,10 @@ type InitOptions struct {
|
|||||||
RepositoryVersion string
|
RepositoryVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
var initOptions InitOptions
|
func (opts *InitOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
opts.secondaryRepoOptions.AddFlags(f, "secondary", "to copy chunker parameters from")
|
||||||
func init() {
|
f.BoolVar(&opts.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
|
||||||
cmdRoot.AddCommand(cmdInit)
|
f.StringVar(&opts.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'")
|
||||||
|
|
||||||
f := cmdInit.Flags()
|
|
||||||
initSecondaryRepoOptions(f, &initOptions.secondaryRepoOptions, "secondary", "to copy chunker parameters from")
|
|
||||||
f.BoolVar(&initOptions.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
|
|
||||||
f.StringVar(&initOptions.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []string) error {
|
func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []string) error {
|
||||||
|
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
@@ -16,6 +18,11 @@ func testRunInit(t testing.TB, opts GlobalOptions) {
|
|||||||
|
|
||||||
rtest.OK(t, runInit(context.TODO(), InitOptions{}, opts, nil))
|
rtest.OK(t, runInit(context.TODO(), InitOptions{}, opts, nil))
|
||||||
t.Logf("repository initialized at %v", opts.Repo)
|
t.Logf("repository initialized at %v", opts.Repo)
|
||||||
|
|
||||||
|
// create temporary junk files to verify that restic does not trip over them
|
||||||
|
for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
|
||||||
|
rtest.OK(t, os.WriteFile(filepath.Join(opts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitCopyChunkerParams(t *testing.T) {
|
func TestInitCopyChunkerParams(t *testing.T) {
|
||||||
|
@@ -4,15 +4,23 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdKey = &cobra.Command{
|
func newKeyCommand() *cobra.Command {
|
||||||
Use: "key",
|
cmd := &cobra.Command{
|
||||||
Short: "Manage keys (passwords)",
|
Use: "key",
|
||||||
Long: `
|
Short: "Manage keys (passwords)",
|
||||||
|
Long: `
|
||||||
The "key" command allows you to set multiple access keys or passwords
|
The "key" command allows you to set multiple access keys or passwords
|
||||||
per repository.
|
per repository.
|
||||||
`,
|
`,
|
||||||
}
|
DisableAutoGenTag: true,
|
||||||
|
GroupID: cmdGroupDefault,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
cmd.AddCommand(
|
||||||
cmdRoot.AddCommand(cmdKey)
|
newKeyAddCommand(),
|
||||||
|
newKeyListCommand(),
|
||||||
|
newKeyPasswdCommand(),
|
||||||
|
newKeyRemoveCommand(),
|
||||||
|
)
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,13 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdKeyAdd = &cobra.Command{
|
func newKeyAddCommand() *cobra.Command {
|
||||||
Use: "add",
|
var opts KeyAddOptions
|
||||||
Short: "Add a new key (password) to the repository; returns the new key ID",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "add",
|
||||||
|
Short: "Add a new key (password) to the repository; returns the new key ID",
|
||||||
|
Long: `
|
||||||
The "add" sub-command creates a new key and validates the key. Returns the new key ID.
|
The "add" sub-command creates a new key and validates the key. Returns the new key ID.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
@@ -23,8 +26,16 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runKeyAdd(cmd.Context(), globalOptions, opts, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Add(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyAddOptions struct {
|
type KeyAddOptions struct {
|
||||||
@@ -41,16 +52,6 @@ func (opts *KeyAddOptions) Add(flags *pflag.FlagSet) {
|
|||||||
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
|
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
cmdKey.AddCommand(cmdKeyAdd)
|
|
||||||
|
|
||||||
var keyAddOpts KeyAddOptions
|
|
||||||
keyAddOpts.Add(cmdKeyAdd.Flags())
|
|
||||||
cmdKeyAdd.RunE = func(cmd *cobra.Command, args []string) error {
|
|
||||||
return runKeyAdd(cmd.Context(), globalOptions, keyAddOpts, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error {
|
func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")
|
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")
|
||||||
|
@@ -12,10 +12,11 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdKeyList = &cobra.Command{
|
func newKeyListCommand() *cobra.Command {
|
||||||
Use: "list",
|
cmd := &cobra.Command{
|
||||||
Short: "List keys (passwords)",
|
Use: "list",
|
||||||
Long: `
|
Short: "List keys (passwords)",
|
||||||
|
Long: `
|
||||||
The "list" sub-command lists all the keys (passwords) associated with the repository.
|
The "list" sub-command lists all the keys (passwords) associated with the repository.
|
||||||
Returns the key ID, username, hostname, created time and if it's the current key being
|
Returns the key ID, username, hostname, created time and if it's the current key being
|
||||||
used to access the repository.
|
used to access the repository.
|
||||||
@@ -27,15 +28,14 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runKeyList(cmd.Context(), globalOptions, args)
|
return runKeyList(cmd.Context(), globalOptions, args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
return cmd
|
||||||
func init() {
|
|
||||||
cmdKey.AddCommand(cmdKeyList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error {
|
func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||||
|
@@ -7,12 +7,16 @@ import (
|
|||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdKeyPasswd = &cobra.Command{
|
func newKeyPasswdCommand() *cobra.Command {
|
||||||
Use: "passwd",
|
var opts KeyPasswdOptions
|
||||||
Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "passwd",
|
||||||
|
Short: "Change key (password); creates a new key ID and removes the old key ID, returns new key ID",
|
||||||
|
Long: `
|
||||||
The "passwd" sub-command creates a new key, validates the key and remove the old key ID.
|
The "passwd" sub-command creates a new key, validates the key and remove the old key ID.
|
||||||
Returns the new key ID.
|
Returns the new key ID.
|
||||||
|
|
||||||
@@ -23,22 +27,24 @@ Exit status is 0 if the command was successful.
|
|||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runKeyPasswd(cmd.Context(), globalOptions, opts, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyPasswdOptions struct {
|
type KeyPasswdOptions struct {
|
||||||
KeyAddOptions
|
KeyAddOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func (opts *KeyPasswdOptions) AddFlags(flags *pflag.FlagSet) {
|
||||||
cmdKey.AddCommand(cmdKeyPasswd)
|
opts.KeyAddOptions.Add(flags)
|
||||||
|
|
||||||
var keyPasswdOpts KeyPasswdOptions
|
|
||||||
keyPasswdOpts.KeyAddOptions.Add(cmdKeyPasswd.Flags())
|
|
||||||
cmdKeyPasswd.RunE = func(cmd *cobra.Command, args []string) error {
|
|
||||||
return runKeyPasswd(cmd.Context(), globalOptions, keyPasswdOpts, args)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error {
|
func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user