mirror of
https://github.com/restic/restic.git
synced 2026-09-14 08:17:58 +00:00
Compare commits
682
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6335213501 | ||
|
|
6aa3a516ce | ||
|
|
b1da29738c | ||
|
|
09a1eb6714 | ||
|
|
c2b4d6a959 | ||
|
|
437945bf90 | ||
|
|
fc422d4145 | ||
|
|
f058670219 | ||
|
|
df8454770b | ||
|
|
c9185dd19c | ||
|
|
32be2e559b | ||
|
|
add4fa1efb | ||
|
|
c7c3414641 | ||
|
|
8ef295e2f5 | ||
|
|
bc714d1864 | ||
|
|
debf95f58d | ||
|
|
27c44c11aa | ||
|
|
93f695a803 | ||
|
|
3e21d1fea6 | ||
|
|
8ee674da55 | ||
|
|
c21f0fabc6 | ||
|
|
6aaa2eace4 | ||
|
|
f2d614db56 | ||
|
|
ed655b476a | ||
|
|
80f694b32e | ||
|
|
eaa835becd | ||
|
|
7aa82d68b4 | ||
|
|
a3fa3eb182 | ||
|
|
cb24c4f566 | ||
|
|
d6fd4ae26f | ||
|
|
4134bdfac2 | ||
|
|
c08e7fd956 | ||
|
|
d07bcd50da | ||
|
|
e6b9118f51 | ||
|
|
82a665384b | ||
|
|
1e249b0da1 | ||
|
|
5967f9a894 | ||
|
|
1733dd56b9 | ||
|
|
ed39b60e7d | ||
|
|
12875cc48e | ||
|
|
8c37a3983a | ||
|
|
d718c4cf20 | ||
|
|
60448342b3 | ||
|
|
4f59ed8806 | ||
|
|
522efd1175 | ||
|
|
f01359ff92 | ||
|
|
0b3c0fc222 | ||
|
|
ed571c8d12 | ||
|
|
0ddd98e03b | ||
|
|
cebc5c8957 | ||
|
|
724b7dc231 | ||
|
|
2141fd83a9 | ||
|
|
6693611497 | ||
|
|
0e9446277d | ||
|
|
b6bfa13f5f | ||
|
|
88aa8132ae | ||
|
|
bff8acb2d9 | ||
|
|
4ae2e26021 | ||
|
|
042feb2b7f | ||
|
|
474c094cac | ||
|
|
7566069bf3 | ||
|
|
53de77ea1a | ||
|
|
a54ac48f60 | ||
|
|
91a6e74fd5 | ||
|
|
e666980cec | ||
|
|
78b3411076 | ||
|
|
5b39ad861e | ||
|
|
f86307d223 | ||
|
|
77a6bf3bb7 | ||
|
|
c95ef18afb | ||
|
|
4c23c5946f | ||
|
|
640b2489f6 | ||
|
|
6a3f447327 | ||
|
|
ce24640d75 | ||
|
|
7e8f9a9221 | ||
|
|
62cf574fd8 | ||
|
|
32bcd92f60 | ||
|
|
02cf8e5f23 | ||
|
|
451cc6c048 | ||
|
|
c221cd06ad | ||
|
|
e7d3a4ae51 | ||
|
|
2a95591b48 | ||
|
|
c669cc7a7d | ||
|
|
2802bec8b9 | ||
|
|
f000da3b35 | ||
|
|
dd4f1f06a1 | ||
|
|
77f6795b32 | ||
|
|
850ab61cdf | ||
|
|
df03e8a0ec | ||
|
|
9299c45818 | ||
|
|
2b72f3eb42 | ||
|
|
496ea804a6 | ||
|
|
ccfb31b5fa | ||
|
|
a639b8d711 | ||
|
|
de55280003 | ||
|
|
990329013e | ||
|
|
10645ccd2a | ||
|
|
fa1a318780 | ||
|
|
8b6eff5a47 | ||
|
|
770ec1e25a | ||
|
|
febdb8b564 | ||
|
|
91c2aefbe1 | ||
|
|
3cc592463f | ||
|
|
c04a1d857d | ||
|
|
bd945df2ea | ||
|
|
5105015f5d | ||
|
|
d4397926cc | ||
|
|
5caa33e7b9 | ||
|
|
ef750c4c5d | ||
|
|
3148494a92 | ||
|
|
e212076f71 | ||
|
|
b16f0bf410 | ||
|
|
75f92f22d9 | ||
|
|
4c9a07e9c8 | ||
|
|
5bc7398e5e | ||
|
|
07bb05deaa | ||
|
|
4b52ef9a97 | ||
|
|
bd7f3483ac | ||
|
|
f6c8751575 | ||
|
|
2419c03177 | ||
|
|
b7d8c98214 | ||
|
|
265d070255 | ||
|
|
6de6cc6c75 | ||
|
|
8abbc3703d | ||
|
|
7acab7b1a9 | ||
|
|
5d70bf9f10 | ||
|
|
6c63c1c244 | ||
|
|
acc95531a4 | ||
|
|
bf14a94600 | ||
|
|
4547fd7b18 | ||
|
|
65d90641bb | ||
|
|
7878485eec | ||
|
|
d494e37dc1 | ||
|
|
59697213f9 | ||
|
|
df2d65bb88 | ||
|
|
bd8aad3b9b | ||
|
|
cf34130a05 | ||
|
|
e33bcede2f | ||
|
|
4f781b69f9 | ||
|
|
ff575a978d | ||
|
|
9a0432a5d7 | ||
|
|
f3854cf299 | ||
|
|
f3ca1a5c13 | ||
|
|
a213daef29 | ||
|
|
e3f065ad54 | ||
|
|
7ee77133fc | ||
|
|
1cdb0a9c96 | ||
|
|
f8afd4ed04 | ||
|
|
4a63f7fd13 | ||
|
|
10475c9841 | ||
|
|
5608e86cdb | ||
|
|
bdb2196378 | ||
|
|
4c94678d7d | ||
|
|
a241652787 | ||
|
|
a1c870c675 | ||
|
|
6080c784e4 | ||
|
|
ed418d8d60 | ||
|
|
93357440da | ||
|
|
5b7df8135e | ||
|
|
adce279d04 | ||
|
|
5c935e71fa | ||
|
|
934c615e51 | ||
|
|
ba638b6602 | ||
|
|
320f709fbc | ||
|
|
e33ed5d0c1 | ||
|
|
39084a912e | ||
|
|
24c3b73f7c | ||
|
|
f5ab39b33d | ||
|
|
ca555897f2 | ||
|
|
4c0dc9e202 | ||
|
|
b24d210b45 | ||
|
|
3dcd60d979 | ||
|
|
fbf6fad74f | ||
|
|
302e713c5d | ||
|
|
2ef86a8410 | ||
|
|
760610a29a | ||
|
|
48d7f232c2 | ||
|
|
25e72d3259 | ||
|
|
90ce78dbaa | ||
|
|
461bddb0e8 | ||
|
|
0f8c28aee4 | ||
|
|
1807d269cd | ||
|
|
b1e9811ffc | ||
|
|
54347eb6fa | ||
|
|
271d622823 | ||
|
|
541eb5cfd8 | ||
|
|
f9b63050eb | ||
|
|
49c988be07 | ||
|
|
9b1d45935a | ||
|
|
102ea6da2b | ||
|
|
4e71921a17 | ||
|
|
c5e09ae9b1 | ||
|
|
1f329cd933 | ||
|
|
a8f0ad5cc4 | ||
|
|
4c56384481 | ||
|
|
8b567a9270 | ||
|
|
27c560b371 | ||
|
|
66d915ef79 | ||
|
|
7077500a3b | ||
|
|
6566f786e9 | ||
|
|
d1937a530b | ||
|
|
5be6d9c73f | ||
|
|
7101f11133 | ||
|
|
8bff5cead0 | ||
|
|
5e43a44b15 | ||
|
|
67c13c643d | ||
|
|
b706c19614 | ||
|
|
da2ed89ffd | ||
|
|
cf3793bb41 | ||
|
|
db8e379fd4 | ||
|
|
4f73daa761 | ||
|
|
48cfa908ed | ||
|
|
d3c225627f | ||
|
|
07d380d54b | ||
|
|
b544e71cac | ||
|
|
099650f883 | ||
|
|
6154685c3a | ||
|
|
66bb196591 | ||
|
|
2be17d2313 | ||
|
|
34ba097162 | ||
|
|
38f1fb61f3 | ||
|
|
827c7bcae8 | ||
|
|
bcd4168428 | ||
|
|
901235efc9 | ||
|
|
ef1d525f22 | ||
|
|
74d60ad223 | ||
|
|
0d71f70a22 | ||
|
|
ee154ce0ab | ||
|
|
b6af01bb28 | ||
|
|
5148608c39 | ||
|
|
083cdf0675 | ||
|
|
ce7c144aac | ||
|
|
81948937ca | ||
|
|
fa8889eec4 | ||
|
|
6de64911fb | ||
|
|
17688c2313 | ||
|
|
e1a5550a27 | ||
|
|
24d56fe2a6 | ||
|
|
350f29d921 | ||
|
|
1e183509d4 | ||
|
|
25a5aa3520 | ||
|
|
278e457e1f | ||
|
|
f84d398989 | ||
|
|
d82ea53735 | ||
|
|
34fdf5ba96 | ||
|
|
70591f00ed | ||
|
|
4bc6bb7e27 | ||
|
|
2628daba97 | ||
|
|
2269ec82e1 | ||
|
|
86ccc6d445 | ||
|
|
d0a5d0e2f7 | ||
|
|
fa13f1895f | ||
|
|
880b08f9ec | ||
|
|
1368db5777 | ||
|
|
f78e3f369d | ||
|
|
39271a9984 | ||
|
|
2c1e8a0412 | ||
|
|
155372404a | ||
|
|
79c37f3d1a | ||
|
|
80531dbe53 | ||
|
|
40fe9f34e7 | ||
|
|
4d0ec87f35 | ||
|
|
d6f376b6c8 | ||
|
|
8179c4f676 | ||
|
|
ee099e4662 | ||
|
|
655e527765 | ||
|
|
d34ba9b6bd | ||
|
|
0673b58d7c | ||
|
|
9e2d60e28c | ||
|
|
ebc51e60c9 | ||
|
|
a9a13afcec | ||
|
|
d7b87cedbc | ||
|
|
a8be8e36fa | ||
|
|
74f72ec707 | ||
|
|
0b0b714b84 | ||
|
|
3df4582b2b | ||
|
|
a24184357e | ||
|
|
0d024ad046 | ||
|
|
3efd7b5fd0 | ||
|
|
4fd9bfc32b | ||
|
|
7a3b06f78a | ||
|
|
a58d176500 | ||
|
|
0af1257184 | ||
|
|
a3f1c65022 | ||
|
|
fa4ca9b5b4 | ||
|
|
ebdeecde42 | ||
|
|
1e6ed458ff | ||
|
|
760d0220f4 | ||
|
|
24fcfeafcb | ||
|
|
0ee9360f3e | ||
|
|
ae6d6bd9a6 | ||
|
|
b9afdf795e | ||
|
|
ce57961f14 | ||
|
|
e1bc2fb71a | ||
|
|
8fdbdc57a0 | ||
|
|
69ac0d84ac | ||
|
|
0a96f0d623 | ||
|
|
0d8b715d92 | ||
|
|
31e3717b25 | ||
|
|
42133ccffe | ||
|
|
77374b5bf0 | ||
|
|
f3a89bfff6 | ||
|
|
7696e4b495 | ||
|
|
5cc8636047 | ||
|
|
6769d26068 | ||
|
|
5607fd759f | ||
|
|
9f87e9096a | ||
|
|
d8dcd6d115 | ||
|
|
3f92987974 | ||
|
|
7f6fdcc52c | ||
|
|
dd6cb0dd8e | ||
|
|
046b0e711d | ||
|
|
4d2da63829 | ||
|
|
134893bd35 | ||
|
|
7b59dd7cf4 | ||
|
|
84dda4dc74 | ||
|
|
46ebee948f | ||
|
|
d91fe1d7e1 | ||
|
|
ff099a216a | ||
|
|
07d090f233 | ||
|
|
0f05277b47 | ||
|
|
7e80536a9b | ||
|
|
f9e5660e75 | ||
|
|
e79b01d82f | ||
|
|
857b42fca4 | ||
|
|
39db78446f | ||
|
|
f1aabdd293 | ||
|
|
50d376c543 | ||
|
|
7d08c9282a | ||
|
|
cf409b7c66 | ||
|
|
f95dc73d38 | ||
|
|
63bc1405ea | ||
|
|
405813f250 | ||
|
|
05364500b6 | ||
|
|
e775192fe7 | ||
|
|
4395a77154 | ||
|
|
81d8bc4ade | ||
|
|
d681b8af5e | ||
|
|
629eaa5d21 | ||
|
|
6174c91042 | ||
|
|
b24b088978 | ||
|
|
fc3de018bc | ||
|
|
b87f7586e4 | ||
|
|
dc4e9b31f6 | ||
|
|
8767549367 | ||
|
|
5afe61585b | ||
|
|
46f3ece883 | ||
|
|
96adbbaa42 | ||
|
|
7297047b71 | ||
|
|
132f2f8a23 | ||
|
|
a519d1e8df | ||
|
|
c1a89d5150 | ||
|
|
3826167474 | ||
|
|
98f56d8ada | ||
|
|
1caeb2aa4d | ||
|
|
3ab68d4d11 | ||
|
|
ffc5e9bd5c | ||
|
|
0ff3e20c4b | ||
|
|
3b854d9c04 | ||
|
|
87f26accb7 | ||
|
|
8fae46011a | ||
|
|
c854338ad1 | ||
|
|
10a10b8d63 | ||
|
|
7f3e3b77ce | ||
|
|
d81f95c777 | ||
|
|
2bd6649813 | ||
|
|
3b71c44755 | ||
|
|
1e3b96bf99 | ||
|
|
25611f4628 | ||
|
|
90ac3efa88 | ||
|
|
5b173d2206 | ||
|
|
14f3bc8232 | ||
|
|
4ef7b4676b | ||
|
|
b587c126e0 | ||
|
|
9944ef7a7c | ||
|
|
38c543457e | ||
|
|
393e49fc89 | ||
|
|
a0925fa922 | ||
|
|
b2afccbd96 | ||
|
|
0624b656b8 | ||
|
|
fadeb03f84 | ||
|
|
fc06a79518 | ||
|
|
d5977deb49 | ||
|
|
e3b7bbd020 | ||
|
|
157f174dd9 | ||
|
|
bcc5417dc8 | ||
|
|
d14823eb81 | ||
|
|
01bf8977e7 | ||
|
|
f5a18a7799 | ||
|
|
f756c6a441 | ||
|
|
8cbca05853 | ||
|
|
b0eb3652b8 | ||
|
|
71432c7f4b | ||
|
|
c6e33c3954 | ||
|
|
1ef785daa3 | ||
|
|
aa0fb0210a | ||
|
|
b6aef592f5 | ||
|
|
588c40aaef | ||
|
|
aa7bd241d9 | ||
|
|
536a2f38bd | ||
|
|
a816b827cf | ||
|
|
2c677d8db4 | ||
|
|
394c8de502 | ||
|
|
a632f490fa | ||
|
|
718b97f37f | ||
|
|
ac4642b479 | ||
|
|
20b38010e1 | ||
|
|
f9ff2301e8 | ||
|
|
e65ee3cba8 | ||
|
|
34a94afc48 | ||
|
|
9bcd09bde0 | ||
|
|
e80e832130 | ||
|
|
dd2d562b7b | ||
|
|
e320ef0a62 | ||
|
|
30ed992af9 | ||
|
|
481fcb9ca7 | ||
|
|
22f254c9ca | ||
|
|
f17027eeaa | ||
|
|
f3d95893b2 | ||
|
|
4759e58994 | ||
|
|
a2a49cf784 | ||
|
|
b7bbb408ee | ||
|
|
35fca09326 | ||
|
|
adbd4a1d18 | ||
|
|
537d107b6c | ||
|
|
06aa0f08cb | ||
|
|
3ae6a69154 | ||
|
|
264cd67c36 | ||
|
|
fd241b8ec7 | ||
|
|
76aa9e4f7c | ||
|
|
aae1acf4d7 | ||
|
|
cc0480fc32 | ||
|
|
838ef0a9bd | ||
|
|
4426dfe6a9 | ||
|
|
f0955fa931 | ||
|
|
189b295c30 | ||
|
|
82971ad7f0 | ||
|
|
bfc2ce97fd | ||
|
|
d84c3e3c60 | ||
|
|
93720f0717 | ||
|
|
70a24cca85 | ||
|
|
56ac8360c7 | ||
|
|
c85b157e0e | ||
|
|
13e476e1eb | ||
|
|
3335f62a8f | ||
|
|
d8da3d2f2d | ||
|
|
df7924f4df | ||
|
|
f2b9ea6455 | ||
|
|
711194276c | ||
|
|
f045297348 | ||
|
|
52eb66929f | ||
|
|
b459d66288 | ||
|
|
76b2cdd4fb | ||
|
|
c293736841 | ||
|
|
1939cff334 | ||
|
|
1a76f988ea | ||
|
|
e753941ad3 | ||
|
|
ff5a0cc851 | ||
|
|
013c565c29 | ||
|
|
96af35555a | ||
|
|
ca5b0c0249 | ||
|
|
3410808dcf | ||
|
|
1ae2d08d1b | ||
|
|
c745e4221e | ||
|
|
b6c50662da | ||
|
|
4dc71f24c5 | ||
|
|
13f743e26b | ||
|
|
3e1632c412 | ||
|
|
6bd85d2412 | ||
|
|
e4395a9d73 | ||
|
|
4d1f6b1fe2 | ||
|
|
331260e1d4 | ||
|
|
eb13789b2b | ||
|
|
0cd079147f | ||
|
|
0b4b092941 | ||
|
|
01d3357880 | ||
|
|
1c7bb15327 | ||
|
|
d491c1bdbf | ||
|
|
97933d1404 | ||
|
|
4edfd36c8f | ||
|
|
a30a36ca51 | ||
|
|
d52f92e8cc | ||
|
|
a4e565d921 | ||
|
|
ec796e6edd | ||
|
|
e30acefbff | ||
|
|
3e6b5c34c9 | ||
|
|
9017fefddd | ||
|
|
93d1e3b211 | ||
|
|
8f858829ed | ||
|
|
db3b3e31e6 | ||
|
|
3f7121e180 | ||
|
|
d5dd8ce6a7 | ||
|
|
08443fe593 | ||
|
|
daeb55a4fb | ||
|
|
6ebc23543d | ||
|
|
7257cd2e5f | ||
|
|
88bdf20bd8 | ||
|
|
8518c1f7d9 | ||
|
|
60d80a6127 | ||
|
|
575eac8d80 | ||
|
|
5c667f0501 | ||
|
|
f091e6aed0 | ||
|
|
39a737fe14 | ||
|
|
4871390a81 | ||
|
|
65b21e3348 | ||
|
|
4a7b122fb6 | ||
|
|
86ddee8518 | ||
|
|
2fe271980f | ||
|
|
4f1390436d | ||
|
|
2d7611373e | ||
|
|
f71278138f | ||
|
|
7d5ebdd0b3 | ||
|
|
d6c75ba2dc | ||
|
|
2a9105c050 | ||
|
|
8e87a37df0 | ||
|
|
a8f506ea4d | ||
|
|
0a1ce4f207 | ||
|
|
364271c6c3 | ||
|
|
6b5c8ce14e | ||
|
|
5a16b29177 | ||
|
|
320fb5fb98 | ||
|
|
c14cf48776 | ||
|
|
109a211fbe | ||
|
|
9d3efc2088 | ||
|
|
8b5dbc18ca | ||
|
|
b0eef4b965 | ||
|
|
6c0dccf4a5 | ||
|
|
6b23d0328b | ||
|
|
52f33d2d54 | ||
|
|
d89535634d | ||
|
|
902cd1e9d6 | ||
|
|
51299b8ea7 | ||
|
|
fd8f8d64f5 | ||
|
|
114cc33fe9 | ||
|
|
44dbd4469e | ||
|
|
d8f3e35730 | ||
|
|
333dbd18d8 | ||
|
|
0226e46681 | ||
|
|
74fb43e0c2 | ||
|
|
69186350fc | ||
|
|
3e7aad8916 | ||
|
|
c3912ae7bc | ||
|
|
d3e26f2868 | ||
|
|
2e91e81c83 | ||
|
|
0dcd9bee88 | ||
|
|
a304826b98 | ||
|
|
8510f09225 | ||
|
|
e63aee2ec6 | ||
|
|
94b19d64be | ||
|
|
03600ca509 | ||
|
|
ef9930cce4 | ||
|
|
91ecac8003 | ||
|
|
e9b6149303 | ||
|
|
32b7168a9e | ||
|
|
6cdb9a75e6 | ||
|
|
81fe559222 | ||
|
|
f21fd9d115 | ||
|
|
d757e39992 | ||
|
|
ce089f7e2d | ||
|
|
576d35b37b | ||
|
|
18b8f8870f | ||
|
|
79c41966af | ||
|
|
c0a30e12b4 | ||
|
|
de29d74707 | ||
|
|
424316e016 | ||
|
|
b71b77fa77 | ||
|
|
e7890d7b81 | ||
|
|
529baf50f8 | ||
|
|
d10bd1d321 | ||
|
|
43b5166de8 | ||
|
|
0b0dd07f15 | ||
|
|
93ccc548c8 | ||
|
|
0ab38faa2e | ||
|
|
48cbbf9651 | ||
|
|
6ff7cd9050 | ||
|
|
cc1fe6c111 | ||
|
|
1ed93bd54d | ||
|
|
c55df65bb6 | ||
|
|
289adebbb7 | ||
|
|
eef047cfa4 | ||
|
|
7f1ac5764a | ||
|
|
52aa1cd17f | ||
|
|
42f690dbab | ||
|
|
914bd699be | ||
|
|
4c19d6410f | ||
|
|
2ad703bfd8 | ||
|
|
0864d04c5c | ||
|
|
839c38b4c4 | ||
|
|
e98c44baaf | ||
|
|
01bc60e96f | ||
|
|
484b706dd8 | ||
|
|
350f6452e7 | ||
|
|
484cdf12e5 | ||
|
|
c8bb7bd312 | ||
|
|
a8ce2e45cc | ||
|
|
275507fb3e | ||
|
|
391c27975a | ||
|
|
7d47e60e27 | ||
|
|
5c17c277f3 | ||
|
|
48e5c0984e | ||
|
|
2414771a59 | ||
|
|
0e381bdbf1 | ||
|
|
11cd4a0a88 | ||
|
|
f54989f634 | ||
|
|
af7f10d16b | ||
|
|
6de95a3d58 | ||
|
|
93f436e999 | ||
|
|
6edb199efd | ||
|
|
9b2c0a0c54 | ||
|
|
64273ea027 | ||
|
|
5a00d26431 | ||
|
|
3faad5751d | ||
|
|
f487eb1c66 | ||
|
|
72636238d0 | ||
|
|
51098157e2 | ||
|
|
0b080c44d7 | ||
|
|
b71fe91643 | ||
|
|
9c3b8d171a | ||
|
|
ddb7fb837b | ||
|
|
3433c5abac | ||
|
|
09bc58c950 | ||
|
|
20eb9018a0 | ||
|
|
651f553530 | ||
|
|
aad4b53ead | ||
|
|
f7f6459eb9 | ||
|
|
95a36b55f4 | ||
|
|
2c39b1f84f | ||
|
|
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 |
@@ -0,0 +1,25 @@
|
||||
[codespell]
|
||||
|
||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||
# Skip historical content (CHANGELOG, release notes archive), auto-generated
|
||||
# man pages (which contain troff macros like \(bu that look like typos), and
|
||||
# files we don't want to modify (go.sum, css with vendor prefixes).
|
||||
skip = .git*,.gitignore,.gitattributes,go.sum,*.css,.codespellrc,CHANGELOG.md,changelog,*.1
|
||||
check-hidden = true
|
||||
|
||||
# Protect URLs from corrections (URLs may contain words flagged as typos and
|
||||
# must not be changed); also ignore camelCase/PascalCase identifiers like
|
||||
# AtLeast which are valid Go identifiers, not typos.
|
||||
ignore-regex = https?://\S+|\b[a-z]+[A-Z]\w*\b|\b[A-Z][a-z]+[A-Z]\w*\b
|
||||
|
||||
ignore-words-list =
|
||||
# variable name short for "serialized", also matches [uU]ser glob pattern
|
||||
ser,
|
||||
# German word "ist" appearing in test data string
|
||||
ist,
|
||||
# intentional truncated test string in format truncation tests
|
||||
fo,
|
||||
# CLI flag name (--iinclude, case-insensitive include)
|
||||
iinclude,
|
||||
# intentional in fake test path /doesnt/exist
|
||||
doesnt
|
||||
@@ -0,0 +1,12 @@
|
||||
# Actual layer caching is impossible due to .git, but
|
||||
# that must be included for provenance reasons. These ignores
|
||||
# are strictly for hygienic build.
|
||||
*
|
||||
!/*.go
|
||||
!/go.*
|
||||
!/cmd/*
|
||||
!/docker/entrypoint.sh
|
||||
!/internal/*
|
||||
!/helpers/*
|
||||
!/VERSION
|
||||
!/.git/
|
||||
@@ -36,7 +36,7 @@ Please always follow these steps:
|
||||
- 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 added tests for all code changes.
|
||||
- [ ] I have added tests for all code changes, see [writing tests](https://restic.readthedocs.io/en/stable/090_participating.html#writing-tests)
|
||||
- [ ] 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)).
|
||||
- [ ] I'm done! This pull request is ready for review.
|
||||
|
||||
@@ -5,6 +5,10 @@ updates:
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
golang-x-deps:
|
||||
patterns:
|
||||
- "golang.org/x/*"
|
||||
|
||||
# Dependencies listed in .github/workflows/*.yml
|
||||
- package-ecosystem: "github-actions"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Codespell configuration is within .codespellrc
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
# run checks on push to master, but not when other branches are pushed to
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
# run checks for all pull requests
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
|
||||
@@ -26,10 +26,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
@@ -47,10 +47,10 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
|
||||
|
||||
- name: Ensure consistent binaries
|
||||
run: |
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
|
||||
+21
-19
@@ -13,7 +13,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
latest_go: "1.24.x"
|
||||
latest_go: "1.26.x"
|
||||
GO111MODULE: on
|
||||
|
||||
jobs:
|
||||
@@ -23,29 +23,29 @@ jobs:
|
||||
# list of jobs to run:
|
||||
include:
|
||||
- job_name: Windows
|
||||
go: 1.24.x
|
||||
go: 1.26.x
|
||||
os: windows-latest
|
||||
|
||||
- job_name: macOS
|
||||
go: 1.24.x
|
||||
go: 1.26.x
|
||||
os: macOS-latest
|
||||
test_fuse: false
|
||||
|
||||
- job_name: Linux
|
||||
go: 1.24.x
|
||||
go: 1.26.x
|
||||
os: ubuntu-latest
|
||||
test_cloud_backends: true
|
||||
test_fuse: true
|
||||
check_changelog: true
|
||||
|
||||
- job_name: Linux (race)
|
||||
go: 1.24.x
|
||||
go: 1.26.x
|
||||
os: ubuntu-latest
|
||||
test_fuse: true
|
||||
test_opts: "-race"
|
||||
|
||||
- job_name: Linux
|
||||
go: 1.23.x
|
||||
go: 1.25.x
|
||||
os: ubuntu-latest
|
||||
test_fuse: true
|
||||
|
||||
@@ -57,10 +57,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
@@ -137,12 +137,14 @@ jobs:
|
||||
go run build.go
|
||||
|
||||
- name: Minimal test
|
||||
timeout-minutes: 3
|
||||
run: |
|
||||
./restic init
|
||||
./restic backup .
|
||||
env:
|
||||
RESTIC_REPOSITORY: ../testrepo
|
||||
RESTIC_PASSWORD: password
|
||||
RESTIC_DEBUG_STACKTRACE_SIGINT: "true"
|
||||
|
||||
- name: Run local Tests
|
||||
env:
|
||||
@@ -220,10 +222,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ env.latest_go }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.latest_go }}
|
||||
|
||||
@@ -242,18 +244,18 @@ jobs:
|
||||
checks: write
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ env.latest_go }}
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.latest_go }}
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||
version: v1.64.8
|
||||
version: v2.12
|
||||
args: --verbose --timeout 5m
|
||||
|
||||
# only run golangci-lint for pull requests, otherwise ALL hints get
|
||||
@@ -287,11 +289,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
@@ -307,14 +309,14 @@ jobs:
|
||||
type=sha
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
push: false
|
||||
context: .
|
||||
|
||||
+81
-56
@@ -1,70 +1,95 @@
|
||||
# This is the configuration for golangci-lint for the restic project.
|
||||
#
|
||||
# A sample config with all settings is here:
|
||||
# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
# only enable the linters listed below
|
||||
disable-all: true
|
||||
default: none
|
||||
enable:
|
||||
- asciicheck
|
||||
# ensure that http response bodies are closed
|
||||
- bodyclose
|
||||
# restrict imports from other restic packages for internal/backend (cache exempt)
|
||||
- depguard
|
||||
- copyloopvar
|
||||
# make sure all errors returned by functions are handled
|
||||
- errcheck
|
||||
|
||||
# show how code can be simplified
|
||||
- gosimple
|
||||
|
||||
# make sure code is formatted
|
||||
- gofmt
|
||||
|
||||
# examine code and report suspicious constructs, such as Printf calls whose
|
||||
# arguments do not align with the format string
|
||||
- govet
|
||||
|
||||
# make sure names and comments are used according to the conventions
|
||||
- revive
|
||||
|
||||
# consistent imports
|
||||
- importas
|
||||
# detect when assignments to existing variables are not used
|
||||
- ineffassign
|
||||
|
||||
- nolintlint
|
||||
# make sure names and comments are used according to the conventions
|
||||
- revive
|
||||
# run static analysis and find errors
|
||||
- staticcheck
|
||||
|
||||
# find unused variables, functions, structs, types, etc.
|
||||
- unused
|
||||
|
||||
# parse and typecheck code
|
||||
- typecheck
|
||||
|
||||
# ensure that http response bodies are closed
|
||||
- bodyclose
|
||||
|
||||
- importas
|
||||
|
||||
issues:
|
||||
# don't use the default exclude rules, this hides (among others) ignored
|
||||
# errors from Close() calls
|
||||
exclude-use-default: false
|
||||
|
||||
# list of things to not warn about
|
||||
exclude:
|
||||
# revive: do not warn about missing comments for exported stuff
|
||||
- exported (function|method|var|type|const) .* should have comment or be unexported
|
||||
# revive: ignore constants in all caps
|
||||
- don't use ALL_CAPS in Go names; use CamelCase
|
||||
# revive: lots of packages don't have such a comment
|
||||
- "package-comments: should have a package comment"
|
||||
# staticcheck: there's no easy way to replace these packages
|
||||
- "SA1019: \"golang.org/x/crypto/poly1305\" is deprecated"
|
||||
- "SA1019: \"golang.org/x/crypto/openpgp\" is deprecated"
|
||||
- "redefines-builtin-id:"
|
||||
|
||||
exclude-rules:
|
||||
# revive: ignore unused parameters in tests
|
||||
- path: (_test\.go|testing\.go|backend/.*/tests\.go)
|
||||
text: "unused-parameter:"
|
||||
|
||||
linters-settings:
|
||||
importas:
|
||||
alias:
|
||||
- pkg: github.com/restic/restic/internal/test
|
||||
alias: rtest
|
||||
settings:
|
||||
depguard:
|
||||
rules:
|
||||
# Prevent backend packages from importing the internal/restic package to keep the architectural layers intact.
|
||||
backend-imports:
|
||||
files:
|
||||
- "**/internal/backend/**"
|
||||
- "!**/internal/backend/cache/**"
|
||||
- "!**/internal/backend/test/**"
|
||||
- "!**/*_test.go"
|
||||
deny:
|
||||
- pkg: "github.com/restic/restic/internal/restic"
|
||||
desc: "internal/restic should not be imported to keep the architectural layers intact"
|
||||
- pkg: "github.com/restic/restic/internal/repository"
|
||||
desc: "internal/repository should not be imported to keep the architectural layers intact"
|
||||
importas:
|
||||
alias:
|
||||
- pkg: github.com/restic/restic/internal/test
|
||||
alias: rtest
|
||||
staticcheck:
|
||||
checks:
|
||||
# default
|
||||
- "all"
|
||||
- "-ST1000"
|
||||
- "-ST1003"
|
||||
- "-ST1016"
|
||||
- "-ST1020"
|
||||
- "-ST1021"
|
||||
- "-ST1022"
|
||||
# extra disables
|
||||
- "-QF1008" # don't warn about specifying name of embedded field on access
|
||||
exclusions:
|
||||
rules:
|
||||
# revive: ignore unused parameters in tests
|
||||
- path: (_test\.go|testing\.go|backend/.*/tests\.go)
|
||||
text: "unused-parameter:"
|
||||
# revive: do not warn about missing comments for exported stuff
|
||||
- path: (.+)\.go$
|
||||
text: exported (function|method|var|type|const) .* should have comment or be unexported
|
||||
# revive: ignore constants in all caps
|
||||
- path: (.+)\.go$
|
||||
text: don't use ALL_CAPS in Go names; use CamelCase
|
||||
# revive: lots of packages don't have such a comment
|
||||
- path: (.+)\.go$
|
||||
text: "package-comments: should have a package comment"
|
||||
# staticcheck: there's no easy way to replace these packages
|
||||
- path: (.+)\.go$
|
||||
text: 'SA1019: "golang.org/x/crypto/poly1305" is deprecated'
|
||||
- path: (.+)\.go$
|
||||
text: 'SA1019: "golang.org/x/crypto/openpgp" is deprecated'
|
||||
- path: (.+)\.go$
|
||||
text: "redefines-builtin-id:"
|
||||
# revive: collection of helpers to implement a backend, more descriptive names would be too repetitive
|
||||
- path: internal/backend/util/.*.go$
|
||||
text: "var-naming: avoid meaningless package names"
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
# make sure code is formatted
|
||||
- gofmt
|
||||
exclusions:
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
+626
-2
@@ -1,5 +1,7 @@
|
||||
# Table of Contents
|
||||
|
||||
* [Changelog for 0.19.1](#changelog-for-restic-0191-2026-07-05)
|
||||
* [Changelog for 0.19.0](#changelog-for-restic-0190-2026-06-09)
|
||||
* [Changelog for 0.18.1](#changelog-for-restic-0181-2025-09-21)
|
||||
* [Changelog for 0.18.0](#changelog-for-restic-0180-2025-03-27)
|
||||
* [Changelog for 0.17.3](#changelog-for-restic-0173-2024-11-08)
|
||||
@@ -40,6 +42,627 @@
|
||||
* [Changelog for 0.6.0](#changelog-for-restic-060-2017-05-29)
|
||||
|
||||
|
||||
# Changelog for restic 0.19.1 (2026-07-05)
|
||||
The following sections list the changes in restic 0.19.1 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
## Summary
|
||||
|
||||
* Fix #5234: Prevent mounting over the repository directory
|
||||
* Fix #5667: Skip inaccessible `backup` source paths
|
||||
* Fix #5722: Update `mount` latest symlink after snapshot reload
|
||||
* Fix #21866: Hide `stats` progress bar in JSON mode
|
||||
* Fix #21869: Restore old behavior of `snapshots --latest <n>` without `--group-by`
|
||||
* Fix #21876: Show timezone location in `snapshots` output
|
||||
* Fix #21879: Prevent crash in mountpoint validation if mountpoint is inaccessible
|
||||
* Fix #21895: Remove read-only files via the SFTP backend on Windows servers
|
||||
* Fix #21899: Make `backup` respect excludes for duplicate directory entries
|
||||
|
||||
## Details
|
||||
|
||||
* Bugfix #5234: Prevent mounting over the repository directory
|
||||
|
||||
Using a local repository directory as the `mount` target — or a path that
|
||||
contains it, or that it contains — caused the FUSE server to read its own
|
||||
backend files through the new mount, deadlocking the kernel and requiring a long
|
||||
reboot to recover.
|
||||
|
||||
Restic now resolves both paths and refuses any such overlap with a clear error
|
||||
before mounting.
|
||||
|
||||
https://github.com/restic/restic/issues/5234
|
||||
https://github.com/restic/restic/pull/5348
|
||||
|
||||
* Bugfix #5667: Skip inaccessible `backup` source paths
|
||||
|
||||
The `backup` command only skipped source paths that did not exist. A path that
|
||||
could not be accessed for another reason, such as a malformed path on Windows,
|
||||
was kept and produced an empty snapshot. Restic now skips any such path and
|
||||
aborts if none remain.
|
||||
|
||||
https://github.com/restic/restic/issues/5667
|
||||
https://github.com/restic/restic/pull/21852
|
||||
|
||||
* Bugfix #5722: Update `mount` latest symlink after snapshot reload
|
||||
|
||||
When `restic mount` was kept running while new snapshots were created, the new
|
||||
snapshots appeared in the mountpoint, but the `latest` symlink could still point
|
||||
to the previously latest snapshot. Restic now invalidates the cached snapshot
|
||||
directory entries after a snapshot reload so that `latest` points to the newest
|
||||
snapshot.
|
||||
|
||||
https://github.com/restic/restic/issues/5722
|
||||
https://github.com/restic/restic/pull/21873
|
||||
|
||||
* Bugfix #21866: Hide `stats` progress bar in JSON mode
|
||||
|
||||
Since restic 0.19.0, the `stats` command shows a progress bar. This progress bar
|
||||
was unintentionally displayed also when using the `--json` option, mixing
|
||||
regular text output with JSON. This is now fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/21866
|
||||
https://github.com/restic/restic/pull/21871
|
||||
|
||||
* Bugfix #21869: Restore old behavior of `snapshots --latest <n>` without `--group-by`
|
||||
|
||||
Restic 0.19.0 accidentally changed the behavior of `snapshots --latest <n>` to
|
||||
no longer group snapshots by host and paths by default.
|
||||
|
||||
The `snapshots --latest <n>` command now again uses the old behavior of grouping
|
||||
by host and paths when `--group-by` is not specified. However, when specifying
|
||||
`--group-by` the output is still grouped as requested, as in restic 0.19.0.
|
||||
|
||||
https://github.com/restic/restic/issues/21869
|
||||
https://github.com/restic/restic/pull/21875
|
||||
|
||||
* Bugfix #21876: Show timezone location in `snapshots` output
|
||||
|
||||
With restic 0.19.0, the `snapshots` command printed the current timezone when
|
||||
listing snapshots. However, that timezone label might change during the year,
|
||||
for example with daylight saving time. Restic now prints a more consistent and
|
||||
shorter version of the text.
|
||||
|
||||
https://github.com/restic/restic/pull/21876
|
||||
https://forum.restic.net/t/possible-bug-in-timezone-naming/10867
|
||||
|
||||
* Bugfix #21879: Prevent crash in mountpoint validation if mountpoint is inaccessible
|
||||
|
||||
Since restic 0.19.0, the `mount` command validates a mountpoint before loading
|
||||
the repository. If restic was unable to stat the mountpoint, this would result
|
||||
in a crash. This has now been fixed to correctly return an error instead.
|
||||
|
||||
https://github.com/restic/restic/pull/21879
|
||||
|
||||
* Bugfix #21895: Remove read-only files via the SFTP backend on Windows servers
|
||||
|
||||
Since restic 0.19.0, repository files on the SFTP backend are marked read-only
|
||||
after save. On Windows SFTP servers, removing them failed with a permission
|
||||
error. The SFTP backend now clears the read-only flag before removing the file.
|
||||
|
||||
https://github.com/restic/restic/issues/21895
|
||||
https://github.com/restic/restic/pull/21897
|
||||
|
||||
* Bugfix #21899: Make `backup` respect excludes for duplicate directory entries
|
||||
|
||||
Since restic 0.19.0, backing up a directory with duplicate directory entries
|
||||
always produced "Warning: at least one source file could not be read", even when
|
||||
those files were excluded. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/21899
|
||||
https://github.com/restic/restic/pull/21900
|
||||
|
||||
|
||||
# Changelog for restic 0.19.0 (2026-06-09)
|
||||
The following sections list the changes in restic 0.19.0 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
|
||||
## Summary
|
||||
|
||||
* Fix #2034: Support serving a `restic mount` of a Windows system via Samba
|
||||
* Fix #4447: Use mode 0700 for repository directories created over SFTP
|
||||
* Fix #4467: Exit with code 3 when some `backup` source paths do not exist
|
||||
* Fix #4759: Error out when environment variables hold invalid values
|
||||
* Fix #5233: Return exit code 3 when failing to remove snapshots
|
||||
* Fix #5258: Exit with code 130 on SIGINT
|
||||
* Fix #5280: Reject impossible `find` time bounds immediately
|
||||
* Fix #5280: Make `find --pack` list blobs for tree packs
|
||||
* Fix #5354: Allow `rclone` and `sftp` backends when running in background
|
||||
* Fix #5427: Correctly restore ACL inheritance state on Windows
|
||||
* Fix #5477: Password prompt was sometimes not shown for `backup -v`
|
||||
* Fix #5487: Mark repository files read-only when using the SFTP backend
|
||||
* Fix #5586: Correctly handle `snapshots --group-by` with `--latest`
|
||||
* Fix #5595: Avoid spurious `chmod` errors on certain file backends
|
||||
* Fix #5683: Prevent `backup --stdin-from-command` from hanging
|
||||
* Fix #5757: Respect `--user` and `--host` in `key passwd`
|
||||
* Fix #21820: Correct handling of duplicate index entries
|
||||
* Fix #21820: Correctly handle pack files missing from the index
|
||||
* Chg #5293: Prune small packfiles more aggressively
|
||||
* Chg #5767: Prevent excluding paths explicitly passed to `backup`
|
||||
* Chg #21791: Update dependencies and require Go 1.25 or newer
|
||||
* Enh #3326: Limit `check` to snapshots selected by filters
|
||||
* Enh #3572: Support restoring ownership by name on UNIX systems
|
||||
* Enh #3738: Optional GitHub token for `self-update` API requests
|
||||
* Enh #4278: Support include filters in the `rewrite` command
|
||||
* Enh #4728: Support zstd compression levels `fastest` and `better`
|
||||
* Enh #4868: Include repository ID in the filesystem name used by `mount`
|
||||
* Enh #5175: Add status counters to `copy` in verbose text output
|
||||
* Enh #5352: Support excluding cloud-backed files on macOS
|
||||
* Enh #5383: Reduce progress bar refresh rates to decrease energy usage
|
||||
* Enh #5424: Enable Windows filesystem privileges before file access
|
||||
* Enh #5440: Make `--host` override environment variable `RESTIC_HOST`
|
||||
* Enh #5448: Support configuring `nice` and `ionice` in the Docker image
|
||||
* Enh #5453: Copy multiple snapshots in batches
|
||||
* Enh #5523: Add Open Container Initiative labels to release Docker image
|
||||
* Enh #5531: Reduce Azure storage costs by optimizing uploads
|
||||
* Enh #5562: Rewrite only changed status lines each frame
|
||||
* Enh #5588: Show timezone context in `snapshots` output
|
||||
* Enh #5610: Reduce `check`, `copy`, `diff` and `stats` memory usage
|
||||
* Enh #5689: Show more detailed progress for `stats`
|
||||
* Enh #5713: Significantly speed up index loading
|
||||
* Enh #5718: Stricter and earlier validation of the `mount` point
|
||||
|
||||
## Details
|
||||
|
||||
* Bugfix #2034: Support serving a `restic mount` of a Windows system via Samba
|
||||
|
||||
A repository mounted using `restic mount` on a POSIX system could not use Samba
|
||||
to serve files from backups of Windows systems, while backups of non-Windows
|
||||
systems could be served successfully. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/2034
|
||||
https://github.com/restic/restic/issues/4382
|
||||
https://github.com/restic/restic/pull/21784
|
||||
|
||||
* Bugfix #4447: Use mode 0700 for repository directories created over SFTP
|
||||
|
||||
When creating a repository over SFTP, restic created the repository directories
|
||||
with the SFTP server's default permissions, often 0755, rather than the 0700
|
||||
permissions it uses for local repositories.
|
||||
|
||||
Restic now creates these directories with 0700 permissions.
|
||||
|
||||
https://github.com/restic/restic/issues/4447
|
||||
https://github.com/restic/restic/pull/21817
|
||||
|
||||
* Bugfix #4467: Exit with code 3 when some `backup` source paths do not exist
|
||||
|
||||
Restic used to exit with code 0 when a top-level backup source path was missing,
|
||||
and exited with code 3 only when a child path under an existing source did not
|
||||
exist. Scripts that relied on the exit code could therefore treat an incomplete
|
||||
backup as success.
|
||||
|
||||
Restic now exits with code 3 when any backup source path does not exist.
|
||||
|
||||
https://github.com/restic/restic/issues/4467
|
||||
https://github.com/restic/restic/pull/5347
|
||||
|
||||
* Bugfix #4759: Error out when environment variables hold invalid values
|
||||
|
||||
If the environment variables `RESTIC_COMPRESSION`, `RESTIC_PACK_SIZE`, or
|
||||
`RESTIC_READ_CONCURRENCY` could not be parsed, restic used to ignore them.
|
||||
Restic now fails with an error unless the same setting is overridden on the
|
||||
command line.
|
||||
|
||||
https://github.com/restic/restic/issues/4759
|
||||
https://github.com/restic/restic/pull/5592
|
||||
https://github.com/restic/restic/pull/5700
|
||||
|
||||
* Bugfix #5233: Return exit code 3 when failing to remove snapshots
|
||||
|
||||
Previously, the `forget` command returned exit code 0 when it failed to remove
|
||||
one or more snapshots. This was misleading to scripts.
|
||||
|
||||
The `forget` command now instead returns exit code 3 when failing to remove one
|
||||
or more snapshots.
|
||||
|
||||
https://github.com/restic/restic/issues/5233
|
||||
https://github.com/restic/restic/pull/5322
|
||||
|
||||
* Bugfix #5258: Exit with code 130 on SIGINT
|
||||
|
||||
Restic used to return exit code 1 on SIGINT. It now returns 130, the usual
|
||||
convention for a process stopped by Ctrl-C.
|
||||
|
||||
https://github.com/restic/restic/issues/5258
|
||||
https://github.com/restic/restic/pull/5363
|
||||
|
||||
* Bugfix #5280: Reject impossible `find` time bounds immediately
|
||||
|
||||
The `find` command now fails with an error when both `--oldest` and `--newest`
|
||||
are set and `--oldest` is later than `--newest`.
|
||||
|
||||
https://github.com/restic/restic/issues/5280
|
||||
https://github.com/restic/restic/pull/5310
|
||||
|
||||
* Bugfix #5280: Make `find --pack` list blobs for tree packs
|
||||
|
||||
The `find --pack <tree-pack>` command now also reports blobs for packs that only
|
||||
contain tree blobs.
|
||||
|
||||
https://github.com/restic/restic/issues/5280
|
||||
https://github.com/restic/restic/pull/5664
|
||||
|
||||
* Bugfix #5354: Allow `rclone` and `sftp` backends when running in background
|
||||
|
||||
Previously, starting restic in the background could result in unexpected
|
||||
behavior when using the `rclone` or `sftp` backends. For example, `restic -r
|
||||
rclone:./example --insecure-no-password init &` could cause the calling `bash`
|
||||
shell to exit unexpectedly.
|
||||
|
||||
This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/5354
|
||||
https://github.com/restic/restic/pull/5358
|
||||
https://github.com/restic/restic/pull/5493
|
||||
https://github.com/restic/restic/pull/5494
|
||||
|
||||
* Bugfix #5427: Correctly restore ACL inheritance state on Windows
|
||||
|
||||
Since security descriptor backups were added in restic 0.17.0, Access Control
|
||||
Entry inheritance was not restored correctly on Windows; restored permissions
|
||||
were always marked as explicit (not inherited) even when they were inherited
|
||||
from a parent folder.
|
||||
|
||||
The inheritance flags are now correctly applied when restoring the security
|
||||
descriptor, preserving the original permission structure.
|
||||
|
||||
https://github.com/restic/restic/issues/5427
|
||||
https://github.com/restic/restic/pull/5465
|
||||
|
||||
* Bugfix #5477: Password prompt was sometimes not shown for `backup -v`
|
||||
|
||||
The repository password prompt could be hidden when running the `backup -v`
|
||||
command. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/5477
|
||||
https://github.com/restic/restic/pull/5554
|
||||
|
||||
* Bugfix #5487: Mark repository files read-only when using the SFTP backend
|
||||
|
||||
Files created through the SFTP backend previously stayed writable. New files now
|
||||
get read-only permissions where the server supports `chmod`.
|
||||
|
||||
https://github.com/restic/restic/issues/5487
|
||||
https://github.com/restic/restic/pull/5497
|
||||
|
||||
* Bugfix #5586: Correctly handle `snapshots --group-by` with `--latest`
|
||||
|
||||
For the `snapshots` command, `--latest` did not interact correctly with a
|
||||
non-default `--group-by` value. This combination now behaves as intended.
|
||||
|
||||
https://github.com/restic/restic/issues/5586
|
||||
https://github.com/restic/restic/pull/5601
|
||||
|
||||
* Bugfix #5595: Avoid spurious `chmod` errors on certain file backends
|
||||
|
||||
On filesystems that do not support `chmod` (for example CIFS or FUSE-mounted
|
||||
WebDAV), restic since version 0.18.0 failed to remove stale locks, throwing the
|
||||
error `chmod ...: operation not supported`. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/5595
|
||||
https://github.com/restic/restic/pull/5596
|
||||
|
||||
* Bugfix #5683: Prevent `backup --stdin-from-command` from hanging
|
||||
|
||||
When using `--stdin-from-command`, the `backup` command could hang until
|
||||
manually cancelled if the backup stopped before all subprocess output was
|
||||
consumed, for example after a failed upload to the repository. This has now been
|
||||
fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/5683
|
||||
https://github.com/restic/restic/pull/21829
|
||||
|
||||
* Bugfix #5757: Respect `--user` and `--host` in `key passwd`
|
||||
|
||||
The `key passwd` command previously ignored the `--user` and `--host` options
|
||||
and always stored the new key with the current user and host name. These options
|
||||
are now honored.
|
||||
|
||||
https://github.com/restic/restic/issues/5757
|
||||
https://github.com/restic/restic/pull/21781
|
||||
|
||||
* Bugfix #21820: Correct handling of duplicate index entries
|
||||
|
||||
Before restic 0.10.0, a bug could, in very rare cases, split information about a
|
||||
pack file across multiple index files.
|
||||
|
||||
Since restic 0.17.0, any operation that rewrites the index (like `prune` or
|
||||
`repair packs`) could lose part of that information, resulting in errors in
|
||||
later `check` or `prune` runs. This can be fixed by running `repair packs`, and
|
||||
only repositories using repository format version 1 might be affected.
|
||||
|
||||
Split pack index entries are no longer lost during index rewrites. The `check`
|
||||
command now reports these cases as errors that can be fixed using the `repair
|
||||
packs` command. On older restic versions, running `repair index` twice also
|
||||
fixes the problem.
|
||||
|
||||
https://github.com/restic/restic/issues/21820
|
||||
https://github.com/restic/restic/pull/21828
|
||||
|
||||
* Bugfix #21820: Correctly handle pack files missing from the index
|
||||
|
||||
The `repair packs` command was unable to salvage blobs from a pack file if the
|
||||
pack file was not contained in the index or the index entry was incomplete.
|
||||
|
||||
The command now uses information from both the index and the pack file header.
|
||||
|
||||
https://github.com/restic/restic/issues/21820
|
||||
https://github.com/restic/restic/pull/21827
|
||||
|
||||
* Change #5293: Prune small packfiles more aggressively
|
||||
|
||||
The `prune` command now repacks more small packfiles by default. The option
|
||||
`--repack-small` is no longer needed and has been marked as deprecated. The
|
||||
`--repack-smaller-than` option can still be used to further control repacking of
|
||||
small pack files.
|
||||
|
||||
https://github.com/restic/restic/issues/5293
|
||||
https://github.com/restic/restic/pull/21803
|
||||
|
||||
* Change #5767: Prevent excluding paths explicitly passed to `backup`
|
||||
|
||||
When e.g. `restic backup --exclude-if-present .git /home/user/data` was run and
|
||||
`/home/user/.git` existed, restic excluded the `data` directory from the
|
||||
snapshot. The same applied to `--exclude-caches` or `--one-file-system`.
|
||||
|
||||
Similarly, `restic backup --exclude-larger-than 1M large-file.bin` produced an
|
||||
empty snapshot when the file was larger than one megabyte.
|
||||
|
||||
The `backup` command now tracks which files and directories were specified on
|
||||
the command line and does not apply excludes to those paths. Content inside a
|
||||
backed-up directory is still filtered by excludes as before.
|
||||
|
||||
https://github.com/restic/restic/issues/5767
|
||||
https://github.com/restic/restic/pull/21797
|
||||
|
||||
* Change #21791: Update dependencies and require Go 1.25 or newer
|
||||
|
||||
Dependencies have been updated. Building restic now requires Go 1.25 or newer.
|
||||
The Windows build with Go 1.26 was also fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/21791
|
||||
https://github.com/restic/restic/pull/5619
|
||||
https://github.com/restic/restic/pull/21796
|
||||
|
||||
* Enhancement #3326: Limit `check` to snapshots selected by filters
|
||||
|
||||
The `check` command can now restrict pack verification to snapshots chosen with
|
||||
the usual snapshot filters (`--tag`, `--host`, `--path`, or explicit snapshot
|
||||
IDs on the command line).
|
||||
|
||||
https://github.com/restic/restic/issues/3326
|
||||
https://github.com/restic/restic/pull/5469
|
||||
https://github.com/restic/restic/pull/5644
|
||||
|
||||
* Enhancement #3572: Support restoring ownership by name on UNIX systems
|
||||
|
||||
The `restore` command used to restore file ownership on UNIX systems by UID and
|
||||
GID. It now supports restoring ownership by user and group name with
|
||||
`--ownership-by-name`, so that snapshots can be restored on systems where
|
||||
numeric IDs do not match those on the backup host.
|
||||
|
||||
Note: POSIX ACLs are still restored by numeric value; this change does not add
|
||||
ACL-by-name support.
|
||||
|
||||
https://github.com/restic/restic/issues/3572
|
||||
https://github.com/restic/restic/pull/5449
|
||||
|
||||
* Enhancement #3738: Optional GitHub token for `self-update` API requests
|
||||
|
||||
The `self-update` command used only unauthenticated GitHub API requests when
|
||||
checking for releases. Shared IP addresses could hit the GitHub rate limit,
|
||||
resulting in a 403 Forbidden error and preventing updates.
|
||||
|
||||
Unauthenticated requests remain the default, but authenticated requests are now
|
||||
possible. Set the environment variable `GITHUB_ACCESS_TOKEN` to a GitHub
|
||||
[personal access token](https://github.com/settings/tokens) to avoid rate-limit
|
||||
failures.
|
||||
|
||||
https://github.com/restic/restic/issues/3738
|
||||
https://github.com/restic/restic/pull/5568
|
||||
|
||||
* Enhancement #4278: Support include filters in the `rewrite` command
|
||||
|
||||
The `rewrite` command now accepts the same include filter options as the
|
||||
`restore` command (`--include`, `--include-file`, `--iinclude`,
|
||||
`--iinclude-file`, and short `-i`). Include and exclude filter options are
|
||||
mutually exclusive.
|
||||
|
||||
https://github.com/restic/restic/issues/4278
|
||||
https://github.com/restic/restic/pull/5191
|
||||
|
||||
* Enhancement #4728: Support 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`, or pass the
|
||||
same values with the `--compression` option.
|
||||
|
||||
https://github.com/restic/restic/issues/4728
|
||||
https://github.com/restic/restic/pull/5321
|
||||
|
||||
* Enhancement #4868: Include repository ID in the filesystem name used by `mount`
|
||||
|
||||
The filesystem exposed by the `mount` command now includes the repository ID in
|
||||
its name. The ID is printed when opening a repository or can be read with
|
||||
`restic cat config`.
|
||||
|
||||
```
|
||||
$ 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
|
||||
|
||||
* Enhancement #5175: Add status counters to `copy` in verbose text output
|
||||
|
||||
The `copy` command now prints additional counters in text mode when `--verbose`
|
||||
is set: blobs to copy, their on-disk size, and the number of pack files read
|
||||
from the source repository.
|
||||
|
||||
https://github.com/restic/restic/issues/5175
|
||||
https://github.com/restic/restic/pull/5319
|
||||
|
||||
* Enhancement #5352: Support excluding cloud-backed files on macOS
|
||||
|
||||
Previously, restic treated cloud-backed files (such as files stored on iCloud)
|
||||
like normal local files, forcing a full download of placeholders and other
|
||||
"meant to be cloud only" content during backups.
|
||||
|
||||
The `backup` command now supports `--exclude-cloud-files` (previously only
|
||||
available on Windows) to skip those files on supported macOS versions. From
|
||||
Sonoma (macOS 14.0) onward the option can prevent unwanted downloads. Older
|
||||
macOS versions will still download the files during a backup run.
|
||||
|
||||
https://github.com/restic/restic/issues/5352
|
||||
https://github.com/restic/restic/pull/5370
|
||||
|
||||
* Enhancement #5383: Reduce progress bar refresh rates to decrease energy usage
|
||||
|
||||
Progress bars were previously updated at 60 frames per second, which could cause
|
||||
high CPU or GPU usage in some terminal emulators.
|
||||
|
||||
The refresh rate is now 10 FPS to conserve energy. For some terminal emulators,
|
||||
the lower rate is also necessary to allow selecting text in the terminal.
|
||||
|
||||
https://github.com/restic/restic/issues/5383
|
||||
https://github.com/restic/restic/pull/5551
|
||||
https://github.com/restic/restic/pull/5626
|
||||
|
||||
* Enhancement #5424: Enable Windows filesystem privileges before file access
|
||||
|
||||
Restic used to enable some Windows filesystem privileges only while reading or
|
||||
writing security descriptors. Extended attributes could therefore be read before
|
||||
enabling the backup privilege, possibly resulting in missed data or errors.
|
||||
|
||||
Restic now enables the relevant filesystem privileges before any file access.
|
||||
|
||||
https://github.com/restic/restic/pull/5424
|
||||
|
||||
* Enhancement #5440: Make `--host` override environment variable `RESTIC_HOST`
|
||||
|
||||
Previously, when the environment variable `RESTIC_HOST` was set, snapshot
|
||||
listings and other operations were always filtered to that host.
|
||||
|
||||
Passing `--host` as an empty string (`--host=""` or `--host=`) now overrides
|
||||
`RESTIC_HOST` and shows snapshots from all hosts.
|
||||
|
||||
The same override applies to other commands that support snapshot filters,
|
||||
including `snapshots`, `forget`, `find`, `stats`, `copy`, `tag`, `repair
|
||||
snapshots`, `rewrite`, `mount`, `restore`, `dump`, and `ls`.
|
||||
|
||||
https://github.com/restic/restic/issues/5440
|
||||
https://github.com/restic/restic/pull/5541
|
||||
|
||||
* Enhancement #5448: Support configuring `nice` and `ionice` in the Docker image
|
||||
|
||||
The container entrypoint now reads optional scheduling hints from the
|
||||
environment:
|
||||
|
||||
- The environment variable `NICE` sets the process nice value (see `man nice`).
|
||||
|
||||
- The environment variable `IONICE_CLASS` selects the I/O scheduling class (see
|
||||
`man ionice`). Real-time classes need the `SYS_NICE` capability added to the
|
||||
container.
|
||||
|
||||
- The environment variable `IONICE_PRIORITY` sets the priority within
|
||||
`IONICE_CLASS` and has no effect unless `IONICE_CLASS` is set; it defaults to
|
||||
`4` (neutral priority).
|
||||
|
||||
For further details, please see:
|
||||
https://restic.readthedocs.io/en/stable/020_installation.html#docker-container
|
||||
|
||||
https://github.com/restic/restic/pull/5448
|
||||
|
||||
* Enhancement #5453: Copy multiple snapshots in batches
|
||||
|
||||
The `copy` command used to copy snapshots one at a time, even when doing so
|
||||
produced pack files smaller than the target pack size. This led to many small
|
||||
files when copying small incremental snapshots.
|
||||
|
||||
The `copy` command now copies multiple snapshots together so that small pack
|
||||
files are avoided where possible.
|
||||
|
||||
https://github.com/restic/restic/issues/5453
|
||||
https://github.com/restic/restic/pull/5472
|
||||
|
||||
* Enhancement #5523: Add Open Container Initiative labels to release Docker image
|
||||
|
||||
The release Docker image now includes OCI-style image annotation labels, which
|
||||
helps external tooling identify the image.
|
||||
|
||||
https://github.com/restic/restic/pull/5523
|
||||
|
||||
* Enhancement #5531: Reduce Azure storage costs by optimizing uploads
|
||||
|
||||
Restic previously used Azure PutBlock and PutBlockList for every upload, which
|
||||
cost two storage transactions per file and roughly doubled transaction charges
|
||||
for repositories with many pack files.
|
||||
|
||||
Files up to 256 MiB now use PutBlob, requiring only a single transaction per
|
||||
file and cutting typical transaction costs by about half. Larger blobs still use
|
||||
block uploads as required by Azure.
|
||||
|
||||
https://github.com/restic/restic/issues/5531
|
||||
https://github.com/restic/restic/pull/5544
|
||||
|
||||
* Enhancement #5562: Rewrite only changed status lines each frame
|
||||
|
||||
The status bar rewrote every line on each frame whenever any content changed,
|
||||
which made selecting text impossible in some terminal emulators even when most
|
||||
lines were unchanged.
|
||||
|
||||
Now only lines that actually change are rewritten on each update.
|
||||
|
||||
https://github.com/restic/restic/issues/5562
|
||||
https://github.com/restic/restic/pull/5648
|
||||
|
||||
* Enhancement #5588: Show timezone context in `snapshots` output
|
||||
|
||||
The `snapshots` command now prints which timezone is used for displayed
|
||||
timestamps. Snapshots may have been created in different timezones but are shown
|
||||
in the local timezone, so a footer line (for example, timestamps shown in CET)
|
||||
clarifies the display context when comparing snapshots from several sources.
|
||||
|
||||
https://github.com/restic/restic/pull/5588
|
||||
|
||||
* Enhancement #5610: Reduce `check`, `copy`, `diff` and `stats` memory usage
|
||||
|
||||
The `check`, `copy`, `diff` and `stats` commands now use less memory when
|
||||
handling large snapshots.
|
||||
|
||||
https://github.com/restic/restic/pull/5610
|
||||
|
||||
* Enhancement #5689: Show more detailed progress for `stats`
|
||||
|
||||
The `stats` command used to show progress only while loading the index. During
|
||||
the scan it printed only `scanning...` with no further updates. It now reports
|
||||
how many snapshots, files, and blobs have been processed so far.
|
||||
|
||||
https://github.com/restic/restic/issues/5689
|
||||
https://github.com/restic/restic/pull/5705
|
||||
|
||||
* Enhancement #5713: Significantly speed up index loading
|
||||
|
||||
Loading the index for a large repository is now significantly faster. Also, the
|
||||
`mount` command now loads the index once at startup and then only loads new
|
||||
index files as they appear. It also loads snapshots before printing that the
|
||||
repository is being served.
|
||||
|
||||
https://github.com/restic/restic/pull/5713
|
||||
https://github.com/restic/restic/pull/5720
|
||||
|
||||
* Enhancement #5718: Stricter and earlier validation of the `mount` point
|
||||
|
||||
The `mount` command previously accepted invalid mount points, resulting in an
|
||||
error after loading the repository. The specified mount point must now refer to
|
||||
a directory that the current user can access and write to, and this check is
|
||||
performed before opening the repository.
|
||||
|
||||
https://github.com/restic/restic/pull/5718
|
||||
|
||||
|
||||
# Changelog for restic 0.18.1 (2025-09-21)
|
||||
The following sections list the changes in restic 0.18.1 relevant to
|
||||
restic users. The changes are ordered by importance.
|
||||
@@ -707,8 +1330,9 @@ restic users. The changes are ordered by importance.
|
||||
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:
|
||||
before this bugfix, use the <snapshot>:<subfolder> 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
|
||||
|
||||
@@ -202,6 +202,9 @@ we'll be glad to assist. Having a PR with failing integration tests is nothing
|
||||
to be ashamed of. In contrast, that happens regularly for all of us. That's
|
||||
what the tests are there for.
|
||||
|
||||
More details of how to structure tests can be found here at
|
||||
[writing tests](https://restic.readthedocs.io/en/stable/090_participating.html#writing-tests).
|
||||
|
||||
Git Commits
|
||||
-----------
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
//go:build ignore_build_go
|
||||
// +build ignore_build_go
|
||||
|
||||
package main
|
||||
|
||||
@@ -60,7 +59,7 @@ var config = Config{
|
||||
// see https://github.com/googleapis/google-cloud-go/issues/11448
|
||||
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
|
||||
MinVersion: GoVersion{Major: 1, Minor: 25, Patch: 0}, // minimum Go version supported
|
||||
}
|
||||
|
||||
// Config configures the build.
|
||||
|
||||
@@ -6,7 +6,7 @@ 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
|
||||
created before this bugfix, use the <snapshot>:<subfolder> syntax. For
|
||||
example, to restore a snapshot with ID `12345678` that backed up `C:`,
|
||||
use the following command:
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
Bugfix: Correct handling of duplicate index entries
|
||||
|
||||
Before restic 0.10.0, a bug could, in very rare cases, split information
|
||||
about a pack file across multiple index files.
|
||||
|
||||
Since restic 0.17.0, any operation that rewrites the index (like `prune`
|
||||
or `repair packs`) could lose part of that information, resulting in errors
|
||||
in later `check` or `prune` runs. This can be fixed by running `repair packs`,
|
||||
and only repositories using repository format version 1 might be affected.
|
||||
|
||||
Split pack index entries are no longer lost during index rewrites. The
|
||||
`check` command now reports these cases as errors that can be fixed using
|
||||
the `repair packs` command. On older restic versions, running `repair index`
|
||||
twice also fixes the problem.
|
||||
|
||||
https://github.com/restic/restic/issues/21820
|
||||
https://github.com/restic/restic/pull/21828
|
||||
@@ -0,0 +1,9 @@
|
||||
Enhancement: Limit `check` to snapshots selected by filters
|
||||
|
||||
The `check` command can now restrict pack verification to snapshots chosen with
|
||||
the usual snapshot filters (`--tag`, `--host`, `--path`, or explicit snapshot
|
||||
IDs on the command line).
|
||||
|
||||
https://github.com/restic/restic/issues/3326
|
||||
https://github.com/restic/restic/pull/5469
|
||||
https://github.com/restic/restic/pull/5644
|
||||
@@ -0,0 +1,12 @@
|
||||
Enhancement: Support restoring ownership by name on UNIX systems
|
||||
|
||||
The `restore` command used to restore file ownership on UNIX systems by UID
|
||||
and GID. It now supports restoring ownership by user and group name with
|
||||
`--ownership-by-name`, so that snapshots can be restored on systems where
|
||||
numeric IDs do not match those on the backup host.
|
||||
|
||||
Note: POSIX ACLs are still restored by numeric value; this change does not
|
||||
add ACL-by-name support.
|
||||
|
||||
https://github.com/restic/restic/issues/3572
|
||||
https://github.com/restic/restic/pull/5449
|
||||
@@ -0,0 +1,13 @@
|
||||
Enhancement: Optional GitHub token for `self-update` API requests
|
||||
|
||||
The `self-update` command used only unauthenticated GitHub API requests when
|
||||
checking for releases. Shared IP addresses could hit the GitHub rate limit,
|
||||
resulting in a 403 Forbidden error and preventing updates.
|
||||
|
||||
Unauthenticated requests remain the default, but authenticated requests are now
|
||||
possible. Set the environment variable `GITHUB_ACCESS_TOKEN` to a GitHub
|
||||
[personal access token](https://github.com/settings/tokens) to avoid rate-limit
|
||||
failures.
|
||||
|
||||
https://github.com/restic/restic/issues/3738
|
||||
https://github.com/restic/restic/pull/5568
|
||||
@@ -0,0 +1,8 @@
|
||||
Enhancement: Support include filters in the `rewrite` command
|
||||
|
||||
The `rewrite` command now accepts the same include filter options as the `restore`
|
||||
command (`--include`, `--include-file`, `--iinclude`, `--iinclude-file`, and
|
||||
short `-i`). Include and exclude filter options are mutually exclusive.
|
||||
|
||||
https://github.com/restic/restic/issues/4278
|
||||
https://github.com/restic/restic/pull/5191
|
||||
@@ -0,0 +1,10 @@
|
||||
Bugfix: Use mode 0700 for repository directories created over SFTP
|
||||
|
||||
When creating a repository over SFTP, restic created the repository directories
|
||||
with the SFTP server's default permissions, often 0755, rather than the 0700
|
||||
permissions it uses for local repositories.
|
||||
|
||||
Restic now creates these directories with 0700 permissions.
|
||||
|
||||
https://github.com/restic/restic/issues/4447
|
||||
https://github.com/restic/restic/pull/21817
|
||||
@@ -0,0 +1,11 @@
|
||||
Bugfix: Exit with code 3 when some `backup` source paths do not exist
|
||||
|
||||
Restic used to exit with code 0 when a top-level backup source path was
|
||||
missing, and exited with code 3 only when a child path under an existing
|
||||
source did not exist. Scripts that relied on the exit code could therefore
|
||||
treat an incomplete backup as success.
|
||||
|
||||
Restic now exits with code 3 when any backup source path does not exist.
|
||||
|
||||
https://github.com/restic/restic/issues/4467
|
||||
https://github.com/restic/restic/pull/5347
|
||||
@@ -0,0 +1,8 @@
|
||||
Enhancement: Support 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`, or pass the
|
||||
same values with the `--compression` option.
|
||||
|
||||
https://github.com/restic/restic/issues/4728
|
||||
https://github.com/restic/restic/pull/5321
|
||||
@@ -0,0 +1,10 @@
|
||||
Bugfix: Error out when environment variables hold invalid values
|
||||
|
||||
If the environment variables `RESTIC_COMPRESSION`, `RESTIC_PACK_SIZE`, or
|
||||
`RESTIC_READ_CONCURRENCY` could not be parsed, restic used to ignore them.
|
||||
Restic now fails with an error unless the same setting is overridden on the
|
||||
command line.
|
||||
|
||||
https://github.com/restic/restic/issues/4759
|
||||
https://github.com/restic/restic/pull/5592
|
||||
https://github.com/restic/restic/pull/5700
|
||||
@@ -0,0 +1,14 @@
|
||||
Enhancement: Include repository ID in the filesystem name used by `mount`
|
||||
|
||||
The filesystem exposed by the `mount` command now includes the repository ID
|
||||
in its name. The ID is printed when opening a repository or can be read with
|
||||
`restic cat config`.
|
||||
|
||||
```
|
||||
$ 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
|
||||
@@ -0,0 +1,10 @@
|
||||
Bugfix: Return exit code 3 when failing to remove snapshots
|
||||
|
||||
Previously, the `forget` command returned exit code 0 when it failed
|
||||
to remove one or more snapshots. This was misleading to scripts.
|
||||
|
||||
The `forget` command now instead returns exit code 3 when failing to
|
||||
remove one or more snapshots.
|
||||
|
||||
https://github.com/restic/restic/issues/5233
|
||||
https://github.com/restic/restic/pull/5322
|
||||
@@ -0,0 +1,7 @@
|
||||
Bugfix: Exit with code 130 on SIGINT
|
||||
|
||||
Restic used to return exit code 1 on SIGINT. It now returns 130, the usual
|
||||
convention for a process stopped by Ctrl-C.
|
||||
|
||||
https://github.com/restic/restic/issues/5258
|
||||
https://github.com/restic/restic/pull/5363
|
||||
@@ -0,0 +1,7 @@
|
||||
Bugfix: Reject impossible `find` time bounds immediately
|
||||
|
||||
The `find` command now fails with an error when both `--oldest`
|
||||
and `--newest` are set and `--oldest` is later than `--newest`.
|
||||
|
||||
https://github.com/restic/restic/issues/5280
|
||||
https://github.com/restic/restic/pull/5310
|
||||
@@ -0,0 +1,9 @@
|
||||
Change: Prune small packfiles more aggressively
|
||||
|
||||
The `prune` command now repacks more small packfiles by default. The option
|
||||
`--repack-small` is no longer needed and has been marked as deprecated. The
|
||||
`--repack-smaller-than` option can still be used to further control repacking
|
||||
of small pack files.
|
||||
|
||||
https://github.com/restic/restic/issues/5293
|
||||
https://github.com/restic/restic/pull/21803
|
||||
@@ -0,0 +1,13 @@
|
||||
Enhancement: Support excluding cloud-backed files on macOS
|
||||
|
||||
Previously, restic treated cloud-backed files (such as files stored on iCloud)
|
||||
like normal local files, forcing a full download of placeholders and other
|
||||
"meant to be cloud only" content during backups.
|
||||
|
||||
The `backup` command now supports `--exclude-cloud-files` (previously only
|
||||
available on Windows) to skip those files on supported macOS versions. From
|
||||
Sonoma (macOS 14.0) onward the option can prevent unwanted downloads. Older
|
||||
macOS versions will still download the files during a backup run.
|
||||
|
||||
https://github.com/restic/restic/issues/5352
|
||||
https://github.com/restic/restic/pull/5370
|
||||
@@ -0,0 +1,13 @@
|
||||
Bugfix: Allow `rclone` and `sftp` backends when running in background
|
||||
|
||||
Previously, starting restic in the background could result in unexpected
|
||||
behavior when using the `rclone` or `sftp` backends. For example,
|
||||
`restic -r rclone:./example --insecure-no-password init &` could cause the
|
||||
calling `bash` shell to exit unexpectedly.
|
||||
|
||||
This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/5354
|
||||
https://github.com/restic/restic/pull/5358
|
||||
https://github.com/restic/restic/pull/5493
|
||||
https://github.com/restic/restic/pull/5494
|
||||
@@ -0,0 +1,12 @@
|
||||
Enhancement: Reduce progress bar refresh rates to decrease energy usage
|
||||
|
||||
Progress bars were previously updated at 60 frames per second, which
|
||||
could cause high CPU or GPU usage in some terminal emulators.
|
||||
|
||||
The refresh rate is now 10 FPS to conserve energy. For some terminal
|
||||
emulators, the lower rate is also necessary to allow selecting text
|
||||
in the terminal.
|
||||
|
||||
https://github.com/restic/restic/issues/5383
|
||||
https://github.com/restic/restic/pull/5551
|
||||
https://github.com/restic/restic/pull/5626
|
||||
@@ -0,0 +1,14 @@
|
||||
Enhancement: Make `--host` override environment variable `RESTIC_HOST`
|
||||
|
||||
Previously, when the environment variable `RESTIC_HOST` was set, snapshot
|
||||
listings and other operations were always filtered to that host.
|
||||
|
||||
Passing `--host` as an empty string (`--host=""` or `--host=`) now overrides
|
||||
`RESTIC_HOST` and shows snapshots from all hosts.
|
||||
|
||||
The same override applies to other commands that support snapshot filters,
|
||||
including `snapshots`, `forget`, `find`, `stats`, `copy`, `tag`,
|
||||
`repair snapshots`, `rewrite`, `mount`, `restore`, `dump`, and `ls`.
|
||||
|
||||
https://github.com/restic/restic/issues/5440
|
||||
https://github.com/restic/restic/pull/5541
|
||||
@@ -0,0 +1,11 @@
|
||||
Enhancement: Copy multiple snapshots in batches
|
||||
|
||||
The `copy` command used to copy snapshots one at a time, even when doing so
|
||||
produced pack files smaller than the target pack size. This led to many small
|
||||
files when copying small incremental snapshots.
|
||||
|
||||
The `copy` command now copies multiple snapshots together so that small pack
|
||||
files are avoided where possible.
|
||||
|
||||
https://github.com/restic/restic/issues/5453
|
||||
https://github.com/restic/restic/pull/5472
|
||||
@@ -0,0 +1,7 @@
|
||||
Bugfix: Password prompt was sometimes not shown for `backup -v`
|
||||
|
||||
The repository password prompt could be hidden when running the
|
||||
`backup -v` command. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/5477
|
||||
https://github.com/restic/restic/pull/5554
|
||||
@@ -0,0 +1,7 @@
|
||||
Bugfix: Mark repository files read-only when using the SFTP backend
|
||||
|
||||
Files created through the SFTP backend previously stayed writable. New files now
|
||||
get read-only permissions where the server supports `chmod`.
|
||||
|
||||
https://github.com/restic/restic/issues/5487
|
||||
https://github.com/restic/restic/pull/5497
|
||||
@@ -0,0 +1,12 @@
|
||||
Enhancement: Reduce Azure storage costs by optimizing uploads
|
||||
|
||||
Restic previously used Azure PutBlock and PutBlockList for every upload, which
|
||||
cost two storage transactions per file and roughly doubled transaction charges
|
||||
for repositories with many pack files.
|
||||
|
||||
Files up to 256 MiB now use PutBlob, requiring only a single transaction per
|
||||
file and cutting typical transaction costs by about half. Larger blobs still
|
||||
use block uploads as required by Azure.
|
||||
|
||||
https://github.com/restic/restic/issues/5531
|
||||
https://github.com/restic/restic/pull/5544
|
||||
@@ -0,0 +1,10 @@
|
||||
Enhancement: Rewrite only changed status lines each frame
|
||||
|
||||
The status bar rewrote every line on each frame whenever any content changed,
|
||||
which made selecting text impossible in some terminal emulators even when most
|
||||
lines were unchanged.
|
||||
|
||||
Now only lines that actually change are rewritten on each update.
|
||||
|
||||
https://github.com/restic/restic/issues/5562
|
||||
https://github.com/restic/restic/pull/5648
|
||||
@@ -0,0 +1,7 @@
|
||||
Bugfix: Correctly handle `snapshots --group-by` with `--latest`
|
||||
|
||||
For the `snapshots` command, `--latest` did not interact correctly with a
|
||||
non-default `--group-by` value. This combination now behaves as intended.
|
||||
|
||||
https://github.com/restic/restic/issues/5586
|
||||
https://github.com/restic/restic/pull/5601
|
||||
@@ -0,0 +1,8 @@
|
||||
Bugfix: Avoid spurious `chmod` errors on certain file backends
|
||||
|
||||
On filesystems that do not support `chmod` (for example CIFS or FUSE-mounted
|
||||
WebDAV), restic since version 0.18.0 failed to remove stale locks, throwing
|
||||
the error `chmod ...: operation not supported`. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/5595
|
||||
https://github.com/restic/restic/pull/5596
|
||||
@@ -0,0 +1,9 @@
|
||||
Bugfix: Prevent `backup --stdin-from-command` from hanging
|
||||
|
||||
When using `--stdin-from-command`, the `backup` command could hang until
|
||||
manually cancelled if the backup stopped before all subprocess output was
|
||||
consumed, for example after a failed upload to the repository. This has
|
||||
now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/5683
|
||||
https://github.com/restic/restic/pull/21829
|
||||
@@ -0,0 +1,8 @@
|
||||
Enhancement: Show more detailed progress for `stats`
|
||||
|
||||
The `stats` command used to show progress only while loading the index. During
|
||||
the scan it printed only `scanning...` with no further updates. It now reports
|
||||
how many snapshots, files, and blobs have been processed so far.
|
||||
|
||||
https://github.com/restic/restic/issues/5689
|
||||
https://github.com/restic/restic/pull/5705
|
||||
@@ -0,0 +1,8 @@
|
||||
Bugfix: Respect `--user` and `--host` in `key passwd`
|
||||
|
||||
The `key passwd` command previously ignored the `--user` and
|
||||
`--host` options and always stored the new key with the current
|
||||
user and host name. These options are now honored.
|
||||
|
||||
https://github.com/restic/restic/issues/5757
|
||||
https://github.com/restic/restic/pull/21781
|
||||
@@ -0,0 +1,15 @@
|
||||
Change: Prevent excluding paths explicitly passed to `backup`
|
||||
|
||||
When e.g. `restic backup --exclude-if-present .git /home/user/data` was run
|
||||
and `/home/user/.git` existed, restic excluded the `data` directory from the
|
||||
snapshot. The same applied to `--exclude-caches` or `--one-file-system`.
|
||||
|
||||
Similarly, `restic backup --exclude-larger-than 1M large-file.bin` produced
|
||||
an empty snapshot when the file was larger than one megabyte.
|
||||
|
||||
The `backup` command now tracks which files and directories were specified on
|
||||
the command line and does not apply excludes to those paths. Content inside a
|
||||
backed-up directory is still filtered by excludes as before.
|
||||
|
||||
https://github.com/restic/restic/issues/5767
|
||||
https://github.com/restic/restic/pull/21797
|
||||
@@ -0,0 +1,9 @@
|
||||
Bugfix: Support serving a `restic mount` of a Windows system via Samba
|
||||
|
||||
A repository mounted using `restic mount` on a POSIX system could not use
|
||||
Samba to serve files from backups of Windows systems, while backups of
|
||||
non-Windows systems could be served successfully. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/2034
|
||||
https://github.com/restic/restic/issues/4382
|
||||
https://github.com/restic/restic/pull/21784
|
||||
@@ -0,0 +1,8 @@
|
||||
Change: Update dependencies and require Go 1.25 or newer
|
||||
|
||||
Dependencies have been updated. Building restic now requires
|
||||
Go 1.25 or newer. The Windows build with Go 1.26 was also fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/21791
|
||||
https://github.com/restic/restic/pull/5619
|
||||
https://github.com/restic/restic/pull/21796
|
||||
@@ -0,0 +1,9 @@
|
||||
Bugfix: Correctly handle pack files missing from the index
|
||||
|
||||
The `repair packs` command was unable to salvage blobs from a pack file if
|
||||
the pack file was not contained in the index or the index entry was incomplete.
|
||||
|
||||
The command now uses information from both the index and the pack file header.
|
||||
|
||||
https://github.com/restic/restic/issues/21820
|
||||
https://github.com/restic/restic/pull/21827
|
||||
@@ -0,0 +1,8 @@
|
||||
Enhancement: Add status counters to `copy` in verbose text output
|
||||
|
||||
The `copy` command now prints additional counters in text mode when
|
||||
`--verbose` is set: blobs to copy, their on-disk size, and the number
|
||||
of pack files read from the source repository.
|
||||
|
||||
https://github.com/restic/restic/issues/5175
|
||||
https://github.com/restic/restic/pull/5319
|
||||
@@ -0,0 +1,10 @@
|
||||
Enhancement: Enable Windows filesystem privileges before file access
|
||||
|
||||
Restic used to enable some Windows filesystem privileges only while reading
|
||||
or writing security descriptors. Extended attributes could therefore be read
|
||||
before enabling the backup privilege, possibly resulting in missed data or
|
||||
errors.
|
||||
|
||||
Restic now enables the relevant filesystem privileges before any file access.
|
||||
|
||||
https://github.com/restic/restic/pull/5424
|
||||
@@ -0,0 +1,19 @@
|
||||
Enhancement: Support configuring `nice` and `ionice` in the Docker image
|
||||
|
||||
The container entrypoint now reads optional scheduling hints from the
|
||||
environment:
|
||||
|
||||
- The environment variable `NICE` sets the process nice value (see `man nice`).
|
||||
|
||||
- The environment variable `IONICE_CLASS` selects the I/O scheduling class (see
|
||||
`man ionice`). Real-time classes need the `SYS_NICE` capability added to the
|
||||
container.
|
||||
|
||||
- The environment variable `IONICE_PRIORITY` sets the priority within
|
||||
`IONICE_CLASS` and has no effect unless `IONICE_CLASS` is set; it defaults to
|
||||
`4` (neutral priority).
|
||||
|
||||
For further details, please see:
|
||||
https://restic.readthedocs.io/en/stable/020_installation.html#docker-container
|
||||
|
||||
https://github.com/restic/restic/pull/5448
|
||||
@@ -0,0 +1,12 @@
|
||||
Bugfix: Correctly restore ACL inheritance state on Windows
|
||||
|
||||
Since security descriptor backups were added in restic 0.17.0, Access Control
|
||||
Entry inheritance was not restored correctly on Windows; restored permissions
|
||||
were always marked as explicit (not inherited) even when they were inherited
|
||||
from a parent folder.
|
||||
|
||||
The inheritance flags are now correctly applied when restoring the security
|
||||
descriptor, preserving the original permission structure.
|
||||
|
||||
https://github.com/restic/restic/issues/5427
|
||||
https://github.com/restic/restic/pull/5465
|
||||
@@ -0,0 +1,6 @@
|
||||
Enhancement: Add Open Container Initiative labels to release Docker image
|
||||
|
||||
The release Docker image now includes OCI-style image annotation labels,
|
||||
which helps external tooling identify the image.
|
||||
|
||||
https://github.com/restic/restic/pull/5523
|
||||
@@ -0,0 +1,9 @@
|
||||
Enhancement: Show timezone context in `snapshots` output
|
||||
|
||||
The `snapshots` command now prints which timezone is used for displayed
|
||||
timestamps. Snapshots may have been created in different timezones but are
|
||||
shown in the local timezone, so a footer line (for example, timestamps shown in
|
||||
CET) clarifies the display context when comparing snapshots from several
|
||||
sources.
|
||||
|
||||
https://github.com/restic/restic/pull/5588
|
||||
@@ -0,0 +1,6 @@
|
||||
Enhancement: Reduce `check`, `copy`, `diff` and `stats` memory usage
|
||||
|
||||
The `check`, `copy`, `diff` and `stats` commands now use less memory
|
||||
when handling large snapshots.
|
||||
|
||||
https://github.com/restic/restic/pull/5610
|
||||
@@ -0,0 +1,7 @@
|
||||
Bugfix: Make `find --pack` list blobs for tree packs
|
||||
|
||||
The `find --pack <tree-pack>` command now also reports
|
||||
blobs for packs that only contain tree blobs.
|
||||
|
||||
https://github.com/restic/restic/issues/5280
|
||||
https://github.com/restic/restic/pull/5664
|
||||
@@ -0,0 +1,8 @@
|
||||
Enhancement: Stricter and earlier validation of the `mount` point
|
||||
|
||||
The `mount` command previously accepted invalid mount points, resulting in an
|
||||
error after loading the repository. The specified mount point must now refer to
|
||||
a directory that the current user can access and write to, and this check is
|
||||
performed before opening the repository.
|
||||
|
||||
https://github.com/restic/restic/pull/5718
|
||||
@@ -0,0 +1,9 @@
|
||||
Enhancement: Significantly speed up index loading
|
||||
|
||||
Loading the index for a large repository is now significantly faster. Also,
|
||||
the `mount` command now loads the index once at startup and then only loads
|
||||
new index files as they appear. It also loads snapshots before printing that
|
||||
the repository is being served.
|
||||
|
||||
https://github.com/restic/restic/pull/5713
|
||||
https://github.com/restic/restic/pull/5720
|
||||
@@ -0,0 +1,8 @@
|
||||
Bugfix: Hide `stats` progress bar in JSON mode
|
||||
|
||||
Since restic 0.19.0, the `stats` command shows a progress bar. This
|
||||
progress bar was unintentionally displayed also when using the `--json`
|
||||
option, mixing regular text output with JSON. This is now fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/21866
|
||||
https://github.com/restic/restic/pull/21871
|
||||
@@ -0,0 +1,12 @@
|
||||
Bugfix: Restore old behavior of `snapshots --latest <n>` without `--group-by`
|
||||
|
||||
Restic 0.19.0 accidentally changed the behavior of `snapshots --latest <n>`
|
||||
to no longer group snapshots by host and paths by default.
|
||||
|
||||
The `snapshots --latest <n>` command now again uses the old behavior of
|
||||
grouping by host and paths when `--group-by` is not specified. However, when
|
||||
specifying `--group-by` the output is still grouped as requested, as in restic
|
||||
0.19.0.
|
||||
|
||||
https://github.com/restic/restic/issues/21869
|
||||
https://github.com/restic/restic/pull/21875
|
||||
@@ -0,0 +1,9 @@
|
||||
Bugfix: Remove read-only files via the SFTP backend on Windows servers
|
||||
|
||||
Since restic 0.19.0, repository files on the SFTP backend are marked
|
||||
read-only after save. On Windows SFTP servers, removing them failed
|
||||
with a permission error. The SFTP backend now clears the read-only flag
|
||||
before removing the file.
|
||||
|
||||
https://github.com/restic/restic/issues/21895
|
||||
https://github.com/restic/restic/pull/21897
|
||||
@@ -0,0 +1,8 @@
|
||||
Bugfix: Make `backup` respect excludes for duplicate directory entries
|
||||
|
||||
Since restic 0.19.0, backing up a directory with duplicate directory
|
||||
entries always produced "Warning: at least one source file could not be
|
||||
read", even when those files were excluded. This has now been fixed.
|
||||
|
||||
https://github.com/restic/restic/issues/21899
|
||||
https://github.com/restic/restic/pull/21900
|
||||
@@ -0,0 +1,12 @@
|
||||
Bugfix: Prevent mounting over the repository directory
|
||||
|
||||
Using a local repository directory as the `mount` target — or a path
|
||||
that contains it, or that it contains — caused the FUSE server to
|
||||
read its own backend files through the new mount, deadlocking the
|
||||
kernel and requiring a long reboot to recover.
|
||||
|
||||
Restic now resolves both paths and refuses any such overlap with a
|
||||
clear error before mounting.
|
||||
|
||||
https://github.com/restic/restic/issues/5234
|
||||
https://github.com/restic/restic/pull/5348
|
||||
@@ -0,0 +1,9 @@
|
||||
Bugfix: Skip inaccessible `backup` source paths
|
||||
|
||||
The `backup` command only skipped source paths that did not exist. A path that
|
||||
could not be accessed for another reason, such as a malformed path on Windows,
|
||||
was kept and produced an empty snapshot. Restic now skips any such path and
|
||||
aborts if none remain.
|
||||
|
||||
https://github.com/restic/restic/issues/5667
|
||||
https://github.com/restic/restic/pull/21852
|
||||
@@ -0,0 +1,10 @@
|
||||
Bugfix: Update `mount` latest symlink after snapshot reload
|
||||
|
||||
When `restic mount` was kept running while new snapshots were
|
||||
created, the new snapshots appeared in the mountpoint, but the `latest`
|
||||
symlink could still point to the previously latest snapshot. Restic now
|
||||
invalidates the cached snapshot directory entries after a snapshot reload so
|
||||
that `latest` points to the newest snapshot.
|
||||
|
||||
https://github.com/restic/restic/issues/5722
|
||||
https://github.com/restic/restic/pull/21873
|
||||
@@ -0,0 +1,9 @@
|
||||
Bugfix: Show timezone location in `snapshots` output
|
||||
|
||||
With restic 0.19.0, the `snapshots` command printed the current timezone when
|
||||
listing snapshots. However, that timezone label might change during the year,
|
||||
for example with daylight saving time. Restic now prints a more consistent and
|
||||
shorter version of the text.
|
||||
|
||||
https://github.com/restic/restic/pull/21876
|
||||
https://forum.restic.net/t/possible-bug-in-timezone-naming/10867
|
||||
@@ -0,0 +1,7 @@
|
||||
Bugfix: Prevent crash in mountpoint validation if mountpoint is inaccessible
|
||||
|
||||
Since restic 0.19.0, the `mount` command validates a mountpoint before loading
|
||||
the repository. If restic was unable to stat the mountpoint, this would result
|
||||
in a crash. This has now been fixed to correctly return an error instead.
|
||||
|
||||
https://github.com/restic/restic/pull/21879
|
||||
+11
-5
@@ -1,14 +1,20 @@
|
||||
# The first line must start with Bugfix:, Enhancement: or Change:,
|
||||
# including the colon. Use present tense and the imperative mood. Remove
|
||||
# lines starting with '#' from this template.
|
||||
Enhancement: Allow custom bar in the foo command
|
||||
# including the colon. 'Change:' is for breaking changes only.
|
||||
# Documentation-only changes do not get a changelog entry.
|
||||
# Include the affected command in the summary if relevant.
|
||||
#
|
||||
# Use present tense and the imperative mood. Remove lines starting
|
||||
# with '#' from this template.
|
||||
Enhancement: Allow custom bar in the `foo` command
|
||||
|
||||
# Describe the problem in the past tense, the new behavior in the present
|
||||
# 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.
|
||||
# only explain the new behavior. Aim for a short and concise description.
|
||||
# Use "Restic now ..." instead of "We have changed ...".
|
||||
#
|
||||
# Focus on user-facing behavior, not the implementation. The description should
|
||||
# be understandable for a regular user without knowledge of the implementation.
|
||||
|
||||
Restic foo always used the system-wide bar when deciding how to frob an
|
||||
item in the `baz` backend. It now permits selecting the bar with `--bar`
|
||||
|
||||
+10
-7
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@@ -9,26 +11,27 @@ import (
|
||||
"github.com/restic/restic/internal/debug"
|
||||
)
|
||||
|
||||
func createGlobalContext() context.Context {
|
||||
func createGlobalContext(stderr io.Writer) context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
ch := make(chan os.Signal, 1)
|
||||
go cleanupHandler(ch, cancel)
|
||||
go cleanupHandler(ch, cancel, stderr)
|
||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// cleanupHandler handles the SIGINT and SIGTERM signals.
|
||||
func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc) {
|
||||
func cleanupHandler(c <-chan os.Signal, cancel context.CancelFunc, stderr io.Writer) {
|
||||
s := <-c
|
||||
debug.Log("signal %v received, cleaning up", s)
|
||||
Warnf("%ssignal %v received, cleaning up\n", clearLine(0), s)
|
||||
// ignore error as there's no good way to handle it
|
||||
_, _ = fmt.Fprintf(stderr, "\rsignal %v received, cleaning up \n", s)
|
||||
|
||||
if val, _ := os.LookupEnv("RESTIC_DEBUG_STACKTRACE_SIGINT"); val != "" {
|
||||
_, _ = os.Stderr.WriteString("\n--- STACKTRACE START ---\n\n")
|
||||
_, _ = os.Stderr.WriteString(debug.DumpStacktrace())
|
||||
_, _ = os.Stderr.WriteString("\n--- STACKTRACE END ---\n")
|
||||
_, _ = stderr.Write([]byte("\n--- STACKTRACE START ---\n\n"))
|
||||
_, _ = stderr.Write([]byte(debug.DumpStacktrace()))
|
||||
_, _ = stderr.Write([]byte("\n--- STACKTRACE END ---\n"))
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
+98
-84
@@ -19,19 +19,20 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/restic/restic/internal/archiver"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/filter"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"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/termstatus"
|
||||
)
|
||||
|
||||
func newBackupCommand() *cobra.Command {
|
||||
func newBackupCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts BackupOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -51,22 +52,28 @@ Exit status is 10 if the repository does not exist.
|
||||
Exit status is 11 if the repository is already locked.
|
||||
Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
PreRun: func(_ *cobra.Command, _ []string) {
|
||||
PreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
if envVal := os.Getenv("RESTIC_READ_CONCURRENCY"); envVal != "" && !opts.readConcurrencyFlag.Changed {
|
||||
n, err := strconv.ParseUint(envVal, 10, 32)
|
||||
if err != nil {
|
||||
return errors.Fatalf("invalid value for RESTIC_READ_CONCURRENCY %q: %v", envVal, err)
|
||||
}
|
||||
opts.ReadConcurrency = uint(n)
|
||||
}
|
||||
if opts.Host == "" {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
debug.Log("os.Hostname() returned err: %v", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
opts.Host = hostname
|
||||
}
|
||||
return nil
|
||||
},
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
term, cancel := setupTermstatus()
|
||||
defer cancel()
|
||||
return runBackup(cmd.Context(), opts, globalOptions, term, args)
|
||||
return runBackup(cmd.Context(), opts, *globalOptions, globalOptions.Term, args)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -79,7 +86,7 @@ type BackupOptions struct {
|
||||
filter.ExcludePatternOptions
|
||||
|
||||
Parent string
|
||||
GroupBy restic.SnapshotGroupByOptions
|
||||
GroupBy data.SnapshotGroupByOptions
|
||||
Force bool
|
||||
ExcludeOtherFS bool
|
||||
ExcludeIfPresent []string
|
||||
@@ -89,7 +96,7 @@ type BackupOptions struct {
|
||||
Stdin bool
|
||||
StdinFilename string
|
||||
StdinCommand bool
|
||||
Tags restic.TagLists
|
||||
Tags data.TagLists
|
||||
Host string
|
||||
FilesFrom []string
|
||||
FilesFromVerbatim []string
|
||||
@@ -103,11 +110,13 @@ type BackupOptions struct {
|
||||
ReadConcurrency uint
|
||||
NoScan bool
|
||||
SkipIfUnchanged bool
|
||||
|
||||
readConcurrencyFlag *pflag.Flag
|
||||
}
|
||||
|
||||
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}
|
||||
opts.GroupBy = data.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)`)
|
||||
|
||||
@@ -140,13 +149,13 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
||||
f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
|
||||
if runtime.GOOS == "windows" {
|
||||
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)")
|
||||
}
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||
f.BoolVar(&opts.ExcludeCloudFiles, "exclude-cloud-files", false, "excludes online-only cloud files (such as OneDrive, iCloud drive, …)")
|
||||
}
|
||||
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
|
||||
readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32)
|
||||
opts.ReadConcurrency = uint(readConcurrency)
|
||||
opts.readConcurrencyFlag = f.Lookup("read-concurrency")
|
||||
|
||||
// parse host from env, if not exists or empty the default value will be used
|
||||
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||
@@ -159,13 +168,20 @@ 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
|
||||
// items exist at all.
|
||||
func filterExisting(items []string) (result []string, err error) {
|
||||
// ErrNoSourceData is used to report that no source data was found
|
||||
var ErrNoSourceData = errors.Fatal("all source directories/files do not exist")
|
||||
|
||||
// filterExisting returns the items that exist and can be accessed. It returns
|
||||
// ErrNoSourceData if none remain, or ErrInvalidSourceData if some were skipped.
|
||||
func filterExisting(items []string, warnf func(msg string, args ...interface{})) (result []string, err error) {
|
||||
for _, item := range items {
|
||||
_, err := fs.Lstat(item)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
Warnf("%v does not exist, skipping\n", item)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
warnf("%v does not exist, skipping\n", item)
|
||||
} else {
|
||||
warnf("%v cannot be accessed, skipping\n", item)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -173,10 +189,12 @@ func filterExisting(items []string) (result []string, err error) {
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return nil, errors.Fatal("all source directories/files do not exist")
|
||||
return nil, ErrNoSourceData
|
||||
} else if len(result) < len(items) {
|
||||
return result, ErrInvalidSourceData
|
||||
}
|
||||
|
||||
return
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// readLines reads all lines from the named file and returns them as a
|
||||
@@ -185,7 +203,7 @@ func filterExisting(items []string) (result []string, err error) {
|
||||
// If filename is empty, readPatternsFromFile returns an empty slice.
|
||||
// If filename is a dash (-), readPatternsFromFile will read the lines from the
|
||||
// standard input.
|
||||
func readLines(filename string) ([]string, error) {
|
||||
func readLines(filename string, stdin io.ReadCloser) ([]string, error) {
|
||||
if filename == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -196,7 +214,7 @@ func readLines(filename string) ([]string, error) {
|
||||
)
|
||||
|
||||
if filename == "-" {
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
data, err = io.ReadAll(stdin)
|
||||
} else {
|
||||
data, err = textfile.Read(filename)
|
||||
}
|
||||
@@ -221,8 +239,8 @@ func readLines(filename string) ([]string, error) {
|
||||
// readFilenamesFromFileRaw reads a list of filenames from the given file,
|
||||
// or stdin if filename is "-". Each filename is terminated by a zero byte,
|
||||
// which is stripped off.
|
||||
func readFilenamesFromFileRaw(filename string) (names []string, err error) {
|
||||
f := os.Stdin
|
||||
func readFilenamesFromFileRaw(filename string, stdin io.ReadCloser) (names []string, err error) {
|
||||
f := stdin
|
||||
if filename != "-" {
|
||||
if f, err = os.Open(filename); err != nil {
|
||||
return nil, err
|
||||
@@ -271,8 +289,8 @@ func readFilenamesRaw(r io.Reader) (names []string, err error) {
|
||||
}
|
||||
|
||||
// Check returns an error when an invalid combination of options was set.
|
||||
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
||||
if gopts.password == "" && !gopts.InsecureNoPassword {
|
||||
func (opts BackupOptions) Check(gopts global.Options, args []string) error {
|
||||
if gopts.Password == "" && !gopts.InsecureNoPassword {
|
||||
if opts.Stdin {
|
||||
return errors.Fatal("cannot read both password and data from stdin")
|
||||
}
|
||||
@@ -306,7 +324,7 @@ func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
||||
|
||||
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
||||
// from being saved in a snapshot based on path only
|
||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (fs []archiver.RejectByNameFunc, err error) {
|
||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...interface{})) (fs []archiver.RejectByNameFunc, err error) {
|
||||
// exclude restic cache
|
||||
if repo.Cache() != nil {
|
||||
f, err := rejectResticCache(repo)
|
||||
@@ -317,7 +335,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
|
||||
fs = append(fs, f)
|
||||
}
|
||||
|
||||
fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(Warnf)
|
||||
fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(warnf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -330,7 +348,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
|
||||
|
||||
// collectRejectFuncs returns a list of all functions which may reject data
|
||||
// from being saved in a snapshot based on path and file info
|
||||
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs []archiver.RejectFunc, err error) {
|
||||
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...interface{})) (funcs []archiver.RejectFunc, err error) {
|
||||
// allowed devices
|
||||
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
|
||||
f, err := archiver.RejectByDevice(targets, fs)
|
||||
@@ -354,10 +372,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
|
||||
}
|
||||
|
||||
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)
|
||||
f, err := archiver.RejectCloudFiles(warnf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -369,7 +384,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
|
||||
}
|
||||
|
||||
for _, spec := range opts.ExcludeIfPresent {
|
||||
f, err := archiver.RejectIfPresent(spec, Warnf)
|
||||
f, err := archiver.RejectIfPresent(spec, warnf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -381,13 +396,13 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
|
||||
}
|
||||
|
||||
// collectTargets returns a list of target files/dirs from several sources.
|
||||
func collectTargets(opts BackupOptions, args []string) (targets []string, err error) {
|
||||
func collectTargets(opts BackupOptions, args []string, warnf func(msg string, args ...interface{}), stdin io.ReadCloser) (targets []string, err error) {
|
||||
if opts.Stdin || opts.StdinCommand {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, file := range opts.FilesFrom {
|
||||
fromfile, err := readLines(file)
|
||||
fromfile, err := readLines(file, stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -405,14 +420,14 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
|
||||
return nil, fmt.Errorf("pattern: %s: %w", line, err)
|
||||
}
|
||||
if len(expanded) == 0 {
|
||||
Warnf("pattern %q does not match any files, skipping\n", line)
|
||||
warnf("pattern %q does not match any files, skipping\n", line)
|
||||
}
|
||||
targets = append(targets, expanded...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range opts.FilesFromVerbatim {
|
||||
fromfile, err := readLines(file)
|
||||
fromfile, err := readLines(file, stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -425,7 +440,7 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
|
||||
}
|
||||
|
||||
for _, file := range opts.FilesFromRaw {
|
||||
fromfile, err := readFilenamesFromFileRaw(file)
|
||||
fromfile, err := readFilenamesFromFileRaw(file, stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -439,17 +454,12 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
|
||||
return nil, errors.Fatal("nothing to backup, please specify source files/dirs")
|
||||
}
|
||||
|
||||
targets, err = filterExisting(targets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
return filterExisting(targets, warnf)
|
||||
}
|
||||
|
||||
// parent returns the ID of the parent snapshot. If there is none, nil is
|
||||
// returned.
|
||||
func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, opts BackupOptions, targets []string, timeStampLimit time.Time) (*restic.Snapshot, error) {
|
||||
func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, opts BackupOptions, targets []string, timeStampLimit time.Time) (*data.Snapshot, error) {
|
||||
if opts.Force {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -458,7 +468,7 @@ func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, o
|
||||
if snName == "" {
|
||||
snName = "latest"
|
||||
}
|
||||
f := restic.SnapshotFilter{TimestampLimit: timeStampLimit}
|
||||
f := data.SnapshotFilter{TimestampLimit: timeStampLimit}
|
||||
if opts.GroupBy.Host {
|
||||
f.Hosts = []string{opts.Host}
|
||||
}
|
||||
@@ -466,23 +476,29 @@ func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, o
|
||||
f.Paths = targets
|
||||
}
|
||||
if opts.GroupBy.Tag {
|
||||
f.Tags = []restic.TagList{opts.Tags.Flatten()}
|
||||
f.Tags = []data.TagList{opts.Tags.Flatten()}
|
||||
}
|
||||
|
||||
sn, _, err := f.FindLatest(ctx, repo, repo, snName)
|
||||
// Snapshot not found is ok if no explicit parent was set
|
||||
if opts.Parent == "" && errors.Is(err, restic.ErrNoSnapshotFound) {
|
||||
if opts.Parent == "" && errors.Is(err, data.ErrNoSnapshotFound) {
|
||||
err = nil
|
||||
}
|
||||
return sn, err
|
||||
}
|
||||
|
||||
func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error {
|
||||
func runBackup(ctx context.Context, opts BackupOptions, gopts global.Options, term ui.Terminal, args []string) error {
|
||||
var vsscfg fs.VSSConfig
|
||||
var err error
|
||||
|
||||
var printer backup.ProgressPrinter
|
||||
if gopts.JSON {
|
||||
printer = backup.NewJSONProgress(term, gopts.Verbosity)
|
||||
} else {
|
||||
printer = backup.NewTextProgress(term, gopts.Verbosity)
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
if vsscfg, err = fs.ParseVSSConfig(gopts.extended); err != nil {
|
||||
if vsscfg, err = fs.ParseVSSConfig(gopts.Extended); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -492,47 +508,46 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
return err
|
||||
}
|
||||
|
||||
targets, err := collectTargets(opts, args)
|
||||
success := true
|
||||
targets, err := collectTargets(opts, args, printer.E, term.InputRaw())
|
||||
if err != nil {
|
||||
return err
|
||||
if errors.Is(err, ErrInvalidSourceData) {
|
||||
success = false
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
timeStamp := time.Now()
|
||||
backupStart := timeStamp
|
||||
if opts.TimeStamp != "" {
|
||||
timeStamp, err = time.ParseInLocation(TimeFormat, opts.TimeStamp, time.Local)
|
||||
timeStamp, err = time.ParseInLocation(global.TimeFormat, opts.TimeStamp, time.Local)
|
||||
if err != nil {
|
||||
return errors.Fatalf("error in time option: %v\n", err)
|
||||
return errors.Fatalf("error in time option: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if gopts.verbosity >= 2 && !gopts.JSON {
|
||||
Verbosef("open repository\n")
|
||||
if gopts.Verbosity >= 2 && !gopts.JSON {
|
||||
printer.P("open repository")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, opts.DryRun)
|
||||
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, opts.DryRun, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
var progressPrinter backup.ProgressPrinter
|
||||
if gopts.JSON {
|
||||
progressPrinter = backup.NewJSONProgress(term, gopts.verbosity)
|
||||
} else {
|
||||
progressPrinter = backup.NewTextProgress(term, gopts.verbosity)
|
||||
}
|
||||
progressReporter := backup.NewProgress(progressPrinter,
|
||||
calculateProgressInterval(!gopts.Quiet, gopts.JSON))
|
||||
progressReporter := backup.NewProgress(printer,
|
||||
ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
|
||||
defer progressReporter.Done()
|
||||
|
||||
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
|
||||
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo)
|
||||
rejectByNameFuncs, err := collectRejectByNameFuncs(opts, repo, printer.E)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var parentSnapshot *restic.Snapshot
|
||||
var parentSnapshot *data.Snapshot
|
||||
if !opts.Stdin {
|
||||
parentSnapshot, err = findParentSnapshot(ctx, repo, opts, targets, timeStamp)
|
||||
if err != nil {
|
||||
@@ -541,19 +556,18 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
|
||||
if !gopts.JSON {
|
||||
if parentSnapshot != nil {
|
||||
progressPrinter.P("using parent snapshot %v\n", parentSnapshot.ID().Str())
|
||||
printer.P("using parent snapshot %v\n", parentSnapshot.ID().Str())
|
||||
} else {
|
||||
progressPrinter.P("no parent snapshot found, will read all files\n")
|
||||
printer.P("no parent snapshot found, will read all files\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !gopts.JSON {
|
||||
progressPrinter.V("load index files")
|
||||
printer.V("load index files")
|
||||
}
|
||||
|
||||
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
||||
err = repo.LoadIndex(ctx, bar)
|
||||
err = repo.LoadIndex(ctx, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -570,7 +584,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
|
||||
messageHandler := func(msg string, args ...interface{}) {
|
||||
if !gopts.JSON {
|
||||
progressPrinter.P(msg, args...)
|
||||
printer.P(msg, args...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,12 +595,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
|
||||
if opts.Stdin || opts.StdinCommand {
|
||||
if !gopts.JSON {
|
||||
progressPrinter.V("read data from stdin")
|
||||
printer.V("read data from stdin")
|
||||
}
|
||||
filename := path.Join("/", opts.StdinFilename)
|
||||
var source io.ReadCloser = os.Stdin
|
||||
source := term.InputRaw()
|
||||
if opts.StdinCommand {
|
||||
source, err = fs.NewCommandReader(ctx, args, globalOptions.stderr)
|
||||
source, err = fs.NewCommandReader(ctx, args, printer.E)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -606,7 +620,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
}
|
||||
|
||||
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
||||
rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS)
|
||||
rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS, printer.E)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -622,11 +636,11 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
sc := archiver.NewScanner(targetFS)
|
||||
sc.SelectByName = selectByNameFilter
|
||||
sc.Select = selectFilter
|
||||
sc.Error = progressPrinter.ScannerError
|
||||
sc.Error = printer.ScannerError
|
||||
sc.Result = progressReporter.ReportTotal
|
||||
|
||||
if !gopts.JSON {
|
||||
progressPrinter.V("start scan on %v", targets)
|
||||
printer.V("start scan on %v", targets)
|
||||
}
|
||||
wg.Go(func() error { return sc.Scan(cancelCtx, targets) })
|
||||
}
|
||||
@@ -635,7 +649,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
arch.SelectByName = selectByNameFilter
|
||||
arch.Select = selectFilter
|
||||
arch.WithAtime = opts.WithAtime
|
||||
success := true
|
||||
|
||||
arch.Error = func(item string, err error) error {
|
||||
success = false
|
||||
reterr := progressReporter.Error(item, err)
|
||||
@@ -666,12 +680,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
||||
Time: timeStamp,
|
||||
Hostname: opts.Host,
|
||||
ParentSnapshot: parentSnapshot,
|
||||
ProgramVersion: "restic " + version,
|
||||
ProgramVersion: "restic " + global.Version,
|
||||
SkipIfUnchanged: opts.SkipIfUnchanged,
|
||||
}
|
||||
|
||||
if !gopts.JSON {
|
||||
progressPrinter.V("start backup on %v", targets)
|
||||
printer.V("start backup on %v", targets)
|
||||
}
|
||||
_, id, summary, err := arch.Snapshot(ctx, targets, snapshotOpts)
|
||||
|
||||
|
||||
@@ -3,33 +3,34 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) error {
|
||||
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
||||
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) error {
|
||||
return withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
t.Logf("backing up %v in %v", target, dir)
|
||||
if dir != "" {
|
||||
cleanup := rtest.Chdir(t, dir)
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
||||
return runBackup(ctx, opts, gopts, term, target)
|
||||
opts.GroupBy = data.SnapshotGroupByOptions{Host: true, Path: true}
|
||||
return runBackup(ctx, opts, gopts, gopts.Term, target)
|
||||
})
|
||||
}
|
||||
|
||||
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 global.Options) {
|
||||
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
|
||||
rtest.Assert(t, err == nil, "Error while backing up: %v", err)
|
||||
}
|
||||
@@ -56,13 +57,13 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
||||
testListSnapshots(t, env.gopts, 1)
|
||||
|
||||
testRunCheck(t, env.gopts)
|
||||
stat1 := dirStats(env.repo)
|
||||
stat1 := dirStats(t, env.repo)
|
||||
|
||||
// second backup, implicit incremental
|
||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||
snapshotIDs := testListSnapshots(t, env.gopts, 2)
|
||||
|
||||
stat2 := dirStats(env.repo)
|
||||
stat2 := dirStats(t, env.repo)
|
||||
if stat2.size > stat1.size+stat1.size/10 {
|
||||
t.Error("repository size has grown by more than 10 percent")
|
||||
}
|
||||
@@ -74,7 +75,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||
snapshotIDs = testListSnapshots(t, env.gopts, 3)
|
||||
|
||||
stat3 := dirStats(env.repo)
|
||||
stat3 := dirStats(t, env.repo)
|
||||
if stat3.size > stat1.size+stat1.size/10 {
|
||||
t.Error("repository size has grown by more than 10 percent")
|
||||
}
|
||||
@@ -85,7 +86,7 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||
testRunRestore(t, env.gopts, restoredir, snapshotID.String()+":"+toPathInSnapshot(filepath.Dir(env.testdata)))
|
||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
||||
diff := directoriesContentsDiff(t, env.testdata, filepath.Join(restoredir, "testdata"))
|
||||
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
|
||||
}
|
||||
|
||||
@@ -218,41 +219,41 @@ func TestDryRunBackup(t *testing.T) {
|
||||
// dry run before first backup
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
||||
snapshotIDs := testListSnapshots(t, env.gopts, 0)
|
||||
packIDs := testRunList(t, "packs", env.gopts)
|
||||
packIDs := testRunList(t, env.gopts, "packs")
|
||||
rtest.Assert(t, len(packIDs) == 0,
|
||||
"expected no data, got %v", snapshotIDs)
|
||||
indexIDs := testRunList(t, "index", env.gopts)
|
||||
indexIDs := testRunList(t, env.gopts, "index")
|
||||
rtest.Assert(t, len(indexIDs) == 0,
|
||||
"expected no index, got %v", snapshotIDs)
|
||||
|
||||
// first backup
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
||||
snapshotIDs = testListSnapshots(t, env.gopts, 1)
|
||||
packIDs = testRunList(t, "packs", env.gopts)
|
||||
indexIDs = testRunList(t, "index", env.gopts)
|
||||
packIDs = testRunList(t, env.gopts, "packs")
|
||||
indexIDs = testRunList(t, env.gopts, "index")
|
||||
|
||||
// dry run between backups
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
||||
snapshotIDsAfter := testListSnapshots(t, env.gopts, 1)
|
||||
rtest.Equals(t, snapshotIDs, snapshotIDsAfter)
|
||||
dataIDsAfter := testRunList(t, "packs", env.gopts)
|
||||
dataIDsAfter := testRunList(t, env.gopts, "packs")
|
||||
rtest.Equals(t, packIDs, dataIDsAfter)
|
||||
indexIDsAfter := testRunList(t, "index", env.gopts)
|
||||
indexIDsAfter := testRunList(t, env.gopts, "index")
|
||||
rtest.Equals(t, indexIDs, indexIDsAfter)
|
||||
|
||||
// second backup, implicit incremental
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
||||
snapshotIDs = testListSnapshots(t, env.gopts, 2)
|
||||
packIDs = testRunList(t, "packs", env.gopts)
|
||||
indexIDs = testRunList(t, "index", env.gopts)
|
||||
packIDs = testRunList(t, env.gopts, "packs")
|
||||
indexIDs = testRunList(t, env.gopts, "index")
|
||||
|
||||
// another dry run
|
||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
||||
snapshotIDsAfter = testListSnapshots(t, env.gopts, 2)
|
||||
rtest.Equals(t, snapshotIDs, snapshotIDsAfter)
|
||||
dataIDsAfter = testRunList(t, "packs", env.gopts)
|
||||
dataIDsAfter = testRunList(t, env.gopts, "packs")
|
||||
rtest.Equals(t, packIDs, dataIDsAfter)
|
||||
indexIDsAfter = testRunList(t, "index", env.gopts)
|
||||
indexIDsAfter = testRunList(t, env.gopts, "index")
|
||||
rtest.Equals(t, indexIDs, indexIDsAfter)
|
||||
}
|
||||
|
||||
@@ -262,22 +263,27 @@ func TestBackupNonExistingFile(t *testing.T) {
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
|
||||
_ = withRestoreGlobalOptions(func() error {
|
||||
globalOptions.stderr = io.Discard
|
||||
p := filepath.Join(env.testdata, "0", "0", "9")
|
||||
dirs := []string{
|
||||
filepath.Join(p, "0"),
|
||||
filepath.Join(p, "1"),
|
||||
filepath.Join(p, "nonexisting"),
|
||||
filepath.Join(p, "5"),
|
||||
}
|
||||
|
||||
p := filepath.Join(env.testdata, "0", "0", "9")
|
||||
dirs := []string{
|
||||
filepath.Join(p, "0"),
|
||||
filepath.Join(p, "1"),
|
||||
filepath.Join(p, "nonexisting"),
|
||||
filepath.Join(p, "5"),
|
||||
}
|
||||
opts := BackupOptions{}
|
||||
|
||||
opts := BackupOptions{}
|
||||
|
||||
testRunBackup(t, "", dirs, opts, env.gopts)
|
||||
return nil
|
||||
})
|
||||
// mix of existing and non-existing files
|
||||
err := testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts)
|
||||
rtest.Assert(t, err != nil, "expected error for non-existing file")
|
||||
rtest.Assert(t, errors.Is(err, ErrInvalidSourceData), "expected ErrInvalidSourceData; got %v", err)
|
||||
// only non-existing file
|
||||
dirs = []string{
|
||||
filepath.Join(p, "nonexisting"),
|
||||
}
|
||||
err = testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts)
|
||||
rtest.Assert(t, err != nil, "expected error for non-existing file")
|
||||
rtest.Assert(t, errors.Is(err, ErrNoSourceData), "expected ErrNoSourceData; got %v", err)
|
||||
}
|
||||
|
||||
func TestBackupSelfHealing(t *testing.T) {
|
||||
@@ -438,13 +444,13 @@ func TestIncrementalBackup(t *testing.T) {
|
||||
|
||||
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
||||
testRunCheck(t, env.gopts)
|
||||
stat1 := dirStats(env.repo)
|
||||
stat1 := dirStats(t, env.repo)
|
||||
|
||||
rtest.OK(t, appendRandomData(testfile, incrementalSecondWrite))
|
||||
|
||||
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
||||
testRunCheck(t, env.gopts)
|
||||
stat2 := dirStats(env.repo)
|
||||
stat2 := dirStats(t, env.repo)
|
||||
if stat2.size-stat1.size > incrementalFirstWrite {
|
||||
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
|
||||
}
|
||||
@@ -454,14 +460,13 @@ func TestIncrementalBackup(t *testing.T) {
|
||||
|
||||
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
||||
testRunCheck(t, env.gopts)
|
||||
stat3 := dirStats(env.repo)
|
||||
stat3 := dirStats(t, env.repo)
|
||||
if stat3.size-stat2.size > incrementalFirstWrite {
|
||||
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
|
||||
}
|
||||
t.Logf("repository grown by %d bytes", stat3.size-stat2.size)
|
||||
}
|
||||
|
||||
// nolint: staticcheck // false positive nil pointer dereference check
|
||||
func TestBackupTags(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
@@ -481,7 +486,7 @@ func TestBackupTags(t *testing.T) {
|
||||
"expected no tags, got %v", newest.Tags)
|
||||
parent := newest
|
||||
|
||||
opts.Tags = restic.TagLists{[]string{"NL"}}
|
||||
opts.Tags = data.TagLists{[]string{"NL"}}
|
||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||
testRunCheck(t, env.gopts)
|
||||
newest, _ = testRunSnapshots(t, env.gopts)
|
||||
@@ -497,7 +502,6 @@ func TestBackupTags(t *testing.T) {
|
||||
"expected parent to be %v, got %v", parent.ID, newest.Parent)
|
||||
}
|
||||
|
||||
// nolint: staticcheck // false positive nil pointer dereference check
|
||||
func TestBackupProgramVersion(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
@@ -509,7 +513,7 @@ func TestBackupProgramVersion(t *testing.T) {
|
||||
if newest == nil {
|
||||
t.Fatal("expected a backup, got nil")
|
||||
}
|
||||
resticVersion := "restic " + version
|
||||
resticVersion := "restic " + global.Version
|
||||
rtest.Assert(t, newest.ProgramVersion == resticVersion,
|
||||
"expected %v, got %v", resticVersion, newest.ProgramVersion)
|
||||
}
|
||||
@@ -567,7 +571,7 @@ func TestHardLink(t *testing.T) {
|
||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||
testRunRestore(t, env.gopts, restoredir, snapshotID.String())
|
||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, "testdata"))
|
||||
diff := directoriesContentsDiff(t, env.testdata, filepath.Join(restoredir, "testdata"))
|
||||
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
||||
|
||||
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
|
||||
@@ -703,7 +707,7 @@ func TestBackupEmptyPassword(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
env.gopts.password = ""
|
||||
env.gopts.Password = ""
|
||||
env.gopts.InsecureNoPassword = true
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
@@ -67,10 +68,35 @@ func TestCollectTargets(t *testing.T) {
|
||||
FilesFromRaw: []string{f3.Name()},
|
||||
}
|
||||
|
||||
targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")})
|
||||
targets, err := collectTargets(opts, []string{filepath.Join(dir, "cmdline arg")}, t.Logf, nil)
|
||||
rtest.OK(t, err)
|
||||
sort.Strings(targets)
|
||||
rtest.Equals(t, expect, targets)
|
||||
|
||||
_, err = collectTargets(opts, []string{filepath.Join(dir, "cmdline arg"), filepath.Join(dir, "non-existing-file")}, t.Logf, nil)
|
||||
rtest.Assert(t, err == ErrInvalidSourceData, "expected error when not all targets exist")
|
||||
}
|
||||
|
||||
func TestFilterExistingUnreadable(t *testing.T) {
|
||||
dir := rtest.TempDir(t)
|
||||
|
||||
existing := filepath.Join(dir, "existing")
|
||||
rtest.OK(t, os.Mkdir(existing, 0755))
|
||||
|
||||
file := filepath.Join(dir, "file")
|
||||
rtest.OK(t, os.WriteFile(file, []byte("x"), 0600))
|
||||
|
||||
// Regression test for #5667. A target whose Lstat fails with an error other
|
||||
// than ErrNotExist must be skipped (ENOTDIR on unix, NUL byte everywhere).
|
||||
for _, unreadable := range []string{filepath.Join(file, "child"), "invalid\x00path"} {
|
||||
result, err := filterExisting([]string{unreadable}, t.Logf)
|
||||
rtest.Assert(t, errors.Is(err, ErrNoSourceData), "input %q: expected ErrNoSourceData; got %v", unreadable, err)
|
||||
rtest.Assert(t, len(result) == 0, "input %q: expected no targets; got %v", unreadable, result)
|
||||
|
||||
result, err = filterExisting([]string{existing, unreadable}, t.Logf)
|
||||
rtest.Assert(t, errors.Is(err, ErrInvalidSourceData), "input %q: expected ErrInvalidSourceData; got %v", unreadable, err)
|
||||
rtest.Equals(t, []string{existing}, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFilenamesRaw(t *testing.T) {
|
||||
|
||||
+12
-9
@@ -10,13 +10,14 @@ import (
|
||||
|
||||
"github.com/restic/restic/internal/backend/cache"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newCacheCommand() *cobra.Command {
|
||||
func newCacheCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts CacheOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -34,7 +35,7 @@ Exit status is 1 if there was any error.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return runCache(opts, globalOptions, args)
|
||||
return runCache(opts, *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,7 +56,9 @@ func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
|
||||
f.BoolVar(&opts.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 global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if len(args) > 0 {
|
||||
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
|
||||
}
|
||||
@@ -83,17 +86,17 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
if len(oldDirs) == 0 {
|
||||
Verbosef("no old cache dirs found\n")
|
||||
printer.P("no old cache dirs found")
|
||||
return nil
|
||||
}
|
||||
|
||||
Verbosef("remove %d old cache directories\n", len(oldDirs))
|
||||
printer.P("remove %d old cache directories", len(oldDirs))
|
||||
|
||||
for _, item := range oldDirs {
|
||||
dir := filepath.Join(cachedir, item.Name())
|
||||
err = os.RemoveAll(dir)
|
||||
if err != nil {
|
||||
Warnf("unable to remove %v: %v\n", dir, err)
|
||||
printer.E("unable to remove %v: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +126,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
||||
}
|
||||
|
||||
if len(dirs) == 0 {
|
||||
Printf("no cache dirs found, basedir is %v\n", cachedir)
|
||||
printer.S("no cache dirs found, basedir is %v", cachedir)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -159,8 +162,8 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
||||
})
|
||||
}
|
||||
|
||||
_ = tab.Write(globalOptions.stdout)
|
||||
Printf("%d cache dirs in %s\n", len(dirs), cachedir)
|
||||
_ = tab.Write(gopts.Term.OutputWriter())
|
||||
printer.S("%d cache dirs in %s", len(dirs), cachedir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+27
-24
@@ -7,14 +7,17 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
)
|
||||
|
||||
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
|
||||
|
||||
func newCatCommand() *cobra.Command {
|
||||
func newCatCommand(globalOptions *global.Options) *cobra.Command {
|
||||
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",
|
||||
@@ -33,7 +36,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runCat(cmd.Context(), globalOptions, args)
|
||||
return runCat(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
ValidArgs: catAllowedCmds,
|
||||
}
|
||||
@@ -63,12 +66,14 @@ func validateCatArgs(args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
func runCat(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
if err := validateCatArgs(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -80,7 +85,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
if tpe != "masterkey" && tpe != "config" && tpe != "snapshot" && tpe != "tree" {
|
||||
id, err = restic.ParseID(args[1])
|
||||
if err != nil {
|
||||
return errors.Fatalf("unable to parse ID: %v\n", err)
|
||||
return errors.Fatalf("unable to parse ID: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +96,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
Println(string(buf))
|
||||
printer.S(string(buf))
|
||||
return nil
|
||||
case "index":
|
||||
buf, err := repo.LoadUnpacked(ctx, restic.IndexFile, id)
|
||||
@@ -99,12 +104,12 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
Println(string(buf))
|
||||
printer.S(string(buf))
|
||||
return nil
|
||||
case "snapshot":
|
||||
sn, _, err := restic.FindSnapshot(ctx, repo, repo, args[1])
|
||||
sn, _, err := data.FindSnapshot(ctx, repo, repo, args[1])
|
||||
if err != nil {
|
||||
return errors.Fatalf("could not find snapshot: %v\n", err)
|
||||
return errors.Fatalf("could not find snapshot: %v", err)
|
||||
}
|
||||
|
||||
buf, err := json.MarshalIndent(sn, "", " ")
|
||||
@@ -112,7 +117,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
Println(string(buf))
|
||||
printer.S(string(buf))
|
||||
return nil
|
||||
case "key":
|
||||
key, err := repository.LoadKey(ctx, repo, id)
|
||||
@@ -125,7 +130,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
Println(string(buf))
|
||||
printer.S(string(buf))
|
||||
return nil
|
||||
case "masterkey":
|
||||
buf, err := json.MarshalIndent(repo.Key(), "", " ")
|
||||
@@ -133,7 +138,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
Println(string(buf))
|
||||
printer.S(string(buf))
|
||||
return nil
|
||||
case "lock":
|
||||
lock, err := restic.LoadLock(ctx, repo, id)
|
||||
@@ -146,7 +151,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
Println(string(buf))
|
||||
printer.S(string(buf))
|
||||
return nil
|
||||
|
||||
case "pack":
|
||||
@@ -158,15 +163,14 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
|
||||
hash := restic.Hash(buf)
|
||||
if !hash.Equal(id) {
|
||||
Warnf("Warning: hash of data does not match ID, want\n %v\ngot:\n %v\n", id.String(), hash.String())
|
||||
printer.E("Warning: hash of data does not match ID, want\n %v\ngot:\n %v", id.String(), hash.String())
|
||||
}
|
||||
|
||||
_, err = globalOptions.stdout.Write(buf)
|
||||
_, err = term.OutputRaw().Write(buf)
|
||||
return err
|
||||
|
||||
case "blob":
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
err = repo.LoadIndex(ctx, bar)
|
||||
err = repo.LoadIndex(ctx, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -181,25 +185,24 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = globalOptions.stdout.Write(buf)
|
||||
_, err = term.OutputRaw().Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.Fatal("blob not found")
|
||||
|
||||
case "tree":
|
||||
sn, subfolder, err := restic.FindSnapshot(ctx, repo, repo, args[1])
|
||||
sn, subfolder, err := data.FindSnapshot(ctx, repo, repo, args[1])
|
||||
if err != nil {
|
||||
return errors.Fatalf("could not find snapshot: %v\n", err)
|
||||
return errors.Fatalf("could not find snapshot: %v", err)
|
||||
}
|
||||
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
err = repo.LoadIndex(ctx, bar)
|
||||
err = repo.LoadIndex(ctx, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
|
||||
sn.Tree, err = data.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -208,7 +211,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = globalOptions.stdout.Write(buf)
|
||||
_, err = term.OutputRaw().Write(buf)
|
||||
return err
|
||||
|
||||
default:
|
||||
|
||||
+104
-69
@@ -15,25 +15,30 @@ import (
|
||||
|
||||
"github.com/restic/restic/internal/backend/cache"
|
||||
"github.com/restic/restic/internal/checker"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
func newCheckCommand() *cobra.Command {
|
||||
func newCheckCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts CheckOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "check [flags]",
|
||||
Short: "Check the repository for errors",
|
||||
Long: `
|
||||
The "check" command tests the repository for errors and reports any errors it
|
||||
finds. It can also be used to read all data and therefore simulate a restore.
|
||||
finds.
|
||||
|
||||
By default, the "check" command will always load all data directly from the
|
||||
repository and not use a local cache.
|
||||
By default, check verifies the structural consistency and integrity of
|
||||
snapshots, trees and pack files. To also verify the integrity of the actual
|
||||
backed-up data, use the --read-data or --read-data-subset flags.
|
||||
|
||||
By default, check creates a new temporary cache directory to verify data.
|
||||
To reuse the existing cache, use the --with-cache flag.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -47,14 +52,13 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
term, cancel := setupTermstatus()
|
||||
defer cancel()
|
||||
summary, err := runCheck(cmd.Context(), opts, globalOptions, args, term)
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
summary, err := runCheck(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||
if globalOptions.JSON {
|
||||
if err != nil && summary.NumErrors == 0 {
|
||||
summary.NumErrors = 1
|
||||
}
|
||||
term.Print(ui.ToJSONString(summary))
|
||||
globalOptions.Term.Print(ui.ToJSONString(summary))
|
||||
}
|
||||
return err
|
||||
},
|
||||
@@ -73,6 +77,7 @@ type CheckOptions struct {
|
||||
ReadDataSubset string
|
||||
CheckUnused bool
|
||||
WithCache bool
|
||||
data.SnapshotFilter
|
||||
}
|
||||
|
||||
func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
|
||||
@@ -86,6 +91,7 @@ func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
|
||||
panic(err)
|
||||
}
|
||||
f.BoolVar(&opts.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository")
|
||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||
}
|
||||
|
||||
func checkFlags(opts CheckOptions) error {
|
||||
@@ -173,7 +179,7 @@ func parsePercentage(s string) (float64, error) {
|
||||
// - if the user explicitly requested --no-cache, we don't use any cache
|
||||
// - if the user provides --cache-dir, we use a cache in a temporary sub-directory of the specified directory and the sub-directory is deleted after the check
|
||||
// - by default, we use a cache in a temporary directory that is deleted after the check
|
||||
func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress.Printer) (cleanup func()) {
|
||||
func prepareCheckCache(opts CheckOptions, gopts *global.Options, printer progress.Printer) (cleanup func()) {
|
||||
cleanup = func() {}
|
||||
if opts.WithCache {
|
||||
// use the default cache, no setup needed
|
||||
@@ -194,7 +200,7 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
|
||||
// use a cache in a temporary directory
|
||||
err := os.MkdirAll(cachedir, 0755)
|
||||
if err != nil {
|
||||
Warnf("unable to create cache directory %s, disabling cache: %v\n", cachedir, err)
|
||||
printer.E("unable to create cache directory %s, disabling cache: %v", cachedir, err)
|
||||
gopts.NoCache = true
|
||||
return cleanup
|
||||
}
|
||||
@@ -220,15 +226,12 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
|
||||
return cleanup
|
||||
}
|
||||
|
||||
func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args []string, term *termstatus.Terminal) (checkSummary, error) {
|
||||
func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args []string, term ui.Terminal) (checkSummary, error) {
|
||||
summary := checkSummary{MessageType: "summary"}
|
||||
if len(args) != 0 {
|
||||
return summary, errors.Fatal("the check command expects no arguments, only options - please see `restic help check` for usage and flags")
|
||||
}
|
||||
|
||||
var printer progress.Printer
|
||||
if !gopts.JSON {
|
||||
printer = newTerminalProgressPrinter(gopts.verbosity, term)
|
||||
printer = ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
} else {
|
||||
printer = newJSONErrorPrinter(term)
|
||||
}
|
||||
@@ -239,32 +242,38 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
||||
if !gopts.NoLock {
|
||||
printer.P("create exclusive lock for repository\n")
|
||||
}
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, gopts.NoLock)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
chkr := checker.New(repo, opts.CheckUnused)
|
||||
err = chkr.LoadSnapshots(ctx)
|
||||
err = chkr.LoadSnapshots(ctx, &opts.SnapshotFilter, args)
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
printer.P("load indexes\n")
|
||||
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
||||
hints, errs := chkr.LoadIndex(ctx, bar)
|
||||
hints, errs := chkr.LoadIndex(ctx, printer)
|
||||
if ctx.Err() != nil {
|
||||
return summary, ctx.Err()
|
||||
}
|
||||
|
||||
errorsFound := false
|
||||
salvagePacks := restic.NewIDSet()
|
||||
|
||||
for _, hint := range hints {
|
||||
switch hint.(type) {
|
||||
case *checker.ErrDuplicatePacks:
|
||||
switch hint := hint.(type) {
|
||||
case *repository.ErrIncompletePackEntry:
|
||||
printer.E("%s", hint.Error())
|
||||
salvagePacks.Insert(hint.PackID)
|
||||
errorsFound = true
|
||||
summary.NumErrors++
|
||||
case *repository.ErrDuplicatePacks:
|
||||
printer.S("%s", hint.Error())
|
||||
summary.HintRepairIndex = true
|
||||
case *checker.ErrMixedPack:
|
||||
case *repository.ErrMixedPack:
|
||||
printer.S("%s", hint.Error())
|
||||
summary.HintPrune = true
|
||||
default:
|
||||
@@ -293,13 +302,12 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
||||
|
||||
orphanedPacks := 0
|
||||
errChan := make(chan error)
|
||||
salvagePacks := restic.NewIDSet()
|
||||
|
||||
printer.P("check all packs\n")
|
||||
go chkr.Packs(ctx, errChan)
|
||||
|
||||
for err := range errChan {
|
||||
var packErr *checker.PackError
|
||||
var packErr *repository.PackError
|
||||
if errors.As(err, &packErr) {
|
||||
if packErr.Orphaned {
|
||||
orphanedPacks++
|
||||
@@ -363,6 +371,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
||||
return summary, ctx.Err()
|
||||
}
|
||||
|
||||
// the following block only used for tests
|
||||
if opts.CheckUnused {
|
||||
unused, err := chkr.UnusedBlobs(ctx)
|
||||
if err != nil {
|
||||
@@ -374,12 +383,16 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
||||
}
|
||||
}
|
||||
|
||||
doReadData := func(packs map[restic.ID]int64) {
|
||||
readDataFilter, err := buildPacksFilter(opts, printer, chkr.IsFiltered())
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
|
||||
if readDataFilter != nil {
|
||||
p := printer.NewCounter("packs")
|
||||
p.SetMax(uint64(len(packs)))
|
||||
errChan := make(chan error)
|
||||
|
||||
go chkr.ReadPacks(ctx, packs, p, errChan)
|
||||
go chkr.ReadPacks(ctx, readDataFilter, p, errChan)
|
||||
|
||||
for err := range errChan {
|
||||
errorsFound = true
|
||||
@@ -392,47 +405,6 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
||||
p.Done()
|
||||
}
|
||||
|
||||
switch {
|
||||
case opts.ReadData:
|
||||
printer.P("read all data\n")
|
||||
doReadData(selectPacksByBucket(chkr.GetPacks(), 1, 1))
|
||||
case opts.ReadDataSubset != "":
|
||||
var packs map[restic.ID]int64
|
||||
dataSubset, err := stringToIntSlice(opts.ReadDataSubset)
|
||||
if err == nil {
|
||||
bucket := dataSubset[0]
|
||||
totalBuckets := dataSubset[1]
|
||||
packs = selectPacksByBucket(chkr.GetPacks(), bucket, totalBuckets)
|
||||
packCount := uint64(len(packs))
|
||||
printer.P("read group #%d of %d data packs (out of total %d packs in %d groups)\n", bucket, packCount, chkr.CountPacks(), totalBuckets)
|
||||
} else if strings.HasSuffix(opts.ReadDataSubset, "%") {
|
||||
percentage, err := parsePercentage(opts.ReadDataSubset)
|
||||
if err == nil {
|
||||
packs = selectRandomPacksByPercentage(chkr.GetPacks(), percentage)
|
||||
printer.P("read %.1f%% of data packs\n", percentage)
|
||||
}
|
||||
} else {
|
||||
repoSize := int64(0)
|
||||
allPacks := chkr.GetPacks()
|
||||
for _, size := range allPacks {
|
||||
repoSize += size
|
||||
}
|
||||
if repoSize == 0 {
|
||||
return summary, errors.Fatal("Cannot read from a repository having size 0")
|
||||
}
|
||||
subsetSize, _ := ui.ParseBytes(opts.ReadDataSubset)
|
||||
if subsetSize > repoSize {
|
||||
subsetSize = repoSize
|
||||
}
|
||||
packs = selectRandomPacksByFileSize(chkr.GetPacks(), subsetSize, repoSize)
|
||||
printer.P("read %d bytes of data packs\n", subsetSize)
|
||||
}
|
||||
if packs == nil {
|
||||
return summary, errors.Fatal("internal error: failed to select packs to check")
|
||||
}
|
||||
doReadData(packs)
|
||||
}
|
||||
|
||||
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")
|
||||
for id := range salvagePacks {
|
||||
@@ -456,6 +428,64 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func buildPacksFilter(opts CheckOptions, printer progress.Printer,
|
||||
filteredStatus bool) (func(packs map[restic.ID]int64) map[restic.ID]int64, error) {
|
||||
typeData := ""
|
||||
if filteredStatus {
|
||||
typeData = "filtered "
|
||||
}
|
||||
switch {
|
||||
case opts.ReadData:
|
||||
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
|
||||
printer.P("read all %sdata", typeData)
|
||||
return packs
|
||||
}, nil
|
||||
case opts.ReadDataSubset != "":
|
||||
dataSubset, err := stringToIntSlice(opts.ReadDataSubset)
|
||||
if err == nil {
|
||||
bucket := dataSubset[0]
|
||||
totalBuckets := dataSubset[1]
|
||||
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
|
||||
packCount := uint64(len(packs))
|
||||
packs = selectPacksByBucket(packs, bucket, totalBuckets)
|
||||
printer.P("read group #%d of %d %sdata packs (out of total %d packs in %d groups", bucket, len(packs), typeData, packCount, totalBuckets)
|
||||
return packs
|
||||
}, nil
|
||||
} else if strings.HasSuffix(opts.ReadDataSubset, "%") {
|
||||
percentage, err := parsePercentage(opts.ReadDataSubset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
|
||||
printer.P("read %.1f%% of %spackfiles", percentage, typeData)
|
||||
return selectRandomPacksByPercentage(packs, percentage)
|
||||
}, nil
|
||||
}
|
||||
|
||||
repoSize := int64(0)
|
||||
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
|
||||
for _, size := range packs {
|
||||
repoSize += size
|
||||
}
|
||||
subsetSize, _ := ui.ParseBytes(opts.ReadDataSubset)
|
||||
if subsetSize > repoSize {
|
||||
subsetSize = repoSize
|
||||
}
|
||||
if repoSize > 0 {
|
||||
packs = selectRandomPacksByFileSize(packs, subsetSize, repoSize)
|
||||
}
|
||||
percentage := float64(subsetSize) / float64(repoSize) * 100.0
|
||||
if repoSize == 0 {
|
||||
percentage = 100
|
||||
}
|
||||
printer.P("read %d bytes (%.1f%%) of %sdata packs\n", subsetSize, percentage, typeData)
|
||||
return packs
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// selectPacksByBucket selects subsets of packs by ranges of buckets.
|
||||
func selectPacksByBucket(allPacks map[restic.ID]int64, bucket, totalBuckets uint) map[restic.ID]int64 {
|
||||
packs := make(map[restic.ID]int64)
|
||||
@@ -527,6 +557,10 @@ func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) *progress.Counter {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
||||
status := checkError{
|
||||
MessageType: "error",
|
||||
@@ -536,5 +570,6 @@ func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
||||
}
|
||||
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
|
||||
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
|
||||
|
||||
@@ -1,39 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
func testRunCheck(t testing.TB, gopts GlobalOptions) {
|
||||
func testRunCheck(t testing.TB, gopts global.Options) {
|
||||
t.Helper()
|
||||
output, err := testRunCheckOutput(gopts, true)
|
||||
output, err := testRunCheckOutput(t, gopts, true)
|
||||
if err != nil {
|
||||
t.Error(output)
|
||||
t.Fatalf("unexpected error: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) {
|
||||
func testRunCheckMustFail(t testing.TB, gopts global.Options) {
|
||||
t.Helper()
|
||||
_, err := testRunCheckOutput(gopts, false)
|
||||
_, err := testRunCheckOutput(t, gopts, false)
|
||||
rtest.Assert(t, err != nil, "expected non nil error after check of damaged repository")
|
||||
}
|
||||
|
||||
func testRunCheckOutput(gopts GlobalOptions, checkUnused bool) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
gopts.stdout = buf
|
||||
err := withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
||||
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, error) {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
opts := CheckOptions{
|
||||
ReadData: true,
|
||||
CheckUnused: checkUnused,
|
||||
}
|
||||
_, err := runCheck(context.TODO(), opts, gopts, nil, term)
|
||||
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.Term)
|
||||
return err
|
||||
})
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func testRunCheckOutputWithOpts(t testing.TB, gopts global.Options, opts CheckOptions, args []string) (string, error) {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
gopts.Verbosity = 2
|
||||
_, err := runCheck(context.TODO(), opts, gopts, args, gopts.Term)
|
||||
return err
|
||||
})
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
func TestCheckWithSnaphotFilter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
opts CheckOptions
|
||||
args []string
|
||||
expectedOutput string
|
||||
}{
|
||||
{ // full --read-data, all snapshots
|
||||
CheckOptions{ReadData: true},
|
||||
nil,
|
||||
"4 / 4 packs",
|
||||
},
|
||||
{ // full --read-data, all snapshots
|
||||
CheckOptions{ReadData: true},
|
||||
nil,
|
||||
"2 / 2 snapshots",
|
||||
},
|
||||
{ // full --read-data, latest snapshot
|
||||
CheckOptions{ReadData: true},
|
||||
[]string{"latest"},
|
||||
"2 / 2 packs",
|
||||
},
|
||||
{ // full --read-data, latest snapshot
|
||||
CheckOptions{ReadData: true},
|
||||
[]string{"latest"},
|
||||
"1 / 1 snapshots",
|
||||
},
|
||||
{ // --read-data-subset, latest snapshot
|
||||
CheckOptions{ReadDataSubset: "1%"},
|
||||
[]string{"latest"},
|
||||
"1 / 1 packs",
|
||||
},
|
||||
{ // --read-data-subset, latest snapshot
|
||||
CheckOptions{ReadDataSubset: "1%"},
|
||||
[]string{"latest"},
|
||||
"filtered",
|
||||
},
|
||||
}
|
||||
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
opts := BackupOptions{}
|
||||
testRunBackup(t, env.testdata+"/0", []string{"for_cmd_ls"}, opts, env.gopts)
|
||||
testRunBackup(t, env.testdata+"/0", []string{"0/9"}, opts, env.gopts)
|
||||
|
||||
for _, testCase := range testCases {
|
||||
output, err := testRunCheckOutputWithOpts(t, env.gopts, testCase.opts, testCase.args)
|
||||
rtest.OK(t, err)
|
||||
|
||||
hasOutput := strings.Contains(output, testCase.expectedOutput)
|
||||
rtest.Assert(t, hasOutput, `expected to find substring %q, but did not find it`, testCase.expectedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
@@ -202,7 +203,7 @@ func TestPrepareCheckCache(t *testing.T) {
|
||||
err := os.Remove(tmpDirBase)
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
gopts := GlobalOptions{CacheDir: tmpDirBase}
|
||||
gopts := global.Options{CacheDir: tmpDirBase}
|
||||
cleanup := prepareCheckCache(testCase.opts, &gopts, &progress.NoopPrinter{})
|
||||
files, err := os.ReadDir(tmpDirBase)
|
||||
rtest.OK(t, err)
|
||||
@@ -232,7 +233,7 @@ func TestPrepareCheckCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareDefaultCheckCache(t *testing.T) {
|
||||
gopts := GlobalOptions{CacheDir: ""}
|
||||
gopts := global.Options{CacheDir: ""}
|
||||
cleanup := prepareCheckCache(CheckOptions{}, &gopts, &progress.NoopPrinter{})
|
||||
_, err := os.ReadDir(gopts.CacheDir)
|
||||
rtest.OK(t, err)
|
||||
|
||||
+197
-109
@@ -3,18 +3,24 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newCopyCommand() *cobra.Command {
|
||||
func newCopyCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts CopyOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "copy [flags] [snapshotID ...]",
|
||||
@@ -46,7 +52,8 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runCopy(cmd.Context(), opts, globalOptions, args)
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runCopy(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -56,17 +63,51 @@ Exit status is 12 if the password is incorrect.
|
||||
|
||||
// CopyOptions bundles all options for the copy command.
|
||||
type CopyOptions struct {
|
||||
secondaryRepoOptions
|
||||
restic.SnapshotFilter
|
||||
global.SecondaryRepoOptions
|
||||
data.SnapshotFilter
|
||||
}
|
||||
|
||||
func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
|
||||
opts.secondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from")
|
||||
opts.SecondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from")
|
||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||
}
|
||||
|
||||
func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error {
|
||||
secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination")
|
||||
// collectAllSnapshots: select all snapshot trees to be copied
|
||||
func collectAllSnapshots(ctx context.Context, opts CopyOptions,
|
||||
srcSnapshotLister restic.Lister, srcRepo restic.Repository,
|
||||
dstSnapshotByOriginal map[restic.ID][]*data.Snapshot, args []string, printer progress.Printer,
|
||||
) iter.Seq[*data.Snapshot] {
|
||||
return func(yield func(*data.Snapshot) bool) {
|
||||
for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args, printer) {
|
||||
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
|
||||
srcOriginal := *sn.ID()
|
||||
if sn.Original != nil {
|
||||
srcOriginal = *sn.Original
|
||||
}
|
||||
if originalSns, ok := dstSnapshotByOriginal[srcOriginal]; ok {
|
||||
isCopy := false
|
||||
for _, originalSn := range originalSns {
|
||||
if similarSnapshots(originalSn, sn) {
|
||||
printer.V("\n%v", sn)
|
||||
printer.V("skipping source snapshot %s, was already copied to snapshot %s", sn.ID().Str(), originalSn.ID().Str())
|
||||
isCopy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isCopy {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !yield(sn) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runCopy(ctx context.Context, opts CopyOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
secondaryGopts, isFromRepo, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "destination")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,13 +116,13 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
|
||||
gopts, secondaryGopts = secondaryGopts, gopts
|
||||
}
|
||||
|
||||
ctx, srcRepo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||
ctx, srcRepo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
ctx, dstRepo, unlock, err := openWithAppendLock(ctx, secondaryGopts, false)
|
||||
ctx, dstRepo, unlock, err := openWithAppendLock(ctx, secondaryGopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -98,18 +139,16 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
|
||||
}
|
||||
|
||||
debug.Log("Loading source index")
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
if err := srcRepo.LoadIndex(ctx, bar); err != nil {
|
||||
if err := srcRepo.LoadIndex(ctx, printer); err != nil {
|
||||
return err
|
||||
}
|
||||
bar = newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
debug.Log("Loading destination index")
|
||||
if err := dstRepo.LoadIndex(ctx, bar); err != nil {
|
||||
if err := dstRepo.LoadIndex(ctx, printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot)
|
||||
for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil) {
|
||||
dstSnapshotByOriginal := make(map[restic.ID][]*data.Snapshot)
|
||||
for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil, printer) {
|
||||
if sn.Original != nil && !sn.Original.IsNull() {
|
||||
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
||||
}
|
||||
@@ -120,53 +159,16 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// remember already processed trees across all snapshots
|
||||
visitedTrees := restic.NewIDSet()
|
||||
selectedSnapshots := collectAllSnapshots(ctx, opts, srcSnapshotLister, srcRepo, dstSnapshotByOriginal, args, printer)
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args) {
|
||||
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
|
||||
srcOriginal := *sn.ID()
|
||||
if sn.Original != nil {
|
||||
srcOriginal = *sn.Original
|
||||
}
|
||||
|
||||
if originalSns, ok := dstSnapshotByOriginal[srcOriginal]; ok {
|
||||
isCopy := false
|
||||
for _, originalSn := range originalSns {
|
||||
if similarSnapshots(originalSn, sn) {
|
||||
Verboseff("\n%v\n", sn)
|
||||
Verboseff("skipping source snapshot %s, was already copied to snapshot %s\n", sn.ID().Str(), originalSn.ID().Str())
|
||||
isCopy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isCopy {
|
||||
continue
|
||||
}
|
||||
}
|
||||
Verbosef("\n%v\n", sn)
|
||||
Verbosef(" copy started, this may take a while...\n")
|
||||
if err := copyTree(ctx, srcRepo, dstRepo, visitedTrees, *sn.Tree, gopts.Quiet); err != nil {
|
||||
return err
|
||||
}
|
||||
debug.Log("tree copied")
|
||||
|
||||
// save snapshot
|
||||
sn.Parent = nil // Parent does not have relevance in the new repo.
|
||||
// Use Original as a persistent snapshot ID
|
||||
if sn.Original == nil {
|
||||
sn.Original = sn.ID()
|
||||
}
|
||||
newID, err := restic.SaveSnapshot(ctx, dstRepo, sn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Verbosef("snapshot %s saved\n", newID.Str())
|
||||
if err := copyTreeBatched(ctx, srcRepo, dstRepo, selectedSnapshots, printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
|
||||
func similarSnapshots(sna *data.Snapshot, snb *data.Snapshot) bool {
|
||||
// everything except Parent and Original must match
|
||||
if !sna.Time.Equal(snb.Time) || !sna.Tree.Equal(*snb.Tree) || sna.Hostname != snb.Hostname ||
|
||||
sna.Username != snb.Username || sna.UID != snb.UID || sna.GID != snb.GID ||
|
||||
@@ -185,72 +187,158 @@ func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
||||
visitedTrees restic.IDSet, rootTreeID restic.ID, quiet bool) error {
|
||||
// copyTreeBatched copies multiple snapshots in one go. Snapshots are written after
|
||||
// data equivalent to at least 10 packfiles was written.
|
||||
func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
||||
selectedSnapshots iter.Seq[*data.Snapshot], printer progress.Printer) error {
|
||||
|
||||
wg, wgCtx := errgroup.WithContext(ctx)
|
||||
// remember already processed trees across all snapshots
|
||||
visitedTrees := srcRepo.NewAssociatedBlobSet()
|
||||
|
||||
treeStream := restic.StreamTrees(wgCtx, wg, srcRepo, restic.IDs{rootTreeID}, func(treeID restic.ID) bool {
|
||||
visited := visitedTrees.Has(treeID)
|
||||
visitedTrees.Insert(treeID)
|
||||
return visited
|
||||
}, nil)
|
||||
targetSize := uint64(dstRepo.PackSize()) * 100
|
||||
minDuration := 1 * time.Minute
|
||||
|
||||
copyBlobs := restic.NewBlobSet()
|
||||
packList := restic.NewIDSet()
|
||||
// use pull-based iterator to allow iteration in multiple steps
|
||||
next, stop := iter.Pull(selectedSnapshots)
|
||||
defer stop()
|
||||
|
||||
enqueue := func(h restic.BlobHandle) {
|
||||
pb := srcRepo.LookupBlob(h.Type, h.ID)
|
||||
copyBlobs.Insert(h)
|
||||
for _, p := range pb {
|
||||
packList.Insert(p.PackID)
|
||||
for {
|
||||
var batch []*data.Snapshot
|
||||
batchSize := uint64(0)
|
||||
startTime := time.Now()
|
||||
|
||||
// call WithBlobUploader() once and then loop over all selectedSnapshots
|
||||
err := dstRepo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||
for batchSize < targetSize || time.Since(startTime) < minDuration {
|
||||
sn, ok := next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
batch = append(batch, sn)
|
||||
|
||||
printer.P("\n%v", sn)
|
||||
printer.P(" copy started, this may take a while...")
|
||||
sizeBlobs, err := copyTree(ctx, srcRepo, dstRepo, visitedTrees, *sn.Tree, printer, uploader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
debug.Log("tree copied")
|
||||
batchSize += sizeBlobs
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if no snapshots were processed in this batch, we're done
|
||||
if len(batch) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// add a newline to separate saved snapshot messages from the other messages
|
||||
if len(batch) > 1 {
|
||||
printer.P("")
|
||||
}
|
||||
// save all the snapshots
|
||||
for _, sn := range batch {
|
||||
err := copySaveSnapshot(ctx, sn, dstRepo, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg.Go(func() error {
|
||||
for tree := range treeStream {
|
||||
if tree.Error != nil {
|
||||
return fmt.Errorf("LoadTree(%v) returned error %v", tree.ID.Str(), tree.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do we already have this tree blob?
|
||||
treeHandle := restic.BlobHandle{ID: tree.ID, Type: restic.TreeBlob}
|
||||
if _, ok := dstRepo.LookupBlobSize(treeHandle.Type, treeHandle.ID); !ok {
|
||||
// copy raw tree bytes to avoid problems if the serialization changes
|
||||
enqueue(treeHandle)
|
||||
}
|
||||
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
||||
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer progress.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
|
||||
|
||||
for _, entry := range tree.Nodes {
|
||||
// Recursion into directories is handled by StreamTrees
|
||||
// Copy the blobs for this file.
|
||||
for _, blobID := range entry.Content {
|
||||
h := restic.BlobHandle{Type: restic.DataBlob, ID: blobID}
|
||||
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
|
||||
enqueue(h)
|
||||
}
|
||||
}
|
||||
copyBlobs := srcRepo.NewAssociatedBlobSet()
|
||||
packList := restic.NewIDSet()
|
||||
var lock sync.Mutex
|
||||
|
||||
enqueue := func(h restic.BlobHandle) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
|
||||
pb := srcRepo.LookupBlob(h.Type, h.ID)
|
||||
copyBlobs.Insert(h)
|
||||
for _, p := range pb {
|
||||
packList.Insert(p.PackID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := data.StreamTrees(ctx, srcRepo, restic.IDs{rootTreeID}, nil, func(treeID restic.ID) bool {
|
||||
handle := restic.BlobHandle{ID: treeID, Type: restic.TreeBlob}
|
||||
visited := visitedTrees.Has(handle)
|
||||
visitedTrees.Insert(handle)
|
||||
return visited
|
||||
}, func(treeID restic.ID, err error, nodes data.TreeNodeIterator) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("LoadTree(%v) returned error %v", treeID.Str(), err)
|
||||
}
|
||||
|
||||
// copy raw tree bytes to avoid problems if the serialization changes
|
||||
enqueue(restic.BlobHandle{ID: treeID, Type: restic.TreeBlob})
|
||||
|
||||
for item := range nodes {
|
||||
if item.Error != nil {
|
||||
return item.Error
|
||||
}
|
||||
// Recursion into directories is handled by StreamTrees
|
||||
// Copy the blobs for this file.
|
||||
for _, blobID := range item.Node.Content {
|
||||
enqueue(restic.BlobHandle{Type: restic.DataBlob, ID: blobID})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
err := wg.Wait()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
sizeBlobs := copyStats(srcRepo, copyBlobs, packList, printer)
|
||||
bar := printer.NewCounter("packs copied")
|
||||
err = repository.CopyBlobs(ctx, srcRepo, dstRepo, uploader, packList, copyBlobs, bar, printer.P)
|
||||
if err != nil {
|
||||
return 0, errors.Fatalf("%s", err)
|
||||
}
|
||||
return sizeBlobs, nil
|
||||
}
|
||||
|
||||
// copyStats: print statistics for the blobs to be copied
|
||||
func copyStats(srcRepo restic.Repository, copyBlobs restic.AssociatedBlobSet, packList restic.IDSet, printer progress.Printer) uint64 {
|
||||
// count and size
|
||||
countBlobs := 0
|
||||
sizeBlobs := uint64(0)
|
||||
for blob := range copyBlobs.Keys() {
|
||||
for _, blob := range srcRepo.LookupBlob(blob.Type, blob.ID) {
|
||||
countBlobs++
|
||||
sizeBlobs += uint64(blob.Length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
printer.V(" copy %d blobs with disk size %s in %d packfiles\n",
|
||||
countBlobs, ui.FormatBytes(uint64(sizeBlobs)), len(packList))
|
||||
return sizeBlobs
|
||||
}
|
||||
|
||||
func copySaveSnapshot(ctx context.Context, sn *data.Snapshot, dstRepo restic.Repository, printer progress.Printer) error {
|
||||
sn.Parent = nil // Parent does not have relevance in the new repo.
|
||||
// Use Original as a persistent snapshot ID
|
||||
if sn.Original == nil {
|
||||
sn.Original = sn.ID()
|
||||
}
|
||||
newID, err := data.SaveSnapshot(ctx, dstRepo, sn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bar := newProgressMax(!quiet, uint64(len(packList)), "packs copied")
|
||||
_, err = repository.Repack(
|
||||
ctx,
|
||||
srcRepo,
|
||||
dstRepo,
|
||||
packList,
|
||||
copyBlobs,
|
||||
bar,
|
||||
func(msg string, args ...interface{}) { fmt.Printf(msg+"\n", args...) },
|
||||
)
|
||||
bar.Done()
|
||||
if err != nil {
|
||||
return errors.Fatal(err.Error())
|
||||
}
|
||||
printer.P("snapshot %s saved, copied from source snapshot %s", newID.Str(), sn.ID().Str())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,23 +6,28 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
)
|
||||
|
||||
func testRunCopy(t testing.TB, srcGopts GlobalOptions, dstGopts GlobalOptions) {
|
||||
func testRunCopy(t testing.TB, srcGopts global.Options, dstGopts global.Options) {
|
||||
gopts := srcGopts
|
||||
gopts.Repo = dstGopts.Repo
|
||||
gopts.password = dstGopts.password
|
||||
gopts.Password = dstGopts.Password
|
||||
gopts.InsecureNoPassword = dstGopts.InsecureNoPassword
|
||||
copyOpts := CopyOptions{
|
||||
secondaryRepoOptions: secondaryRepoOptions{
|
||||
SecondaryRepoOptions: global.SecondaryRepoOptions{
|
||||
Repo: srcGopts.Repo,
|
||||
password: srcGopts.password,
|
||||
Password: srcGopts.Password,
|
||||
InsecureNoPassword: srcGopts.InsecureNoPassword,
|
||||
},
|
||||
}
|
||||
|
||||
rtest.OK(t, runCopy(context.TODO(), copyOpts, gopts, nil))
|
||||
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runCopy(context.TODO(), copyOpts, gopts, nil, gopts.Term)
|
||||
}))
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
@@ -45,8 +50,8 @@ func TestCopy(t *testing.T) {
|
||||
copiedSnapshotIDs := testListSnapshots(t, env2.gopts, 3)
|
||||
|
||||
// Check that the copies size seems reasonable
|
||||
stat := dirStats(env.repo)
|
||||
stat2 := dirStats(env2.repo)
|
||||
stat := dirStats(t, env.repo)
|
||||
stat2 := dirStats(t, env2.repo)
|
||||
sizeDiff := int64(stat.size) - int64(stat2.size)
|
||||
if sizeDiff < 0 {
|
||||
sizeDiff = -sizeDiff
|
||||
@@ -69,7 +74,7 @@ func TestCopy(t *testing.T) {
|
||||
testRunRestore(t, env2.gopts, restoredir, snapshotID.String())
|
||||
foundMatch := false
|
||||
for cmpdir := range origRestores {
|
||||
diff := directoriesContentsDiff(restoredir, cmpdir)
|
||||
diff := directoriesContentsDiff(t, restoredir, cmpdir)
|
||||
if diff == "" {
|
||||
delete(origRestores, cmpdir)
|
||||
foundMatch = true
|
||||
@@ -80,6 +85,41 @@ func TestCopy(t *testing.T) {
|
||||
}
|
||||
|
||||
rtest.Assert(t, len(origRestores) == 0, "found not copied snapshots")
|
||||
|
||||
// check that snapshots were properly batched while copying
|
||||
_, _, countBlobs := testPackAndBlobCounts(t, env.gopts)
|
||||
countTreePacksDst, countDataPacksDst, countBlobsDst := testPackAndBlobCounts(t, env2.gopts)
|
||||
|
||||
rtest.Equals(t, countBlobs, countBlobsDst, "expected blob count in both repos to be equal")
|
||||
rtest.Equals(t, countTreePacksDst, 1, "expected 1 tree packfile")
|
||||
rtest.Equals(t, countDataPacksDst, 1, "expected 1 data packfile")
|
||||
}
|
||||
|
||||
func testPackAndBlobCounts(t testing.TB, gopts global.Options) (countTreePacks int, countDataPacks int, countBlobs int) {
|
||||
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
rtest.OK(t, repo.List(context.TODO(), restic.PackFile, func(id restic.ID, size int64) error {
|
||||
blobs, _, err := repo.ListPack(context.TODO(), id, size)
|
||||
rtest.OK(t, err)
|
||||
rtest.Assert(t, len(blobs) > 0, "a packfile should contain at least one blob")
|
||||
|
||||
switch blobs[0].Type {
|
||||
case restic.TreeBlob:
|
||||
countTreePacks++
|
||||
case restic.DataBlob:
|
||||
countDataPacks++
|
||||
}
|
||||
countBlobs += len(blobs)
|
||||
return nil
|
||||
}))
|
||||
return nil
|
||||
}))
|
||||
|
||||
return countTreePacks, countDataPacks, countBlobs
|
||||
}
|
||||
|
||||
func TestCopyIncremental(t *testing.T) {
|
||||
@@ -142,7 +182,7 @@ func TestCopyToEmptyPassword(t *testing.T) {
|
||||
defer cleanup()
|
||||
env2, cleanup2 := withTestEnvironment(t)
|
||||
defer cleanup2()
|
||||
env2.gopts.password = ""
|
||||
env2.gopts.Password = ""
|
||||
env2.gopts.InsecureNoPassword = true
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
|
||||
+83
-91
@@ -1,5 +1,4 @@
|
||||
//go:build debug
|
||||
// +build debug
|
||||
|
||||
package main
|
||||
|
||||
@@ -12,7 +11,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -22,32 +20,36 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/restic/restic/internal/crypto"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/repository/index"
|
||||
"github.com/restic/restic/internal/repository/pack"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func registerDebugCommand(cmd *cobra.Command) {
|
||||
func registerDebugCommand(cmd *cobra.Command, globalOptions *global.Options) {
|
||||
cmd.AddCommand(
|
||||
newDebugCommand(),
|
||||
newDebugCommand(globalOptions),
|
||||
)
|
||||
}
|
||||
|
||||
func newDebugCommand() *cobra.Command {
|
||||
func newDebugCommand(globalOptions *global.Options) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "debug",
|
||||
Short: "Debug commands",
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
cmd.AddCommand(newDebugDumpCommand())
|
||||
cmd.AddCommand(newDebugExamineCommand())
|
||||
cmd.AddCommand(newDebugDumpCommand(globalOptions))
|
||||
cmd.AddCommand(newDebugExamineCommand(globalOptions))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newDebugDumpCommand() *cobra.Command {
|
||||
func newDebugDumpCommand(globalOptions *global.Options) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dump [indexes|snapshots|all|packs]",
|
||||
Short: "Dump data structures",
|
||||
@@ -66,13 +68,13 @@ Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDebugDump(cmd.Context(), globalOptions, args)
|
||||
return runDebugDump(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newDebugExamineCommand() *cobra.Command {
|
||||
func newDebugExamineCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts DebugExamineOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -80,7 +82,7 @@ func newDebugExamineCommand() *cobra.Command {
|
||||
Short: "Examine a pack file",
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDebugExamine(cmd.Context(), globalOptions, opts, args)
|
||||
return runDebugExamine(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -113,7 +115,7 @@ func prettyPrintJSON(wr io.Writer, item interface{}) error {
|
||||
}
|
||||
|
||||
func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io.Writer) error {
|
||||
return restic.ForAllSnapshots(ctx, repo, repo, nil, func(id restic.ID, snapshot *restic.Snapshot, err error) error {
|
||||
return data.ForAllSnapshots(ctx, repo, repo, nil, func(id restic.ID, snapshot *data.Snapshot, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -141,13 +143,13 @@ type Blob struct {
|
||||
Offset uint `json:"offset"`
|
||||
}
|
||||
|
||||
func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer) error {
|
||||
func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer, printer progress.Printer) error {
|
||||
|
||||
var m sync.Mutex
|
||||
return restic.ParallelList(ctx, repo, restic.PackFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error {
|
||||
blobs, _, err := repo.ListPack(ctx, id, size)
|
||||
if err != nil {
|
||||
Warnf("error for pack %v: %v\n", id.Str(), err)
|
||||
printer.E("error for pack %v: %v", id.Str(), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -170,9 +172,9 @@ 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, printer progress.Printer) error {
|
||||
return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error {
|
||||
Printf("index_id: %v\n", id)
|
||||
printer.S("index_id: %v", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -181,12 +183,14 @@ func dumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Wr
|
||||
})
|
||||
}
|
||||
|
||||
func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
func runDebugDump(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.Fatal("type not specified")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -196,20 +200,20 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
|
||||
|
||||
switch tpe {
|
||||
case "indexes":
|
||||
return dumpIndexes(ctx, repo, globalOptions.stdout)
|
||||
return dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
case "snapshots":
|
||||
return debugPrintSnapshots(ctx, repo, globalOptions.stdout)
|
||||
return debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
||||
case "packs":
|
||||
return printPacks(ctx, repo, globalOptions.stdout)
|
||||
return printPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
case "all":
|
||||
Printf("snapshots:\n")
|
||||
err := debugPrintSnapshots(ctx, repo, globalOptions.stdout)
|
||||
printer.S("snapshots:")
|
||||
err := debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Printf("\nindexes:\n")
|
||||
err = dumpIndexes(ctx, repo, globalOptions.stdout)
|
||||
printer.S("indexes:")
|
||||
err = dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -220,11 +224,11 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
|
||||
}
|
||||
}
|
||||
|
||||
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
||||
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer progress.Printer) []byte {
|
||||
if bytewise {
|
||||
Printf(" trying to repair blob by finding a broken byte\n")
|
||||
printer.S(" trying to repair blob by finding a broken byte")
|
||||
} else {
|
||||
Printf(" trying to repair blob with single bit flip\n")
|
||||
printer.S(" trying to repair blob with single bit flip")
|
||||
}
|
||||
|
||||
ch := make(chan int)
|
||||
@@ -234,7 +238,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
||||
var found bool
|
||||
|
||||
workers := runtime.GOMAXPROCS(0)
|
||||
Printf(" spinning up %d worker functions\n", runtime.GOMAXPROCS(0))
|
||||
printer.S(" spinning up %d worker functions", runtime.GOMAXPROCS(0))
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Go(func() error {
|
||||
// make a local copy of the buffer
|
||||
@@ -248,9 +252,9 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
||||
nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
|
||||
plaintext, err := key.Open(plaintext[:0], nonce, plaintext, nil)
|
||||
if err == nil {
|
||||
Printf("\n")
|
||||
Printf(" blob could be repaired by XORing byte %v with 0x%02x\n", idx, pattern)
|
||||
Printf(" hash is %v\n", restic.Hash(plaintext))
|
||||
printer.S("")
|
||||
printer.S(" blob could be repaired by XORing byte %v with 0x%02x", idx, pattern)
|
||||
printer.S(" hash is %v", restic.Hash(plaintext))
|
||||
close(done)
|
||||
found = true
|
||||
fixed = plaintext
|
||||
@@ -291,7 +295,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
||||
select {
|
||||
case ch <- i:
|
||||
case <-done:
|
||||
Printf(" done after %v\n", time.Since(start))
|
||||
printer.S(" done after %v", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -301,7 +305,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
||||
remaining := len(input) - i
|
||||
eta := time.Duration(float64(remaining)/gps) * time.Second
|
||||
|
||||
Printf("\r%d byte of %d done (%.2f%%), %.0f byte per second, ETA %v",
|
||||
printer.S("\r%d byte of %d done (%.2f%%), %.0f byte per second, ETA %v",
|
||||
i, len(input), float32(i)/float32(len(input))*100, gps, eta)
|
||||
info = time.Now()
|
||||
}
|
||||
@@ -314,7 +318,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
||||
}
|
||||
|
||||
if !found {
|
||||
Printf("\n blob could not be repaired\n")
|
||||
printer.S("\n blob could not be repaired")
|
||||
}
|
||||
return fixed
|
||||
}
|
||||
@@ -335,7 +339,7 @@ func decryptUnsigned(k *crypto.Key, buf []byte) []byte {
|
||||
return out
|
||||
}
|
||||
|
||||
func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, packID restic.ID, list []restic.Blob) error {
|
||||
func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, packID restic.ID, list restic.Blobs, printer progress.Printer) error {
|
||||
dec, err := zstd.NewReader(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -347,17 +351,11 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
||||
return err
|
||||
}
|
||||
|
||||
wg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
if opts.ReuploadBlobs {
|
||||
repo.StartPackUploader(ctx, wg)
|
||||
}
|
||||
|
||||
wg.Go(func() error {
|
||||
err = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||
for _, blob := range list {
|
||||
Printf(" loading blob %v at %v (length %v)\n", blob.ID, blob.Offset, blob.Length)
|
||||
printer.S(" loading blob %v at %v (length %v)", blob.ID, blob.Offset, blob.Length)
|
||||
if int(blob.Offset+blob.Length) > len(pack) {
|
||||
Warnf("skipping truncated blob\n")
|
||||
printer.E("skipping truncated blob")
|
||||
continue
|
||||
}
|
||||
buf := pack[blob.Offset : blob.Offset+blob.Length]
|
||||
@@ -368,16 +366,16 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
||||
outputPrefix := ""
|
||||
filePrefix := ""
|
||||
if err != nil {
|
||||
Warnf("error decrypting blob: %v\n", err)
|
||||
printer.E("error decrypting blob: %v", err)
|
||||
if opts.TryRepair || opts.RepairByte {
|
||||
plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte)
|
||||
plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte, printer)
|
||||
}
|
||||
if plaintext != nil {
|
||||
outputPrefix = "repaired "
|
||||
filePrefix = "repaired-"
|
||||
} else {
|
||||
plaintext = decryptUnsigned(key, buf)
|
||||
err = storePlainBlob(blob.ID, "damaged-", plaintext)
|
||||
err = storePlainBlob(blob.ID, "damaged-", plaintext, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -388,7 +386,7 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
||||
if blob.IsCompressed() {
|
||||
decompressed, err := dec.DecodeAll(plaintext, nil)
|
||||
if err != nil {
|
||||
Printf(" failed to decompress blob %v\n", blob.ID)
|
||||
printer.S(" failed to decompress blob %v", blob.ID)
|
||||
}
|
||||
if decompressed != nil {
|
||||
plaintext = decompressed
|
||||
@@ -398,37 +396,32 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
||||
id := restic.Hash(plaintext)
|
||||
var prefix string
|
||||
if !id.Equal(blob.ID) {
|
||||
Printf(" successfully %vdecrypted blob (length %v), hash is %v, ID does not match, wanted %v\n", outputPrefix, len(plaintext), id, blob.ID)
|
||||
printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID does not match, wanted %v", outputPrefix, len(plaintext), id, blob.ID)
|
||||
prefix = "wrong-hash-"
|
||||
} else {
|
||||
Printf(" successfully %vdecrypted blob (length %v), hash is %v, ID matches\n", outputPrefix, len(plaintext), id)
|
||||
printer.S(" successfully %vdecrypted blob (length %v), hash is %v, ID matches", outputPrefix, len(plaintext), id)
|
||||
prefix = "correct-"
|
||||
}
|
||||
if opts.ExtractPack {
|
||||
err = storePlainBlob(id, filePrefix+prefix, plaintext)
|
||||
err = storePlainBlob(id, filePrefix+prefix, plaintext, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if opts.ReuploadBlobs {
|
||||
_, _, _, err := repo.SaveBlob(ctx, blob.Type, plaintext, id, true)
|
||||
_, _, _, err := uploader.SaveBlob(ctx, blob.Type, plaintext, id, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Printf(" uploaded %v %v\n", blob.Type, id)
|
||||
printer.S(" uploaded %v %v", blob.Type, id)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.ReuploadBlobs {
|
||||
return repo.Flush(ctx)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
func storePlainBlob(id restic.ID, prefix string, plain []byte) error {
|
||||
func storePlainBlob(id restic.ID, prefix string, plain []byte, printer progress.Printer) error {
|
||||
filename := fmt.Sprintf("%s%s.bin", prefix, id)
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
@@ -446,16 +439,18 @@ func storePlainBlob(id restic.ID, prefix string, plain []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
Printf("decrypt of blob %v stored at %v\n", id, filename)
|
||||
printer.S("decrypt of blob %v stored at %v", id, filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamineOptions, args []string) error {
|
||||
func runDebugExamine(ctx context.Context, gopts global.Options, opts DebugExamineOptions, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if opts.ExtractPack && gopts.NoLock {
|
||||
return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, gopts.NoLock)
|
||||
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -467,7 +462,7 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
|
||||
if err != nil {
|
||||
id, err = restic.Find(ctx, repo, restic.PackFile, name)
|
||||
if err != nil {
|
||||
Warnf("error: %v\n", err)
|
||||
printer.E("error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -478,16 +473,15 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
|
||||
return errors.Fatal("no pack files to examine")
|
||||
}
|
||||
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
err = repo.LoadIndex(ctx, bar)
|
||||
err = repo.LoadIndex(ctx, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
err := examinePack(ctx, opts, repo, id)
|
||||
err := examinePack(ctx, opts, repo, id, printer)
|
||||
if err != nil {
|
||||
Warnf("error: %v\n", err)
|
||||
printer.E("error: %v", err)
|
||||
}
|
||||
if err == context.Canceled {
|
||||
break
|
||||
@@ -496,24 +490,24 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
|
||||
return nil
|
||||
}
|
||||
|
||||
func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID) error {
|
||||
Printf("examine %v\n", id)
|
||||
func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID, printer progress.Printer) error {
|
||||
printer.S("examine %v", id)
|
||||
|
||||
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
|
||||
// also process damaged pack files
|
||||
if buf == nil {
|
||||
return err
|
||||
}
|
||||
Printf(" file size is %v\n", len(buf))
|
||||
printer.S(" file size is %v", len(buf))
|
||||
gotID := restic.Hash(buf)
|
||||
if !id.Equal(gotID) {
|
||||
Printf(" wanted hash %v, got %v\n", id, gotID)
|
||||
printer.S(" wanted hash %v, got %v", id, gotID)
|
||||
} else {
|
||||
Printf(" hash for file content matches\n")
|
||||
printer.S(" hash for file content matches")
|
||||
}
|
||||
|
||||
Printf(" ========================================\n")
|
||||
Printf(" looking for info in the indexes\n")
|
||||
printer.S(" ========================================")
|
||||
printer.S(" looking for info in the indexes")
|
||||
|
||||
blobsLoaded := false
|
||||
// examine all data the indexes have for the pack file
|
||||
@@ -523,43 +517,41 @@ func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repo
|
||||
continue
|
||||
}
|
||||
|
||||
checkPackSize(blobs, len(buf))
|
||||
checkPackSize(blobs, len(buf), printer)
|
||||
|
||||
err = loadBlobs(ctx, opts, repo, id, blobs)
|
||||
err = loadBlobs(ctx, opts, repo, id, blobs, printer)
|
||||
if err != nil {
|
||||
Warnf("error: %v\n", err)
|
||||
printer.E("error: %v", err)
|
||||
} else {
|
||||
blobsLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
Printf(" ========================================\n")
|
||||
Printf(" inspect the pack itself\n")
|
||||
printer.S(" ========================================")
|
||||
printer.S(" inspect the pack itself")
|
||||
|
||||
blobs, _, err := repo.ListPack(ctx, id, int64(len(buf)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("pack %v: %v", id.Str(), err)
|
||||
}
|
||||
checkPackSize(blobs, len(buf))
|
||||
checkPackSize(blobs, len(buf), printer)
|
||||
|
||||
if !blobsLoaded {
|
||||
return loadBlobs(ctx, opts, repo, id, blobs)
|
||||
return loadBlobs(ctx, opts, repo, id, blobs, printer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPackSize(blobs []restic.Blob, fileSize int) {
|
||||
func checkPackSize(blobs restic.Blobs, fileSize int, printer progress.Printer) {
|
||||
// track current size and offset
|
||||
var size, offset uint64
|
||||
|
||||
sort.Slice(blobs, func(i, j int) bool {
|
||||
return blobs[i].Offset < blobs[j].Offset
|
||||
})
|
||||
blobs.Sort()
|
||||
|
||||
for _, pb := range blobs {
|
||||
Printf(" %v blob %v, offset %-6d, raw length %-6d\n", pb.Type, pb.ID, pb.Offset, pb.Length)
|
||||
printer.S(" %v blob %v, offset %-6d, raw length %-6d", pb.Type, pb.ID, pb.Offset, pb.Length)
|
||||
if offset != uint64(pb.Offset) {
|
||||
Printf(" hole in file, want offset %v, got %v\n", offset, pb.Offset)
|
||||
printer.S(" hole in file, want offset %v, got %v", offset, pb.Offset)
|
||||
}
|
||||
offset = uint64(pb.Offset + pb.Length)
|
||||
size += uint64(pb.Length)
|
||||
@@ -567,8 +559,8 @@ func checkPackSize(blobs []restic.Blob, fileSize int) {
|
||||
size += uint64(pack.CalculateHeaderSize(blobs))
|
||||
|
||||
if uint64(fileSize) != size {
|
||||
Printf(" file sizes do not match: computed %v, file size is %v\n", size, fileSize)
|
||||
printer.S(" file sizes do not match: computed %v, file size is %v", size, fileSize)
|
||||
} else {
|
||||
Printf(" file sizes match\n")
|
||||
printer.S(" file sizes match")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func registerDebugCommand(_ *cobra.Command) {
|
||||
func registerDebugCommand(_ *cobra.Command, _ *global.Options) {
|
||||
// No commands to register in non-debug mode
|
||||
}
|
||||
|
||||
+102
-106
@@ -5,17 +5,18 @@ import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newDiffCommand() *cobra.Command {
|
||||
func newDiffCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts DiffOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -38,7 +39,7 @@ Metadata comparison will likely not work if a backup was created using the
|
||||
|
||||
To only compare files in specific subfolders, you can use the
|
||||
"snapshotID:subfolder" syntax, where "subfolder" is a path within the
|
||||
snapshot.
|
||||
snapshot tree as shown by "restic ls".
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -52,7 +53,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDiff(cmd.Context(), opts, globalOptions, args)
|
||||
return runDiff(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -69,10 +70,10 @@ func (opts *DiffOptions) AddFlags(f *pflag.FlagSet) {
|
||||
f.BoolVar(&opts.ShowMetadata, "metadata", false, "print changes in metadata")
|
||||
}
|
||||
|
||||
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*restic.Snapshot, string, error) {
|
||||
sn, subfolder, err := restic.FindSnapshot(ctx, be, repo, desc)
|
||||
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*data.Snapshot, string, error) {
|
||||
sn, subfolder, err := data.FindSnapshot(ctx, be, repo, desc)
|
||||
if err != nil {
|
||||
return nil, "", errors.Fatal(err.Error())
|
||||
return nil, "", errors.Fatalf("%s", err)
|
||||
}
|
||||
return sn, subfolder, err
|
||||
}
|
||||
@@ -82,6 +83,7 @@ type Comparer struct {
|
||||
repo restic.BlobLoader
|
||||
opts DiffOptions
|
||||
printChange func(change *Change)
|
||||
printError func(string, ...interface{})
|
||||
}
|
||||
|
||||
type Change struct {
|
||||
@@ -105,15 +107,15 @@ type DiffStat struct {
|
||||
}
|
||||
|
||||
// Add adds stats information for node to s.
|
||||
func (s *DiffStat) Add(node *restic.Node) {
|
||||
func (s *DiffStat) Add(node *data.Node) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch node.Type {
|
||||
case restic.NodeTypeFile:
|
||||
case data.NodeTypeFile:
|
||||
s.Files++
|
||||
case restic.NodeTypeDir:
|
||||
case data.NodeTypeDir:
|
||||
s.Dirs++
|
||||
default:
|
||||
s.Others++
|
||||
@@ -121,13 +123,13 @@ func (s *DiffStat) Add(node *restic.Node) {
|
||||
}
|
||||
|
||||
// addBlobs adds the blobs of node to s.
|
||||
func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
||||
func addBlobs(bs restic.AssociatedBlobSet, node *data.Node) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch node.Type {
|
||||
case restic.NodeTypeFile:
|
||||
case data.NodeTypeFile:
|
||||
for _, blob := range node.Content {
|
||||
h := restic.BlobHandle{
|
||||
ID: blob,
|
||||
@@ -135,7 +137,7 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
||||
}
|
||||
bs.Insert(h)
|
||||
}
|
||||
case restic.NodeTypeDir:
|
||||
case data.NodeTypeDir:
|
||||
h := restic.BlobHandle{
|
||||
ID: *node.Subtree,
|
||||
Type: restic.TreeBlob,
|
||||
@@ -145,18 +147,18 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
||||
}
|
||||
|
||||
type DiffStatsContainer struct {
|
||||
MessageType string `json:"message_type"` // "statistics"
|
||||
SourceSnapshot string `json:"source_snapshot"`
|
||||
TargetSnapshot string `json:"target_snapshot"`
|
||||
ChangedFiles int `json:"changed_files"`
|
||||
Added DiffStat `json:"added"`
|
||||
Removed DiffStat `json:"removed"`
|
||||
BlobsBefore, BlobsAfter, BlobsCommon restic.BlobSet `json:"-"`
|
||||
MessageType string `json:"message_type"` // "statistics"
|
||||
SourceSnapshot string `json:"source_snapshot"`
|
||||
TargetSnapshot string `json:"target_snapshot"`
|
||||
ChangedFiles int `json:"changed_files"`
|
||||
Added DiffStat `json:"added"`
|
||||
Removed DiffStat `json:"removed"`
|
||||
BlobsBefore, BlobsAfter, BlobsCommon restic.AssociatedBlobSet `json:"-"`
|
||||
}
|
||||
|
||||
// updateBlobs updates the blob counters in the stats struct.
|
||||
func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) {
|
||||
for h := range blobs {
|
||||
func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *DiffStat, printError func(string, ...interface{})) {
|
||||
for h := range blobs.Keys() {
|
||||
switch h.Type {
|
||||
case restic.DataBlob:
|
||||
stats.DataBlobs++
|
||||
@@ -166,7 +168,7 @@ func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) {
|
||||
|
||||
size, found := repo.LookupBlobSize(h.Type, h.ID)
|
||||
if !found {
|
||||
Warnf("unable to find blob size for %v\n", h)
|
||||
printError("unable to find blob size for %v", h)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -174,30 +176,33 @@ func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, blobs restic.BlobSet, prefix string, id restic.ID) error {
|
||||
func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, blobs restic.AssociatedBlobSet, prefix string, id restic.ID) error {
|
||||
debug.Log("print %v tree %v", mode, id)
|
||||
tree, err := restic.LoadTree(ctx, c.repo, id)
|
||||
tree, err := data.LoadTree(ctx, c.repo, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range tree.Nodes {
|
||||
for item := range tree {
|
||||
if item.Error != nil {
|
||||
return item.Error
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
node := item.Node
|
||||
name := path.Join(prefix, node.Name)
|
||||
if node.Type == restic.NodeTypeDir {
|
||||
if node.Type == data.NodeTypeDir {
|
||||
name += "/"
|
||||
}
|
||||
c.printChange(NewChange(name, mode))
|
||||
stats.Add(node)
|
||||
addBlobs(blobs, node)
|
||||
|
||||
if node.Type == restic.NodeTypeDir {
|
||||
if node.Type == data.NodeTypeDir {
|
||||
err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree)
|
||||
if err != nil && err != context.Canceled {
|
||||
Warnf("error: %v\n", err)
|
||||
c.printError("error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,24 +210,28 @@ func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, b
|
||||
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.AssociatedBlobSet, id restic.ID) error {
|
||||
debug.Log("print tree %v", id)
|
||||
tree, err := restic.LoadTree(ctx, c.repo, id)
|
||||
tree, err := data.LoadTree(ctx, c.repo, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, node := range tree.Nodes {
|
||||
for item := range tree {
|
||||
if item.Error != nil {
|
||||
return item.Error
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
node := item.Node
|
||||
addBlobs(blobs, node)
|
||||
|
||||
if node.Type == restic.NodeTypeDir {
|
||||
if node.Type == data.NodeTypeDir {
|
||||
err := c.collectDir(ctx, blobs, *node.Subtree)
|
||||
if err != nil && err != context.Canceled {
|
||||
Warnf("error: %v\n", err)
|
||||
c.printError("error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,56 +239,41 @@ func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id rest
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func uniqueNodeNames(tree1, tree2 *restic.Tree) (tree1Nodes, tree2Nodes map[string]*restic.Node, uniqueNames []string) {
|
||||
names := make(map[string]struct{})
|
||||
tree1Nodes = make(map[string]*restic.Node)
|
||||
for _, node := range tree1.Nodes {
|
||||
tree1Nodes[node.Name] = node
|
||||
names[node.Name] = struct{}{}
|
||||
}
|
||||
|
||||
tree2Nodes = make(map[string]*restic.Node)
|
||||
for _, node := range tree2.Nodes {
|
||||
tree2Nodes[node.Name] = node
|
||||
names[node.Name] = struct{}{}
|
||||
}
|
||||
|
||||
uniqueNames = make([]string, 0, len(names))
|
||||
for name := range names {
|
||||
uniqueNames = append(uniqueNames, name)
|
||||
}
|
||||
|
||||
sort.Strings(uniqueNames)
|
||||
return tree1Nodes, tree2Nodes, uniqueNames
|
||||
}
|
||||
|
||||
func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, prefix string, id1, id2 restic.ID) error {
|
||||
debug.Log("diffing %v to %v", id1, id2)
|
||||
tree1, err := restic.LoadTree(ctx, c.repo, id1)
|
||||
tree1, err := data.LoadTree(ctx, c.repo, id1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tree2, err := restic.LoadTree(ctx, c.repo, id2)
|
||||
tree2, err := data.LoadTree(ctx, c.repo, id2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tree1Nodes, tree2Nodes, names := uniqueNodeNames(tree1, tree2)
|
||||
|
||||
for _, name := range names {
|
||||
for dt := range data.DualTreeIterator(tree1, tree2) {
|
||||
if dt.Error != nil {
|
||||
return dt.Error
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
node1, t1 := tree1Nodes[name]
|
||||
node2, t2 := tree2Nodes[name]
|
||||
node1 := dt.Tree1
|
||||
node2 := dt.Tree2
|
||||
|
||||
var name string
|
||||
if node1 != nil {
|
||||
name = node1.Name
|
||||
} else {
|
||||
name = node2.Name
|
||||
}
|
||||
|
||||
addBlobs(stats.BlobsBefore, node1)
|
||||
addBlobs(stats.BlobsAfter, node2)
|
||||
|
||||
switch {
|
||||
case t1 && t2:
|
||||
case node1 != nil && node2 != nil:
|
||||
name := path.Join(prefix, name)
|
||||
mod := ""
|
||||
|
||||
@@ -287,12 +281,12 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
||||
mod += "T"
|
||||
}
|
||||
|
||||
if node2.Type == restic.NodeTypeDir {
|
||||
if node2.Type == data.NodeTypeDir {
|
||||
name += "/"
|
||||
}
|
||||
|
||||
if node1.Type == restic.NodeTypeFile &&
|
||||
node2.Type == restic.NodeTypeFile &&
|
||||
if node1.Type == data.NodeTypeFile &&
|
||||
node2.Type == data.NodeTypeFile &&
|
||||
!reflect.DeepEqual(node1.Content, node2.Content) {
|
||||
mod += "M"
|
||||
stats.ChangedFiles++
|
||||
@@ -314,7 +308,7 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
||||
c.printChange(NewChange(name, mod))
|
||||
}
|
||||
|
||||
if node1.Type == restic.NodeTypeDir && node2.Type == restic.NodeTypeDir {
|
||||
if node1.Type == data.NodeTypeDir && node2.Type == data.NodeTypeDir {
|
||||
var err error
|
||||
if (*node1.Subtree).Equal(*node2.Subtree) {
|
||||
err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree)
|
||||
@@ -322,35 +316,35 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
||||
err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
||||
}
|
||||
if err != nil && err != context.Canceled {
|
||||
Warnf("error: %v\n", err)
|
||||
c.printError("error: %v", err)
|
||||
}
|
||||
}
|
||||
case t1 && !t2:
|
||||
case node1 != nil && node2 == nil:
|
||||
prefix := path.Join(prefix, name)
|
||||
if node1.Type == restic.NodeTypeDir {
|
||||
if node1.Type == data.NodeTypeDir {
|
||||
prefix += "/"
|
||||
}
|
||||
c.printChange(NewChange(prefix, "-"))
|
||||
stats.Removed.Add(node1)
|
||||
|
||||
if node1.Type == restic.NodeTypeDir {
|
||||
if node1.Type == data.NodeTypeDir {
|
||||
err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree)
|
||||
if err != nil && err != context.Canceled {
|
||||
Warnf("error: %v\n", err)
|
||||
c.printError("error: %v", err)
|
||||
}
|
||||
}
|
||||
case !t1 && t2:
|
||||
case node1 == nil && node2 != nil:
|
||||
prefix := path.Join(prefix, name)
|
||||
if node2.Type == restic.NodeTypeDir {
|
||||
if node2.Type == data.NodeTypeDir {
|
||||
prefix += "/"
|
||||
}
|
||||
c.printChange(NewChange(prefix, "+"))
|
||||
stats.Added.Add(node2)
|
||||
|
||||
if node2.Type == restic.NodeTypeDir {
|
||||
if node2.Type == data.NodeTypeDir {
|
||||
err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree)
|
||||
if err != nil && err != context.Canceled {
|
||||
Warnf("error: %v\n", err)
|
||||
c.printError("error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,12 +353,14 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []string) error {
|
||||
func runDiff(ctx context.Context, opts DiffOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
if len(args) != 2 {
|
||||
return errors.Fatalf("specify two snapshot IDs")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -386,10 +382,9 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
||||
}
|
||||
|
||||
if !gopts.JSON {
|
||||
Verbosef("comparing snapshot %v to %v:\n\n", sn1.ID().Str(), sn2.ID().Str())
|
||||
printer.P("comparing snapshot %v to %v:\n\n", sn1.ID().Str(), sn2.ID().Str())
|
||||
}
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
if err = repo.LoadIndex(ctx, bar); err != nil {
|
||||
if err = repo.LoadIndex(ctx, printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -401,30 +396,31 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
||||
return errors.Errorf("snapshot %v has nil tree", sn2.ID().Str())
|
||||
}
|
||||
|
||||
sn1.Tree, err = restic.FindTreeDirectory(ctx, repo, sn1.Tree, subfolder1)
|
||||
sn1.Tree, err = data.FindTreeDirectory(ctx, repo, sn1.Tree, subfolder1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sn2.Tree, err = restic.FindTreeDirectory(ctx, repo, sn2.Tree, subfolder2)
|
||||
sn2.Tree, err = data.FindTreeDirectory(ctx, repo, sn2.Tree, subfolder2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := &Comparer{
|
||||
repo: repo,
|
||||
opts: opts,
|
||||
repo: repo,
|
||||
opts: opts,
|
||||
printError: printer.E,
|
||||
printChange: func(change *Change) {
|
||||
Printf("%-5s%v\n", change.Modifier, change.Path)
|
||||
printer.S("%-5s%v", change.Modifier, change.Path)
|
||||
},
|
||||
}
|
||||
|
||||
if gopts.JSON {
|
||||
enc := json.NewEncoder(globalOptions.stdout)
|
||||
enc := json.NewEncoder(gopts.Term.OutputWriter())
|
||||
c.printChange = func(change *Change) {
|
||||
err := enc.Encode(change)
|
||||
if err != nil {
|
||||
Warnf("JSON encode failed: %v\n", err)
|
||||
printer.E("JSON encode failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,9 +433,9 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
||||
MessageType: "statistics",
|
||||
SourceSnapshot: args[0],
|
||||
TargetSnapshot: args[1],
|
||||
BlobsBefore: restic.NewBlobSet(),
|
||||
BlobsAfter: restic.NewBlobSet(),
|
||||
BlobsCommon: restic.NewBlobSet(),
|
||||
BlobsBefore: repo.NewAssociatedBlobSet(),
|
||||
BlobsAfter: repo.NewAssociatedBlobSet(),
|
||||
BlobsCommon: repo.NewAssociatedBlobSet(),
|
||||
}
|
||||
stats.BlobsBefore.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn1.Tree})
|
||||
stats.BlobsAfter.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn2.Tree})
|
||||
@@ -450,23 +446,23 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
||||
}
|
||||
|
||||
both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
|
||||
updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed)
|
||||
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added)
|
||||
updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed, printer.E)
|
||||
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added, printer.E)
|
||||
|
||||
if gopts.JSON {
|
||||
err := json.NewEncoder(globalOptions.stdout).Encode(stats)
|
||||
err := json.NewEncoder(gopts.Term.OutputWriter()).Encode(stats)
|
||||
if err != nil {
|
||||
Warnf("JSON encode failed: %v\n", err)
|
||||
printer.E("JSON encode failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
Printf("\n")
|
||||
Printf("Files: %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
|
||||
Printf("Dirs: %5d new, %5d removed\n", stats.Added.Dirs, stats.Removed.Dirs)
|
||||
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("Tree Blobs: %5d new, %5d removed\n", stats.Added.TreeBlobs, stats.Removed.TreeBlobs)
|
||||
Printf(" Added: %-5s\n", ui.FormatBytes(stats.Added.Bytes))
|
||||
Printf(" Removed: %-5s\n", ui.FormatBytes(stats.Removed.Bytes))
|
||||
printer.S("")
|
||||
printer.S("Files: %5d new, %5d removed, %5d changed", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
|
||||
printer.S("Dirs: %5d new, %5d removed", stats.Added.Dirs, stats.Removed.Dirs)
|
||||
printer.S("Others: %5d new, %5d removed", stats.Added.Others, stats.Removed.Others)
|
||||
printer.S("Data Blobs: %5d new, %5d removed", stats.Added.DataBlobs, stats.Removed.DataBlobs)
|
||||
printer.S("Tree Blobs: %5d new, %5d removed", stats.Added.TreeBlobs, stats.Removed.TreeBlobs)
|
||||
printer.S(" Added: %-5s", ui.FormatBytes(stats.Added.Bytes))
|
||||
printer.S(" Removed: %-5s", ui.FormatBytes(stats.Removed.Bytes))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -11,15 +11,16 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func testRunDiffOutput(gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) {
|
||||
buf, err := withCaptureStdout(func() error {
|
||||
func testRunDiffOutput(t testing.TB, gopts global.Options, firstSnapshotID string, secondSnapshotID string) (string, error) {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
opts := DiffOptions{
|
||||
ShowMetadata: false,
|
||||
}
|
||||
return runDiff(context.TODO(), opts, gopts, []string{firstSnapshotID, secondSnapshotID})
|
||||
return runDiff(ctx, opts, gopts, []string{firstSnapshotID, secondSnapshotID}, gopts.Term)
|
||||
})
|
||||
return buf.String(), err
|
||||
}
|
||||
@@ -123,10 +124,10 @@ func TestDiff(t *testing.T) {
|
||||
|
||||
// quiet suppresses the diff output except for the summary
|
||||
env.gopts.Quiet = false
|
||||
_, err := testRunDiffOutput(env.gopts, "", secondSnapshotID)
|
||||
_, err := testRunDiffOutput(t, env.gopts, "", secondSnapshotID)
|
||||
rtest.Assert(t, err != nil, "expected error on invalid snapshot id")
|
||||
|
||||
out, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
|
||||
out, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
|
||||
rtest.OK(t, err)
|
||||
|
||||
for _, pattern := range diffOutputRegexPatterns {
|
||||
@@ -137,7 +138,7 @@ func TestDiff(t *testing.T) {
|
||||
|
||||
// check quiet output
|
||||
env.gopts.Quiet = true
|
||||
outQuiet, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
|
||||
outQuiet, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
|
||||
rtest.OK(t, err)
|
||||
|
||||
rtest.Assert(t, len(outQuiet) < len(out), "expected shorter output on quiet mode %v vs. %v", len(outQuiet), len(out))
|
||||
@@ -154,7 +155,7 @@ func TestDiffJSON(t *testing.T) {
|
||||
// quiet suppresses the diff output except for the summary
|
||||
env.gopts.Quiet = false
|
||||
env.gopts.JSON = true
|
||||
out, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
|
||||
out, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
|
||||
rtest.OK(t, err)
|
||||
|
||||
var stat DiffStatsContainer
|
||||
@@ -181,7 +182,7 @@ func TestDiffJSON(t *testing.T) {
|
||||
|
||||
// check quiet output
|
||||
env.gopts.Quiet = true
|
||||
outQuiet, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
|
||||
outQuiet, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
|
||||
rtest.OK(t, err)
|
||||
|
||||
stat = DiffStatsContainer{}
|
||||
|
||||
+35
-31
@@ -7,20 +7,23 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/dump"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newDumpCommand() *cobra.Command {
|
||||
func newDumpCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts DumpOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "dump [flags] snapshotID file",
|
||||
Short: "Print a backed-up file to stdout",
|
||||
Short: "Print backed-up files or folders to stdout",
|
||||
Long: `
|
||||
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
|
||||
@@ -32,7 +35,7 @@ repository.
|
||||
|
||||
To include the folder content at the root of the archive, you can use the
|
||||
"snapshotID:subfolder" syntax, where "subfolder" is a path within the
|
||||
snapshot.
|
||||
snapshot tree as shown by "restic ls".
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -46,7 +49,8 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runDump(cmd.Context(), opts, globalOptions, args)
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runDump(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -56,7 +60,7 @@ Exit status is 12 if the password is incorrect.
|
||||
|
||||
// DumpOptions collects all options for the dump command.
|
||||
type DumpOptions struct {
|
||||
restic.SnapshotFilter
|
||||
data.SnapshotFilter
|
||||
Archive string
|
||||
Target string
|
||||
}
|
||||
@@ -76,7 +80,7 @@ func splitPath(p string) []string {
|
||||
return append(s, f)
|
||||
}
|
||||
|
||||
func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoader, prefix string, pathComponents []string, d *dump.Dumper, canWriteArchiveFunc func() error) error {
|
||||
func printFromTree(ctx context.Context, tree data.TreeNodeIterator, repo restic.BlobLoader, prefix string, pathComponents []string, d *dump.Dumper, canWriteArchiveFunc func() error) error {
|
||||
// If we print / we need to assume that there are multiple nodes at that
|
||||
// level in the tree.
|
||||
if pathComponents[0] == "" {
|
||||
@@ -88,35 +92,38 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
|
||||
|
||||
item := filepath.Join(prefix, pathComponents[0])
|
||||
l := len(pathComponents)
|
||||
for _, node := range tree.Nodes {
|
||||
for it := range tree {
|
||||
if it.Error != nil {
|
||||
return it.Error
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
node := it.Node
|
||||
// If dumping something in the highest level it will just take the
|
||||
// first item it finds and dump that according to the switch case below.
|
||||
if node.Name == pathComponents[0] {
|
||||
switch {
|
||||
case l == 1 && node.Type == restic.NodeTypeFile:
|
||||
case l == 1 && node.Type == data.NodeTypeFile:
|
||||
return d.WriteNode(ctx, node)
|
||||
case l > 1 && node.Type == restic.NodeTypeDir:
|
||||
subtree, err := restic.LoadTree(ctx, repo, *node.Subtree)
|
||||
case l > 1 && node.Type == data.NodeTypeDir:
|
||||
subtree, err := data.LoadTree(ctx, repo, *node.Subtree)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
||||
}
|
||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], d, canWriteArchiveFunc)
|
||||
case node.Type == restic.NodeTypeDir:
|
||||
case node.Type == data.NodeTypeDir:
|
||||
if err := canWriteArchiveFunc(); err != nil {
|
||||
return err
|
||||
}
|
||||
subtree, err := restic.LoadTree(ctx, repo, *node.Subtree)
|
||||
subtree, err := data.LoadTree(ctx, repo, *node.Subtree)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.DumpTree(ctx, subtree, item)
|
||||
case l > 1:
|
||||
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
||||
case node.Type != restic.NodeTypeFile:
|
||||
case node.Type != data.NodeTypeFile:
|
||||
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
||||
}
|
||||
}
|
||||
@@ -124,11 +131,13 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
|
||||
return fmt.Errorf("path %q not found in snapshot", item)
|
||||
}
|
||||
|
||||
func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []string) error {
|
||||
func runDump(ctx context.Context, opts DumpOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
if len(args) != 2 {
|
||||
return errors.Fatal("no file and no snapshot ID specified")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
switch opts.Archive {
|
||||
case "tar", "zip":
|
||||
default:
|
||||
@@ -142,39 +151,34 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
|
||||
|
||||
splittedPath := splitPath(path.Clean(pathToPrint))
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
sn, subfolder, err := (&restic.SnapshotFilter{
|
||||
Hosts: opts.Hosts,
|
||||
Paths: opts.Paths,
|
||||
Tags: opts.Tags,
|
||||
}).FindLatest(ctx, repo, repo, snapshotIDString)
|
||||
sn, subfolder, err := opts.SnapshotFilter.FindLatest(ctx, repo, repo, snapshotIDString)
|
||||
if err != nil {
|
||||
return errors.Fatalf("failed to find snapshot: %v", err)
|
||||
}
|
||||
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
err = repo.LoadIndex(ctx, bar)
|
||||
err = repo.LoadIndex(ctx, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
|
||||
sn.Tree, err = data.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tree, err := restic.LoadTree(ctx, repo, *sn.Tree)
|
||||
tree, err := data.LoadTree(ctx, repo, *sn.Tree)
|
||||
if err != nil {
|
||||
return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err)
|
||||
}
|
||||
|
||||
outputFileWriter := os.Stdout
|
||||
canWriteArchiveFunc := checkStdoutArchive
|
||||
outputFileWriter := term.OutputRaw()
|
||||
canWriteArchiveFunc := checkStdoutArchive(term)
|
||||
|
||||
if opts.Target != "" {
|
||||
file, err := os.Create(opts.Target)
|
||||
@@ -198,9 +202,9 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkStdoutArchive() error {
|
||||
if stdoutIsTerminal() {
|
||||
return fmt.Errorf("stdout is the terminal, please redirect output")
|
||||
func checkStdoutArchive(term ui.Terminal) func() error {
|
||||
if term.OutputIsTerminal() {
|
||||
return func() error { return fmt.Errorf("stdout is the terminal, please redirect output") }
|
||||
}
|
||||
return nil
|
||||
return func() error { return nil }
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/feature"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newFeaturesCommand() *cobra.Command {
|
||||
func newFeaturesCommand(globalOptions *global.Options) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "features",
|
||||
Short: "Print list of feature flags",
|
||||
@@ -39,7 +38,7 @@ Exit status is 1 if there was any error.
|
||||
return errors.Fatal("the feature command expects no arguments")
|
||||
}
|
||||
|
||||
fmt.Printf("All Feature Flags:\n")
|
||||
globalOptions.Term.Print("All Feature Flags:\n")
|
||||
flags := feature.Flag.List()
|
||||
|
||||
tab := table.New()
|
||||
@@ -51,7 +50,7 @@ Exit status is 1 if there was any error.
|
||||
for _, flag := range flags {
|
||||
tab.AddRow(flag)
|
||||
}
|
||||
return tab.Write(globalOptions.stdout)
|
||||
return tab.Write(globalOptions.Term.OutputWriter())
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+106
-66
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -10,14 +12,20 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/debug"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/filter"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
)
|
||||
|
||||
func newFindCommand() *cobra.Command {
|
||||
// errFindDone is returned from the tree walk when all requested tree IDs were found.
|
||||
var errFindDone = errors.New("find: all tree IDs found")
|
||||
|
||||
func newFindCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts FindOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -25,16 +33,11 @@ func newFindCommand() *cobra.Command {
|
||||
Short: "Find a file, a directory or restic IDs",
|
||||
Long: `
|
||||
The "find" command searches for files or directories in snapshots stored in the
|
||||
repo.
|
||||
It can also be used to search for restic blobs or trees for troubleshooting.
|
||||
repository. It can also be used to search for restic blobs, trees or pack
|
||||
files for troubleshooting.
|
||||
|
||||
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 --blob 420f620f b46ebe8a ddd38656
|
||||
restic find --show-pack-id --blob 420f620f
|
||||
restic find --tree 577c2bc9 f81f2e22 a62827a9
|
||||
restic find --pack 025c1d06
|
||||
output from oldest to youngest specify --reverse.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -45,10 +48,17 @@ Exit status is 10 if the repository does not exist.
|
||||
Exit status is 11 if the repository is already locked.
|
||||
Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
Example: `restic find config.json
|
||||
restic find --json "*.yml" "*.json"
|
||||
restic find --json --blob 420f620f b46ebe8a ddd38656
|
||||
restic find --show-pack-id --blob 420f620f
|
||||
restic find --tree 577c2bc9 f81f2e22 a62827a9
|
||||
restic find --pack 025c1d06`,
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runFind(cmd.Context(), opts, globalOptions, args)
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runFind(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -67,7 +77,7 @@ type FindOptions struct {
|
||||
ListLong bool
|
||||
HumanReadable bool
|
||||
Reverse bool
|
||||
restic.SnapshotFilter
|
||||
data.SnapshotFilter
|
||||
}
|
||||
|
||||
func (opts *FindOptions) AddFlags(f *pflag.FlagSet) {
|
||||
@@ -121,13 +131,19 @@ type statefulOutput struct {
|
||||
HumanReadable bool
|
||||
JSON bool
|
||||
inuse bool
|
||||
newsn *restic.Snapshot
|
||||
oldsn *restic.Snapshot
|
||||
newsn *data.Snapshot
|
||||
oldsn *data.Snapshot
|
||||
hits int
|
||||
printer interface {
|
||||
S(string, ...interface{})
|
||||
P(string, ...interface{})
|
||||
E(string, ...interface{})
|
||||
}
|
||||
stdout io.Writer
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
||||
type findNode restic.Node
|
||||
func (s *statefulOutput) PrintPatternJSON(path string, node *data.Node) {
|
||||
type findNode data.Node
|
||||
b, err := json.Marshal(struct {
|
||||
// Add these attributes
|
||||
Path string `json:"path,omitempty"`
|
||||
@@ -148,40 +164,40 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
||||
findNode: (*findNode)(node),
|
||||
})
|
||||
if err != nil {
|
||||
Warnf("Marshall failed: %v\n", err)
|
||||
s.printer.E("Marshal failed: %v", err)
|
||||
return
|
||||
}
|
||||
if !s.inuse {
|
||||
Printf("[")
|
||||
_, _ = s.stdout.Write([]byte("["))
|
||||
s.inuse = true
|
||||
}
|
||||
if s.newsn != s.oldsn {
|
||||
if s.oldsn != nil {
|
||||
Printf("],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID())
|
||||
_, _ = fmt.Fprintf(s.stdout, "],\"hits\":%d,\"snapshot\":%q},", s.hits, s.oldsn.ID())
|
||||
}
|
||||
Printf(`{"matches":[`)
|
||||
_, _ = s.stdout.Write([]byte(`{"matches":[`))
|
||||
s.oldsn = s.newsn
|
||||
s.hits = 0
|
||||
}
|
||||
if s.hits > 0 {
|
||||
Printf(",")
|
||||
_, _ = s.stdout.Write([]byte(","))
|
||||
}
|
||||
Print(string(b))
|
||||
_, _ = s.stdout.Write(b)
|
||||
s.hits++
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintPatternNormal(path string, node *restic.Node) {
|
||||
func (s *statefulOutput) PrintPatternNormal(path string, node *data.Node) {
|
||||
if s.newsn != s.oldsn {
|
||||
if s.oldsn != nil {
|
||||
Verbosef("\n")
|
||||
s.printer.P("")
|
||||
}
|
||||
s.oldsn = s.newsn
|
||||
Verbosef("Found matching entries in snapshot %s from %s\n", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(TimeFormat))
|
||||
s.printer.P("Found matching entries in snapshot %s from %s", s.oldsn.ID().Str(), s.oldsn.Time.Local().Format(global.TimeFormat))
|
||||
}
|
||||
Println(formatNode(path, node, s.ListLong, s.HumanReadable))
|
||||
s.printer.S(formatNode(path, node, s.ListLong, s.HumanReadable))
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintPattern(path string, node *restic.Node) {
|
||||
func (s *statefulOutput) PrintPattern(path string, node *data.Node) {
|
||||
if s.JSON {
|
||||
s.PrintPatternJSON(path, node)
|
||||
} else {
|
||||
@@ -189,7 +205,7 @@ func (s *statefulOutput) PrintPattern(path string, node *restic.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
|
||||
func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *data.Snapshot) {
|
||||
b, err := json.Marshal(struct {
|
||||
// Add these attributes
|
||||
ObjectType string `json:"object_type"`
|
||||
@@ -207,32 +223,32 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *
|
||||
Time: sn.Time,
|
||||
})
|
||||
if err != nil {
|
||||
Warnf("Marshall failed: %v\n", err)
|
||||
s.printer.E("Marshal failed: %v", err)
|
||||
return
|
||||
}
|
||||
if !s.inuse {
|
||||
Printf("[")
|
||||
_, _ = s.stdout.Write([]byte("["))
|
||||
s.inuse = true
|
||||
}
|
||||
if s.hits > 0 {
|
||||
Printf(",")
|
||||
_, _ = s.stdout.Write([]byte(","))
|
||||
}
|
||||
Print(string(b))
|
||||
_, _ = s.stdout.Write(b)
|
||||
s.hits++
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
|
||||
Printf("Found %s %s\n", kind, id)
|
||||
func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *data.Snapshot) {
|
||||
s.printer.S("Found %s %s", kind, id)
|
||||
if kind == "blob" {
|
||||
Printf(" ... in file %s\n", nodepath)
|
||||
Printf(" (tree %s)\n", treeID)
|
||||
s.printer.S(" ... in file %s", nodepath)
|
||||
s.printer.S(" (tree %s)", treeID)
|
||||
} else {
|
||||
Printf(" ... path %s\n", nodepath)
|
||||
s.printer.S(" ... path %s", nodepath)
|
||||
}
|
||||
Printf(" ... in snapshot %s (%s)\n", sn.ID().Str(), sn.Time.Local().Format(TimeFormat))
|
||||
s.printer.S(" ... in snapshot %s (%s)", sn.ID().Str(), sn.Time.Local().Format(global.TimeFormat))
|
||||
}
|
||||
|
||||
func (s *statefulOutput) PrintObject(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
|
||||
func (s *statefulOutput) PrintObject(kind, id, nodepath, treeID string, sn *data.Snapshot) {
|
||||
if s.JSON {
|
||||
s.PrintObjectJSON(kind, id, nodepath, treeID, sn)
|
||||
} else {
|
||||
@@ -244,12 +260,12 @@ func (s *statefulOutput) Finish() {
|
||||
if s.JSON {
|
||||
// do some finishing up
|
||||
if s.oldsn != nil {
|
||||
Printf("],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID())
|
||||
_, _ = fmt.Fprintf(s.stdout, "],\"hits\":%d,\"snapshot\":%q}", s.hits, s.oldsn.ID())
|
||||
}
|
||||
if s.inuse {
|
||||
Printf("]\n")
|
||||
_, _ = s.stdout.Write([]byte("]\n"))
|
||||
} else {
|
||||
Printf("[]\n")
|
||||
_, _ = s.stdout.Write([]byte("[]\n"))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -263,9 +279,14 @@ type Finder struct {
|
||||
blobIDs map[string]struct{}
|
||||
treeIDs map[string]struct{}
|
||||
itemsFound int
|
||||
printer interface {
|
||||
S(string, ...interface{})
|
||||
P(string, ...interface{})
|
||||
E(string, ...interface{})
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error {
|
||||
func (f *Finder) findInSnapshot(ctx context.Context, sn *data.Snapshot) error {
|
||||
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), f.pat.oldest, f.pat.newest)
|
||||
|
||||
if sn.Tree == nil {
|
||||
@@ -273,11 +294,12 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
||||
}
|
||||
|
||||
f.out.newsn = sn
|
||||
return walker.Walk(ctx, f.repo, *sn.Tree, walker.WalkVisitor{ProcessNode: func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) error {
|
||||
return walker.Walk(ctx, f.repo, *sn.Tree, walker.WalkVisitor{ProcessNode: func(parentTreeID restic.ID, nodepath string, node *data.Node, err error) error {
|
||||
if err != nil {
|
||||
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
||||
|
||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s\n", parentTreeID, sn.ID())
|
||||
f.printer.S("Unable to load tree %s", parentTreeID)
|
||||
f.printer.S(" ... which belongs to snapshot %s", sn.ID())
|
||||
|
||||
return walker.ErrSkipNode
|
||||
}
|
||||
@@ -305,7 +327,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
||||
}
|
||||
|
||||
var errIfNoMatch error
|
||||
if node.Type == restic.NodeTypeDir {
|
||||
if node.Type == data.NodeTypeDir {
|
||||
var childMayMatch bool
|
||||
for _, pat := range f.pat.pattern {
|
||||
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
|
||||
@@ -357,13 +379,13 @@ func (f *Finder) findTree(treeID restic.ID, nodepath string) error {
|
||||
// looking for blobs)
|
||||
if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil {
|
||||
// Return an error to terminate the Walk
|
||||
return errors.New("OK")
|
||||
return errFindDone
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
||||
func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
|
||||
debug.Log("searching IDs in snapshot %s", sn.ID())
|
||||
|
||||
if sn.Tree == nil {
|
||||
@@ -371,11 +393,12 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
||||
}
|
||||
|
||||
f.out.newsn = sn
|
||||
return walker.Walk(ctx, f.repo, *sn.Tree, walker.WalkVisitor{ProcessNode: func(parentTreeID restic.ID, nodepath string, node *restic.Node, err error) error {
|
||||
return walker.Walk(ctx, f.repo, *sn.Tree, walker.WalkVisitor{ProcessNode: func(parentTreeID restic.ID, nodepath string, node *data.Node, err error) error {
|
||||
if err != nil {
|
||||
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
||||
|
||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s\n", parentTreeID, sn.ID())
|
||||
f.printer.S("Unable to load tree %s", parentTreeID)
|
||||
f.printer.S(" ... which belongs to snapshot %s", sn.ID())
|
||||
|
||||
return walker.ErrSkipNode
|
||||
}
|
||||
@@ -395,7 +418,7 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
||||
}
|
||||
}
|
||||
|
||||
if node.Type == restic.NodeTypeFile && f.blobIDs != nil {
|
||||
if node.Type == data.NodeTypeFile && f.blobIDs != nil {
|
||||
for _, id := range node.Content {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
@@ -431,6 +454,9 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
||||
if f.blobIDs == nil {
|
||||
f.blobIDs = make(map[string]struct{})
|
||||
}
|
||||
if f.treeIDs == nil {
|
||||
f.treeIDs = make(map[string]struct{})
|
||||
}
|
||||
|
||||
debug.Log("Looking for packs...")
|
||||
err := f.repo.List(ctx, restic.PackFile, func(id restic.ID, size int64) error {
|
||||
@@ -451,7 +477,14 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
||||
return err
|
||||
}
|
||||
for _, b := range blobs {
|
||||
f.blobIDs[b.ID.String()] = struct{}{}
|
||||
switch b.Type {
|
||||
case restic.DataBlob:
|
||||
f.blobIDs[b.ID.String()] = struct{}{}
|
||||
case restic.TreeBlob:
|
||||
f.treeIDs[b.ID.String()] = struct{}{}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type %v in blob list", b.Type.String()))
|
||||
}
|
||||
}
|
||||
// Stop searching when all packs have been found
|
||||
if len(packIDs) == 0 {
|
||||
@@ -524,7 +557,7 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
|
||||
for h := range indexPackIDs {
|
||||
list = append(list, h)
|
||||
}
|
||||
Warnf("some pack files are missing from the repository, getting their blobs from the repository index: %v\n\n", list)
|
||||
f.printer.E("some pack files are missing from the repository, getting their blobs from the repository index: %v\n\n", list)
|
||||
}
|
||||
return packIDs, nil
|
||||
}
|
||||
@@ -532,19 +565,20 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
|
||||
func (f *Finder) findObjectPack(id string, t restic.BlobType) {
|
||||
rid, err := restic.ParseID(id)
|
||||
if err != nil {
|
||||
Printf("Note: cannot find pack for object '%s', unable to parse ID: %v\n", id, err)
|
||||
f.printer.S("Note: cannot find pack for object '%s', unable to parse ID: %v", id, err)
|
||||
return
|
||||
}
|
||||
|
||||
blobs := f.repo.LookupBlob(t, rid)
|
||||
if len(blobs) == 0 {
|
||||
Printf("Object %s not found in the index\n", rid.Str())
|
||||
f.printer.S("Object %s with type %s not found in the index", t.String(), rid.Str())
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range blobs {
|
||||
if b.ID.Equal(rid) {
|
||||
Printf("Object belongs to pack %s\n ... Pack %s: %s\n", b.PackID, b.PackID.Str(), b.String())
|
||||
f.printer.S("Object belongs to pack %s", b.PackID)
|
||||
f.printer.S(" ... Pack %s: %s", b.PackID.Str(), b.String())
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -560,11 +594,13 @@ func (f *Finder) findObjectsPacks() {
|
||||
}
|
||||
}
|
||||
|
||||
func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []string) error {
|
||||
func runFind(ctx context.Context, opts FindOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
if len(args) == 0 {
|
||||
return errors.Fatal("wrong number of arguments")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
var err error
|
||||
pat := findPattern{pattern: args}
|
||||
if opts.CaseInsensitive {
|
||||
@@ -586,6 +622,10 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
||||
}
|
||||
}
|
||||
|
||||
if !pat.newest.IsZero() && !pat.oldest.IsZero() && pat.oldest.After(pat.newest) {
|
||||
return errors.Fatal("--oldest must specify a time before --newest")
|
||||
}
|
||||
|
||||
// Check at most only one kind of IDs is provided: currently we
|
||||
// can't mix types
|
||||
if (opts.BlobID && opts.TreeID) ||
|
||||
@@ -594,7 +634,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
||||
return errors.Fatal("cannot have several ID types")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -604,15 +644,15 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
if err = repo.LoadIndex(ctx, bar); err != nil {
|
||||
if err = repo.LoadIndex(ctx, printer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f := &Finder{
|
||||
repo: repo,
|
||||
pat: pat,
|
||||
out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON},
|
||||
repo: repo,
|
||||
pat: pat,
|
||||
out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON, printer: printer, stdout: term.OutputRaw()},
|
||||
printer: printer,
|
||||
}
|
||||
|
||||
if opts.BlobID {
|
||||
@@ -635,8 +675,8 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
||||
}
|
||||
}
|
||||
|
||||
var filteredSnapshots []*restic.Snapshot
|
||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots) {
|
||||
var filteredSnapshots []*data.Snapshot
|
||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots, printer) {
|
||||
filteredSnapshots = append(filteredSnapshots, sn)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
@@ -652,7 +692,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
||||
|
||||
for _, sn := range filteredSnapshots {
|
||||
if f.blobIDs != nil || f.treeIDs != nil {
|
||||
if err = f.findIDs(ctx, sn); err != nil && err.Error() != "OK" {
|
||||
if err = f.findIDs(ctx, sn); err != nil && !errors.Is(err, errFindDone) {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -3,18 +3,23 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
)
|
||||
|
||||
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts GlobalOptions, pattern string) []byte {
|
||||
buf, err := withCaptureStdout(func() error {
|
||||
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts global.Options, pattern string) []byte {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
gopts.JSON = wantJSON
|
||||
|
||||
return runFind(context.TODO(), opts, gopts, []string{pattern})
|
||||
return runFind(ctx, opts, gopts, []string{pattern}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
return buf.Bytes()
|
||||
@@ -95,7 +100,7 @@ func TestFindSorting(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
datafile := testSetupBackupData(t, env)
|
||||
testSetupBackupData(t, env)
|
||||
opts := BackupOptions{}
|
||||
|
||||
// first backup
|
||||
@@ -114,14 +119,14 @@ func TestFindSorting(t *testing.T) {
|
||||
// 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))
|
||||
rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", 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))
|
||||
rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines))
|
||||
matchesReverse := []testMatches{}
|
||||
rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse))
|
||||
|
||||
@@ -131,3 +136,141 @@ func TestFindSorting(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
|
||||
func TestFindInvalidTimeRange(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
err := runFind(context.TODO(), FindOptions{Oldest: "2026-01-01", Newest: "2020-01-01"}, env.gopts, []string{"quack"}, env.gopts.Term)
|
||||
rtest.Assert(t, err != nil && err.Error() == "Fatal: --oldest must specify a time before --newest",
|
||||
"unexpected error message: %v", err)
|
||||
}
|
||||
|
||||
// JsonOutput is the struct `restic find --json` produces
|
||||
type JSONOutput struct {
|
||||
ObjectType string `json:"object_type"`
|
||||
ID string `json:"id"`
|
||||
Path string `json:"path"`
|
||||
ParentTree string `json:"parent_tree,omitempty"`
|
||||
SnapshotID string `json:"snapshot"`
|
||||
Time time.Time `json:"time,omitempty"`
|
||||
}
|
||||
|
||||
func TestFindPackfile(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
|
||||
// backup
|
||||
backupPath := env.testdata + "/0/0/9"
|
||||
testRunBackup(t, "", []string{backupPath}, BackupOptions{}, env.gopts)
|
||||
sn1 := testListSnapshots(t, env.gopts, 1)[0]
|
||||
|
||||
// do all the testing wrapped inside withTermStatus()
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
// load master index
|
||||
rtest.OK(t, repo.LoadIndex(ctx, printer))
|
||||
|
||||
packID := restic.ID{}
|
||||
done := false
|
||||
err = repo.ListBlobs(ctx, func(pb restic.PackedBlob) {
|
||||
if !done && pb.Type == restic.TreeBlob {
|
||||
packID = pb.PackID
|
||||
done = true
|
||||
}
|
||||
})
|
||||
|
||||
rtest.OK(t, err)
|
||||
rtest.Assert(t, !packID.IsNull(), "expected a tree packfile ID")
|
||||
findOptions := FindOptions{PackID: true}
|
||||
results := testRunFind(t, true, findOptions, env.gopts, packID.String())
|
||||
|
||||
// get the json records
|
||||
jsonResult := []JSONOutput{}
|
||||
rtest.OK(t, json.Unmarshal(results, &jsonResult))
|
||||
rtest.Assert(t, len(jsonResult) > 0, "expected at least one tree record in the packfile")
|
||||
|
||||
// look at the last record
|
||||
lastIndex := len(jsonResult) - 1
|
||||
record := jsonResult[lastIndex]
|
||||
rtest.Assert(t, record.ObjectType == "tree" && record.SnapshotID == sn1.String(),
|
||||
"expected a tree record with known snapshot id, but got type=%s and snapID=%s instead of %s",
|
||||
record.ObjectType, record.SnapshotID, sn1.String())
|
||||
backupPath = filepath.ToSlash(backupPath)[2:] // take the offending drive mapping away
|
||||
rtest.Assert(t, strings.Contains(record.Path, backupPath), "expected %q as part of %q", backupPath, record.Path)
|
||||
|
||||
return nil
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
|
||||
func TestFindPackID(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
testSetupBackupData(t, env)
|
||||
|
||||
dir009 := filepath.Join(env.testdata, "0", "0", "9")
|
||||
dirEntries, err := os.ReadDir(dir009)
|
||||
rtest.OK(t, err)
|
||||
numberOfFiles := len(dirEntries)
|
||||
|
||||
// backup
|
||||
testRunBackup(t, "", []string{dir009}, BackupOptions{}, env.gopts)
|
||||
sn1 := testListSnapshots(t, env.gopts, 1)[0]
|
||||
|
||||
// extract packfile ID from repository index
|
||||
dataPackID := restic.ID{}
|
||||
treePackID := restic.ID{}
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
// load Index
|
||||
rtest.OK(t, repo.LoadIndex(ctx, nil))
|
||||
// go through all index entries and collect data and tree packfile(s)
|
||||
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
||||
switch blob.Type {
|
||||
case restic.DataBlob:
|
||||
dataPackID = blob.PackID
|
||||
case restic.TreeBlob:
|
||||
treePackID = blob.PackID
|
||||
}
|
||||
}))
|
||||
return nil
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
// look for data packfile
|
||||
rtest.Assert(t, !dataPackID.IsNull(), "expected to find data packfile in repo")
|
||||
packID := dataPackID.String()
|
||||
out := testRunFind(t, true, FindOptions{PackID: true}, env.gopts, packID)
|
||||
|
||||
findRes := []JSONOutput{}
|
||||
rtest.OK(t, json.Unmarshal(out, &findRes))
|
||||
rtest.Assert(t, len(findRes) == numberOfFiles, "expected %d entries for this packfile, got %d",
|
||||
numberOfFiles, len(findRes))
|
||||
|
||||
// look for tree packfile
|
||||
rtest.Assert(t, !treePackID.IsNull(), "expected to find tree packfile in repo")
|
||||
packID = treePackID.String()
|
||||
out = testRunFind(t, true, FindOptions{PackID: true}, env.gopts, packID)
|
||||
|
||||
findRes = []JSONOutput{}
|
||||
rtest.OK(t, json.Unmarshal(out, &findRes))
|
||||
record := findRes[len(findRes)-1]
|
||||
|
||||
rtest.Equals(t, record.ObjectType, "tree")
|
||||
rtest.Equals(t, record.SnapshotID, sn1.String())
|
||||
// windows path are messy, so we get rid of the messy bits at the start
|
||||
// exp: "/C/Users/RUNNER~1/AppData/Local/Temp/restic-test-2921201257/testdata/0/0/9"
|
||||
// got: "C:/Users/RUNNER~1/AppData/Local/Temp/restic-test-2921201257/testdata/0/0/9"
|
||||
rtest.Equals(t, filepath.ToSlash(record.Path)[2:], filepath.ToSlash(dir009)[2:])
|
||||
}
|
||||
|
||||
+47
-38
@@ -7,14 +7,16 @@ import (
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newForgetCommand() *cobra.Command {
|
||||
func newForgetCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts ForgetOptions
|
||||
var pruneOpts PruneOptions
|
||||
|
||||
@@ -41,6 +43,7 @@ EXIT STATUS
|
||||
|
||||
Exit status is 0 if the command was successful.
|
||||
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 11 if the repository is already locked.
|
||||
Exit status is 12 if the password is incorrect.
|
||||
@@ -48,9 +51,8 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
term, cancel := setupTermstatus()
|
||||
defer cancel()
|
||||
return runForget(cmd.Context(), opts, pruneOpts, globalOptions, term, args)
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runForget(cmd.Context(), opts, pruneOpts, *globalOptions, globalOptions.Term, args)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -62,6 +64,7 @@ Exit status is 12 if the password is incorrect.
|
||||
type ForgetPolicyCount int
|
||||
|
||||
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 {
|
||||
switch s {
|
||||
@@ -102,21 +105,21 @@ type ForgetOptions struct {
|
||||
Weekly ForgetPolicyCount
|
||||
Monthly ForgetPolicyCount
|
||||
Yearly ForgetPolicyCount
|
||||
Within restic.Duration
|
||||
WithinHourly restic.Duration
|
||||
WithinDaily restic.Duration
|
||||
WithinWeekly restic.Duration
|
||||
WithinMonthly restic.Duration
|
||||
WithinYearly restic.Duration
|
||||
KeepTags restic.TagLists
|
||||
Within data.Duration
|
||||
WithinHourly data.Duration
|
||||
WithinDaily data.Duration
|
||||
WithinWeekly data.Duration
|
||||
WithinMonthly data.Duration
|
||||
WithinYearly data.Duration
|
||||
KeepTags data.TagLists
|
||||
|
||||
UnsafeAllowRemoveAll bool
|
||||
|
||||
restic.SnapshotFilter
|
||||
data.SnapshotFilter
|
||||
Compact bool
|
||||
|
||||
// Grouping
|
||||
GroupBy restic.SnapshotGroupByOptions
|
||||
GroupBy data.SnapshotGroupByOptions
|
||||
DryRun bool
|
||||
Prune bool
|
||||
}
|
||||
@@ -147,7 +150,7 @@ func (opts *ForgetOptions) AddFlags(f *pflag.FlagSet) {
|
||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, false)
|
||||
|
||||
f.BoolVarP(&opts.Compact, "compact", "c", false, "use compact output format")
|
||||
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
||||
opts.GroupBy = data.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.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
|
||||
f.BoolVar(&opts.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
|
||||
@@ -161,7 +164,7 @@ func verifyForgetOptions(opts *ForgetOptions) error {
|
||||
return errors.Fatal("negative values other than -1 are not allowed for --keep-*")
|
||||
}
|
||||
|
||||
for _, d := range []restic.Duration{opts.Within, opts.WithinHourly, opts.WithinDaily,
|
||||
for _, d := range []data.Duration{opts.Within, opts.WithinHourly, opts.WithinDaily,
|
||||
opts.WithinMonthly, opts.WithinWeekly, opts.WithinYearly} {
|
||||
if d.Hours < 0 || d.Days < 0 || d.Months < 0 || d.Years < 0 {
|
||||
return errors.Fatal("durations containing negative values are not allowed for --keep-within*")
|
||||
@@ -171,7 +174,7 @@ func verifyForgetOptions(opts *ForgetOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error {
|
||||
func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOptions, gopts global.Options, term ui.Terminal, args []string) error {
|
||||
err := verifyForgetOptions(&opts)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -186,22 +189,17 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for forget command")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock)
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
verbosity := gopts.verbosity
|
||||
if gopts.JSON {
|
||||
verbosity = 0
|
||||
}
|
||||
printer := newTerminalProgressPrinter(verbosity, term)
|
||||
|
||||
var snapshots restic.Snapshots
|
||||
var snapshots data.Snapshots
|
||||
removeSnIDs := restic.NewIDSet()
|
||||
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) {
|
||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
|
||||
snapshots = append(snapshots, sn)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
@@ -216,12 +214,12 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
removeSnIDs.Insert(*sn.ID())
|
||||
}
|
||||
} else {
|
||||
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
||||
snapshotGroups, _, err := data.GroupSnapshots(snapshots, opts.GroupBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policy := restic.ExpirePolicy{
|
||||
policy := data.ExpirePolicy{
|
||||
Last: int(opts.Last),
|
||||
Hourly: int(opts.Hourly),
|
||||
Daily: int(opts.Daily),
|
||||
@@ -256,14 +254,14 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
}
|
||||
|
||||
if gopts.Verbose >= 1 && !gopts.JSON {
|
||||
err = PrintSnapshotGroupHeader(globalOptions.stdout, k)
|
||||
err = PrintSnapshotGroupHeader(gopts.Term.OutputWriter(), k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var key restic.SnapshotGroupKey
|
||||
if json.Unmarshal([]byte(k), &key) != nil {
|
||||
var key data.SnapshotGroupKey
|
||||
if err := json.Unmarshal([]byte(k), &key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -272,21 +270,25 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
fg.Host = key.Hostname
|
||||
fg.Paths = key.Paths
|
||||
|
||||
keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)
|
||||
keep, remove, reasons := data.ApplyPolicy(snapshotGroup, policy)
|
||||
|
||||
if !policy.Empty() && len(keep) == 0 {
|
||||
return fmt.Errorf("refusing to delete last snapshot of snapshot group \"%v\"", key.String())
|
||||
}
|
||||
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
|
||||
printer.P("keep %d snapshots:\n", len(keep))
|
||||
PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact)
|
||||
if err := PrintSnapshots(gopts.Term.OutputWriter(), keep, reasons, opts.Compact); err != nil {
|
||||
return err
|
||||
}
|
||||
printer.P("\n")
|
||||
}
|
||||
fg.Keep = asJSONSnapshots(keep)
|
||||
|
||||
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
|
||||
printer.P("remove %d snapshots:\n", len(remove))
|
||||
PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact)
|
||||
if err := PrintSnapshots(gopts.Term.OutputWriter(), remove, nil, opts.Compact); err != nil {
|
||||
return err
|
||||
}
|
||||
printer.P("\n")
|
||||
}
|
||||
fg.Remove = asJSONSnapshots(remove)
|
||||
@@ -305,12 +307,15 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// these are the snapshots that failed to be removed
|
||||
failedSnIDs := restic.NewIDSet()
|
||||
if len(removeSnIDs) > 0 {
|
||||
if !opts.DryRun {
|
||||
bar := printer.NewCounter("files deleted")
|
||||
err := restic.ParallelRemove(ctx, repo, removeSnIDs, restic.WriteableSnapshotFile, func(id restic.ID, err error) error {
|
||||
if err != nil {
|
||||
printer.E("unable to remove %v/%v from the repository\n", restic.SnapshotFile, id)
|
||||
failedSnIDs.Insert(id)
|
||||
} else {
|
||||
printer.VV("removed %v/%v\n", restic.SnapshotFile, id)
|
||||
}
|
||||
@@ -326,12 +331,16 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
}
|
||||
|
||||
if gopts.JSON && len(jsonGroups) > 0 {
|
||||
err = printJSONForget(globalOptions.stdout, jsonGroups)
|
||||
err = printJSONForget(gopts.Term.OutputWriter(), jsonGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedSnIDs) > 0 {
|
||||
return ErrFailedToRemoveOneOrMoreSnapshots
|
||||
}
|
||||
|
||||
if len(removeSnIDs) > 0 && opts.Prune {
|
||||
if opts.DryRun {
|
||||
printer.P("%d snapshots would be removed, running prune dry run\n", len(removeSnIDs))
|
||||
@@ -339,7 +348,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
||||
printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs))
|
||||
}
|
||||
pruneOptions.DryRun = opts.DryRun
|
||||
return runPruneWithRepo(ctx, pruneOptions, gopts, repo, removeSnIDs, term)
|
||||
return runPruneWithRepo(ctx, pruneOptions, repo, removeSnIDs, printer)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -355,7 +364,7 @@ type ForgetGroup struct {
|
||||
Reasons []KeepReason `json:"reasons"`
|
||||
}
|
||||
|
||||
func asJSONSnapshots(list restic.Snapshots) []Snapshot {
|
||||
func asJSONSnapshots(list data.Snapshots) []Snapshot {
|
||||
var resultList []Snapshot
|
||||
for _, sn := range list {
|
||||
k := Snapshot{
|
||||
@@ -374,7 +383,7 @@ type KeepReason struct {
|
||||
Matches []string `json:"matches"`
|
||||
}
|
||||
|
||||
func asJSONKeeps(list []restic.KeepReason) []KeepReason {
|
||||
func asJSONKeeps(list []data.KeepReason) []KeepReason {
|
||||
var resultList []KeepReason
|
||||
for _, keep := range list {
|
||||
k := KeepReason{
|
||||
|
||||
@@ -6,22 +6,22 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/global"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/termstatus"
|
||||
)
|
||||
|
||||
func testRunForgetMayFail(gopts GlobalOptions, opts ForgetOptions, args ...string) error {
|
||||
func testRunForgetMayFail(t testing.TB, gopts global.Options, opts ForgetOptions, args ...string) error {
|
||||
pruneOpts := PruneOptions{
|
||||
MaxUnused: "5%",
|
||||
}
|
||||
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
||||
return runForget(context.TODO(), opts, pruneOpts, gopts, term, args)
|
||||
return withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.Term, args)
|
||||
})
|
||||
}
|
||||
|
||||
func testRunForget(t testing.TB, gopts GlobalOptions, opts ForgetOptions, args ...string) {
|
||||
rtest.OK(t, testRunForgetMayFail(gopts, opts, args...))
|
||||
func testRunForget(t testing.TB, gopts global.Options, opts ForgetOptions, args ...string) {
|
||||
rtest.OK(t, testRunForgetMayFail(t, gopts, opts, args...))
|
||||
}
|
||||
|
||||
func TestRunForgetSafetyNet(t *testing.T) {
|
||||
@@ -38,27 +38,27 @@ func TestRunForgetSafetyNet(t *testing.T) {
|
||||
testListSnapshots(t, env.gopts, 2)
|
||||
|
||||
// --keep-tags invalid
|
||||
err := testRunForgetMayFail(env.gopts, ForgetOptions{
|
||||
KeepTags: restic.TagLists{restic.TagList{"invalid"}},
|
||||
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true},
|
||||
err := testRunForgetMayFail(t, env.gopts, ForgetOptions{
|
||||
KeepTags: data.TagLists{data.TagList{"invalid"}},
|
||||
GroupBy: data.SnapshotGroupByOptions{Host: true, Path: true},
|
||||
})
|
||||
rtest.Assert(t, strings.Contains(err.Error(), `refusing to delete last snapshot of snapshot group "host example, path`), "wrong error message got %v", err)
|
||||
|
||||
// disallow `forget --unsafe-allow-remove-all`
|
||||
err = testRunForgetMayFail(env.gopts, ForgetOptions{
|
||||
err = testRunForgetMayFail(t, env.gopts, ForgetOptions{
|
||||
UnsafeAllowRemoveAll: true,
|
||||
})
|
||||
rtest.Assert(t, strings.Contains(err.Error(), `--unsafe-allow-remove-all is not allowed unless a snapshot filter option is specified`), "wrong error message got %v", err)
|
||||
|
||||
// disallow `forget` without options
|
||||
err = testRunForgetMayFail(env.gopts, ForgetOptions{})
|
||||
err = testRunForgetMayFail(t, env.gopts, ForgetOptions{})
|
||||
rtest.Assert(t, strings.Contains(err.Error(), `no policy was specified, no snapshots will be removed`), "wrong error message got %v", err)
|
||||
|
||||
// `forget --host example --unsafe-allow-remove-all` should work
|
||||
testRunForget(t, env.gopts, ForgetOptions{
|
||||
UnsafeAllowRemoveAll: true,
|
||||
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true},
|
||||
SnapshotFilter: restic.SnapshotFilter{
|
||||
GroupBy: data.SnapshotGroupByOptions{Host: true, Path: true},
|
||||
SnapshotFilter: data.SnapshotFilter{
|
||||
Hosts: []string{opts.Host},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/data"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -69,18 +69,18 @@ func TestForgetOptionValues(t *testing.T) {
|
||||
{ForgetOptions{Weekly: -2}, negValErrorMsg},
|
||||
{ForgetOptions{Monthly: -2}, negValErrorMsg},
|
||||
{ForgetOptions{Yearly: -2}, negValErrorMsg},
|
||||
{ForgetOptions{Within: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
||||
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
||||
{ForgetOptions{Within: restic.ParseDurationOrPanic("-1y2m3d3h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y-2m3d3h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m-3d3h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d-3h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("-2y4m6d8h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y-4m6d8h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{Within: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||
{ForgetOptions{WithinHourly: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||
{ForgetOptions{WithinDaily: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||
{ForgetOptions{WithinWeekly: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||
{ForgetOptions{WithinMonthly: data.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
||||
{ForgetOptions{WithinYearly: data.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
||||
{ForgetOptions{Within: data.ParseDurationOrPanic("-1y2m3d3h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinHourly: data.ParseDurationOrPanic("1y-2m3d3h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinDaily: data.ParseDurationOrPanic("1y2m-3d3h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinWeekly: data.ParseDurationOrPanic("1y2m3d-3h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinMonthly: data.ParseDurationOrPanic("-2y4m6d8h")}, negDurationValErrorMsg},
|
||||
{ForgetOptions{WithinYearly: data.ParseDurationOrPanic("2y-4m6d8h")}, negDurationValErrorMsg},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@@ -96,7 +96,38 @@ 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)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "env default when flag not set",
|
||||
args: nil,
|
||||
want: []string{"testhost"},
|
||||
},
|
||||
{
|
||||
name: "flag overrides env",
|
||||
args: []string{"--host", "flaghost"},
|
||||
want: []string{"flaghost"},
|
||||
},
|
||||
{
|
||||
name: "empty flag clears env",
|
||||
args: []string{"--host", ""},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
set := pflag.NewFlagSet(tt.name, pflag.ContinueOnError)
|
||||
opts := ForgetOptions{}
|
||||
opts.AddFlags(set)
|
||||
err := set.Parse(tt.args)
|
||||
rtest.Assert(t, err == nil, "expected no error for input")
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
rtest.Equals(t, tt.want, opts.Hosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+19
-17
@@ -6,12 +6,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newGenerateCommand() *cobra.Command {
|
||||
func newGenerateCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts generateOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -19,7 +22,7 @@ func newGenerateCommand() *cobra.Command {
|
||||
Short: "Generate manual pages and auto-completion files (bash, fish, zsh, powershell)",
|
||||
Long: `
|
||||
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, powershell and zsh).
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -29,7 +32,7 @@ Exit status is 1 if there was any error.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return runGenerate(opts, args)
|
||||
return runGenerate(opts, *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
opts.AddFlags(cmd.Flags())
|
||||
@@ -52,7 +55,7 @@ func (opts *generateOptions) AddFlags(f *pflag.FlagSet) {
|
||||
f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
|
||||
}
|
||||
|
||||
func writeManpages(root *cobra.Command, dir string) error {
|
||||
func writeManpages(root *cobra.Command, dir string, printer progress.Printer) error {
|
||||
// use a fixed date for the man pages so that generating them is deterministic
|
||||
date, err := time.Parse("Jan 2006", "Jan 2017")
|
||||
if err != nil {
|
||||
@@ -66,14 +69,12 @@ func writeManpages(root *cobra.Command, dir string) error {
|
||||
Date: &date,
|
||||
}
|
||||
|
||||
Verbosef("writing man pages to directory %v\n", dir)
|
||||
printer.P("writing man pages to directory %v", dir)
|
||||
return doc.GenManTree(root, header, dir)
|
||||
}
|
||||
|
||||
func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) {
|
||||
if stdoutIsTerminal() {
|
||||
Verbosef("writing %s completion file to %v\n", shell, filename)
|
||||
}
|
||||
func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts global.Options) (err error) {
|
||||
printer.PT("writing %s completion file to %v", shell, filename)
|
||||
var outWriter io.Writer
|
||||
if filename != "-" {
|
||||
var outFile *os.File
|
||||
@@ -84,7 +85,7 @@ func writeCompletion(filename string, shell string, generate func(w io.Writer) e
|
||||
defer func() { err = outFile.Close() }()
|
||||
outWriter = outFile
|
||||
} else {
|
||||
outWriter = globalOptions.stdout
|
||||
outWriter = gopts.Term.OutputWriter()
|
||||
}
|
||||
|
||||
err = generate(outWriter)
|
||||
@@ -110,15 +111,16 @@ func checkStdoutForSingleShell(opts generateOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runGenerate(opts generateOptions, args []string) error {
|
||||
func runGenerate(opts generateOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
if len(args) > 0 {
|
||||
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
|
||||
}
|
||||
|
||||
cmdRoot := newRootCommand()
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
cmdRoot := newRootCommand(&global.Options{})
|
||||
|
||||
if opts.ManDir != "" {
|
||||
err := writeManpages(cmdRoot, opts.ManDir)
|
||||
err := writeManpages(cmdRoot, opts.ManDir, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -130,28 +132,28 @@ func runGenerate(opts generateOptions, args []string) error {
|
||||
}
|
||||
|
||||
if opts.BashCompletionFile != "" {
|
||||
err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion)
|
||||
err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion, printer, gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.FishCompletionFile != "" {
|
||||
err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) })
|
||||
err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }, printer, gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.ZSHCompletionFile != "" {
|
||||
err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion)
|
||||
err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion, printer, gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.PowerShellCompletionFile != "" {
|
||||
err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion)
|
||||
err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion, printer, gopts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
)
|
||||
|
||||
func testRunGenerate(t testing.TB, gopts global.Options, opts generateOptions) ([]byte, error) {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runGenerate(opts, gopts, []string{}, gopts.Term)
|
||||
})
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func TestGenerateStdout(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -21,20 +29,14 @@ func TestGenerateStdout(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
globalOptions.stdout = buf
|
||||
err := runGenerate(tc.opts, []string{})
|
||||
output, err := testRunGenerate(t, global.Options{}, tc.opts)
|
||||
rtest.OK(t, err)
|
||||
completionString := buf.String()
|
||||
rtest.Assert(t, strings.Contains(completionString, "# "+tc.name+" completion for restic"), "has no expected completion header")
|
||||
rtest.Assert(t, strings.Contains(string(output), "# "+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{})
|
||||
_, err := testRunGenerate(t, global.Options{}, generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"})
|
||||
rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails")
|
||||
})
|
||||
}
|
||||
|
||||
+28
-53
@@ -8,14 +8,16 @@ import (
|
||||
"github.com/restic/chunker"
|
||||
"github.com/restic/restic/internal/backend/location"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newInitCommand() *cobra.Command {
|
||||
func newInitCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts InitOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -33,7 +35,7 @@ Exit status is 1 if there was any error.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInit(cmd.Context(), opts, globalOptions, args)
|
||||
return runInit(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
opts.AddFlags(cmd.Flags())
|
||||
@@ -42,105 +44,78 @@ Exit status is 1 if there was any error.
|
||||
|
||||
// InitOptions bundles all options for the init command.
|
||||
type InitOptions struct {
|
||||
secondaryRepoOptions
|
||||
global.SecondaryRepoOptions
|
||||
CopyChunkerParameters bool
|
||||
RepositoryVersion string
|
||||
}
|
||||
|
||||
func (opts *InitOptions) AddFlags(f *pflag.FlagSet) {
|
||||
opts.secondaryRepoOptions.AddFlags(f, "secondary", "to copy chunker parameters from")
|
||||
opts.SecondaryRepoOptions.AddFlags(f, "secondary", "to copy chunker parameters from")
|
||||
f.BoolVar(&opts.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
|
||||
f.StringVar(&opts.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 global.Options, args []string, term ui.Terminal) error {
|
||||
if len(args) > 0 {
|
||||
return errors.Fatal("the init command expects no arguments, only options - please see `restic help init` for usage and flags")
|
||||
}
|
||||
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
var version uint
|
||||
if opts.RepositoryVersion == "latest" || opts.RepositoryVersion == "" {
|
||||
switch opts.RepositoryVersion {
|
||||
case "latest", "":
|
||||
version = restic.MaxRepoVersion
|
||||
} else if opts.RepositoryVersion == "stable" {
|
||||
case "stable":
|
||||
version = restic.StableRepoVersion
|
||||
} else {
|
||||
default:
|
||||
v, err := strconv.ParseUint(opts.RepositoryVersion, 10, 32)
|
||||
if err != nil {
|
||||
return errors.Fatal("invalid repository version")
|
||||
}
|
||||
version = uint(v)
|
||||
}
|
||||
if version < restic.MinRepoVersion || version > restic.MaxRepoVersion {
|
||||
return errors.Fatalf("only repository versions between %v and %v are allowed", restic.MinRepoVersion, restic.MaxRepoVersion)
|
||||
}
|
||||
|
||||
chunkerPolynomial, err := maybeReadChunkerPolynomial(ctx, opts, gopts)
|
||||
chunkerPolynomial, err := maybeReadChunkerPolynomial(ctx, opts, gopts, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gopts.Repo, err = ReadRepo(gopts)
|
||||
s, err := global.CreateRepository(ctx, gopts, version, chunkerPolynomial, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gopts.password, err = ReadPasswordTwice(ctx, gopts,
|
||||
"enter password for new repository: ",
|
||||
"enter password again: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
be, err := create(ctx, gopts.Repo, gopts, gopts.extended)
|
||||
if err != nil {
|
||||
return errors.Fatalf("create repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err)
|
||||
}
|
||||
|
||||
s, err := repository.New(be, repository.Options{
|
||||
Compression: gopts.Compression,
|
||||
PackSize: gopts.PackSize * 1024 * 1024,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Fatal(err.Error())
|
||||
}
|
||||
|
||||
err = s.Init(ctx, version, gopts.password, chunkerPolynomial)
|
||||
if err != nil {
|
||||
return errors.Fatalf("create key in repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err)
|
||||
return errors.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
if !gopts.JSON {
|
||||
Verbosef("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.backends, gopts.Repo))
|
||||
printer.P("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.Backends, gopts.Repo))
|
||||
if opts.CopyChunkerParameters && chunkerPolynomial != nil {
|
||||
Verbosef(" with chunker parameters copied from secondary repository\n")
|
||||
} else {
|
||||
Verbosef("\n")
|
||||
printer.P(" with chunker parameters copied from secondary repository")
|
||||
}
|
||||
Verbosef("\n")
|
||||
Verbosef("Please note that knowledge of your password is required to access\n")
|
||||
Verbosef("the repository. Losing your password means that your data is\n")
|
||||
Verbosef("irrecoverably lost.\n")
|
||||
printer.P("")
|
||||
printer.P("Please note that knowledge of your password is required to access")
|
||||
printer.P("the repository. Losing your password means that your data is")
|
||||
printer.P("irrecoverably lost.")
|
||||
|
||||
} else {
|
||||
status := initSuccess{
|
||||
MessageType: "initialized",
|
||||
ID: s.Config().ID,
|
||||
Repository: location.StripPassword(gopts.backends, gopts.Repo),
|
||||
Repository: location.StripPassword(gopts.Backends, gopts.Repo),
|
||||
}
|
||||
return json.NewEncoder(globalOptions.stdout).Encode(status)
|
||||
return json.NewEncoder(gopts.Term.OutputWriter()).Encode(status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts GlobalOptions) (*chunker.Pol, error) {
|
||||
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer progress.Printer) (*chunker.Pol, error) {
|
||||
if opts.CopyChunkerParameters {
|
||||
otherGopts, _, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "secondary")
|
||||
otherGopts, _, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "secondary")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
otherRepo, err := OpenRepository(ctx, otherGopts)
|
||||
otherRepo, err := global.OpenRepository(ctx, otherGopts, printer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,22 +6,27 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func testRunInit(t testing.TB, opts GlobalOptions) {
|
||||
func testRunInit(t testing.TB, gopts global.Options) {
|
||||
repository.TestUseLowSecurityKDFParameters(t)
|
||||
restic.TestDisableCheckPolynomial(t)
|
||||
restic.TestSetLockTimeout(t, 0)
|
||||
|
||||
rtest.OK(t, runInit(context.TODO(), InitOptions{}, opts, nil))
|
||||
t.Logf("repository initialized at %v", opts.Repo)
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runInit(ctx, InitOptions{}, gopts, nil, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
t.Logf("repository initialized at %v", gopts.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))
|
||||
rtest.OK(t, os.WriteFile(filepath.Join(gopts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,20 +39,34 @@ func TestInitCopyChunkerParams(t *testing.T) {
|
||||
testRunInit(t, env2.gopts)
|
||||
|
||||
initOpts := InitOptions{
|
||||
secondaryRepoOptions: secondaryRepoOptions{
|
||||
SecondaryRepoOptions: global.SecondaryRepoOptions{
|
||||
Repo: env2.gopts.Repo,
|
||||
password: env2.gopts.password,
|
||||
Password: env2.gopts.Password,
|
||||
},
|
||||
}
|
||||
rtest.Assert(t, runInit(context.TODO(), initOpts, env.gopts, nil) != nil, "expected invalid init options to fail")
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runInit(ctx, initOpts, gopts, nil, gopts.Term)
|
||||
})
|
||||
rtest.Assert(t, err != nil, "expected invalid init options to fail")
|
||||
|
||||
initOpts.CopyChunkerParameters = true
|
||||
rtest.OK(t, runInit(context.TODO(), initOpts, env.gopts, nil))
|
||||
|
||||
repo, err := OpenRepository(context.TODO(), env.gopts)
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runInit(ctx, initOpts, gopts, nil, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
otherRepo, err := OpenRepository(context.TODO(), env2.gopts)
|
||||
var repo *repository.Repository
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
repo, err = global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||
return err
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
var otherRepo *repository.Repository
|
||||
err = withTermStatus(t, env2.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
otherRepo, err = global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||
return err
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newKeyCommand() *cobra.Command {
|
||||
func newKeyCommand(globalOptions *global.Options) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "key",
|
||||
Short: "Manage keys (passwords)",
|
||||
@@ -17,10 +18,10 @@ per repository.
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newKeyAddCommand(),
|
||||
newKeyListCommand(),
|
||||
newKeyPasswdCommand(),
|
||||
newKeyRemoveCommand(),
|
||||
newKeyAddCommand(globalOptions),
|
||||
newKeyListCommand(globalOptions),
|
||||
newKeyPasswdCommand(globalOptions),
|
||||
newKeyRemoveCommand(globalOptions),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
+17
-13
@@ -5,19 +5,22 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newKeyAddCommand() *cobra.Command {
|
||||
func newKeyAddCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts KeyAddOptions
|
||||
|
||||
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 "key add" command creates a new key and validates the key. Returns the new key ID.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -30,7 +33,7 @@ Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runKeyAdd(cmd.Context(), globalOptions, opts, args)
|
||||
return runKeyAdd(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -52,21 +55,22 @@ func (opts *KeyAddOptions) Add(flags *pflag.FlagSet) {
|
||||
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
|
||||
}
|
||||
|
||||
func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error {
|
||||
func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, args []string, term ui.Terminal) error {
|
||||
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")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false)
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
return addKey(ctx, repo, gopts, opts)
|
||||
return addKey(ctx, repo, gopts, opts, printer)
|
||||
}
|
||||
|
||||
func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyAddOptions) error {
|
||||
func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer progress.Printer) error {
|
||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -74,7 +78,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
|
||||
|
||||
id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key())
|
||||
if err != nil {
|
||||
return errors.Fatalf("creating new key failed: %v\n", err)
|
||||
return errors.Fatalf("creating new key failed: %v", err)
|
||||
}
|
||||
|
||||
err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw)
|
||||
@@ -82,7 +86,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
|
||||
return err
|
||||
}
|
||||
|
||||
Verbosef("saved new key with ID %s\n", id.ID())
|
||||
printer.P("saved new key with ID %s", id.ID())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -90,7 +94,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
|
||||
// testKeyNewPassword is used to set a new password during integration testing.
|
||||
var testKeyNewPassword string
|
||||
|
||||
func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string, insecureNoPassword bool) (string, error) {
|
||||
func getNewPassword(ctx context.Context, gopts global.Options, newPasswordFile string, insecureNoPassword bool) (string, error) {
|
||||
if testKeyNewPassword != "" {
|
||||
return testKeyNewPassword, nil
|
||||
}
|
||||
@@ -103,7 +107,7 @@ func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile st
|
||||
}
|
||||
|
||||
if newPasswordFile != "" {
|
||||
password, err := loadPasswordFromFile(newPasswordFile)
|
||||
password, err := global.LoadPasswordFromFile(newPasswordFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -116,11 +120,11 @@ func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile st
|
||||
// Since we already have an open repository, temporary remove the password
|
||||
// to prompt the user for the passwd.
|
||||
newopts := gopts
|
||||
newopts.password = ""
|
||||
newopts.Password = ""
|
||||
// empty passwords are already handled above
|
||||
newopts.InsecureNoPassword = false
|
||||
|
||||
return ReadPasswordTwice(ctx, newopts,
|
||||
return global.ReadPasswordTwice(ctx, newopts,
|
||||
"enter new password: ",
|
||||
"enter password again: ")
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/backend"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
)
|
||||
|
||||
func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
|
||||
buf, err := withCaptureStdout(func() error {
|
||||
return runKeyList(context.TODO(), gopts, []string{})
|
||||
func testRunKeyListOtherIDs(t testing.TB, gopts global.Options) []string {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyList(ctx, gopts, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
@@ -33,49 +35,94 @@ func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
|
||||
return IDs
|
||||
}
|
||||
|
||||
func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions) {
|
||||
func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts global.Options) {
|
||||
testKeyNewPassword = newPassword
|
||||
defer func() {
|
||||
testKeyNewPassword = ""
|
||||
}()
|
||||
|
||||
rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{}, []string{}))
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
|
||||
func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) {
|
||||
func testRunKeyAddNewKeyUserHost(t testing.TB, gopts global.Options) {
|
||||
testKeyNewPassword = "john's geheimnis"
|
||||
defer func() {
|
||||
testKeyNewPassword = ""
|
||||
}()
|
||||
|
||||
t.Log("adding key for john@example.com")
|
||||
rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{
|
||||
Username: "john",
|
||||
Hostname: "example.com",
|
||||
}, []string{}))
|
||||
|
||||
repo, err := OpenRepository(context.TODO(), gopts)
|
||||
rtest.OK(t, err)
|
||||
key, err := repository.SearchKey(context.TODO(), repo, testKeyNewPassword, 2, "")
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyAdd(ctx, gopts, KeyAddOptions{
|
||||
Username: "john",
|
||||
Hostname: "example.com",
|
||||
}, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
rtest.Equals(t, "john", key.Username)
|
||||
rtest.Equals(t, "example.com", key.Hostname)
|
||||
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||
rtest.OK(t, err)
|
||||
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 2, "")
|
||||
rtest.OK(t, err)
|
||||
|
||||
rtest.Equals(t, "john", key.Username)
|
||||
rtest.Equals(t, "example.com", key.Hostname)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
|
||||
func testRunKeyPasswd(t testing.TB, newPassword string, gopts global.Options) {
|
||||
testKeyNewPassword = newPassword
|
||||
defer func() {
|
||||
testKeyNewPassword = ""
|
||||
}()
|
||||
|
||||
rtest.OK(t, runKeyPasswd(context.TODO(), gopts, KeyPasswdOptions{}, []string{}))
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
|
||||
func testRunKeyRemove(t testing.TB, gopts GlobalOptions, IDs []string) {
|
||||
func testRunKeyPasswdUserHost(t testing.TB, newPassword string, gopts global.Options) {
|
||||
testKeyNewPassword = newPassword
|
||||
defer func() {
|
||||
testKeyNewPassword = ""
|
||||
}()
|
||||
|
||||
t.Log("changing password and setting key for john@example.com")
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{
|
||||
KeyAddOptions: KeyAddOptions{
|
||||
Username: "john",
|
||||
Hostname: "example.com",
|
||||
},
|
||||
}, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
gopts.Password = testKeyNewPassword
|
||||
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||
rtest.OK(t, err)
|
||||
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 1, "")
|
||||
rtest.OK(t, err)
|
||||
|
||||
rtest.Equals(t, "john", key.Username)
|
||||
rtest.Equals(t, "example.com", key.Hostname)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func testRunKeyRemove(t testing.TB, gopts global.Options, IDs []string) {
|
||||
t.Logf("remove %d keys: %q\n", len(IDs), IDs)
|
||||
for _, id := range IDs {
|
||||
rtest.OK(t, runKeyRemove(context.TODO(), gopts, []string{id}))
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyRemove(ctx, gopts, []string{id}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,25 +134,30 @@ func TestKeyAddRemove(t *testing.T) {
|
||||
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
// must list keys more than once
|
||||
env.gopts.backendTestHook = nil
|
||||
env.gopts.BackendTestHook = nil
|
||||
defer cleanup()
|
||||
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
testRunKeyPasswd(t, "geheim2", env.gopts)
|
||||
env.gopts.password = "geheim2"
|
||||
t.Logf("changed password to %q", env.gopts.password)
|
||||
env.gopts.Password = "geheim2"
|
||||
testRunKeyPasswdUserHost(t, "geheim3", env.gopts)
|
||||
env.gopts.Password = "geheim3"
|
||||
t.Logf("changed password to %q", env.gopts.Password)
|
||||
|
||||
for _, newPassword := range passwordList {
|
||||
testRunKeyAddNewKey(t, newPassword, env.gopts)
|
||||
t.Logf("added new password %q", newPassword)
|
||||
env.gopts.password = newPassword
|
||||
env.gopts.Password = newPassword
|
||||
testRunKeyRemove(t, env.gopts, testRunKeyListOtherIDs(t, env.gopts))
|
||||
}
|
||||
|
||||
env.gopts.password = passwordList[len(passwordList)-1]
|
||||
t.Logf("testing access with last password %q\n", env.gopts.password)
|
||||
rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{}))
|
||||
env.gopts.Password = passwordList[len(passwordList)-1]
|
||||
t.Logf("testing access with last password %q\n", env.gopts.Password)
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyList(ctx, gopts, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
testRunCheck(t, env.gopts)
|
||||
|
||||
testRunKeyAddNewKeyUserHost(t, env.gopts)
|
||||
@@ -116,33 +168,40 @@ func TestKeyAddInvalid(t *testing.T) {
|
||||
defer cleanup()
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
||||
NewPasswordFile: "some-file",
|
||||
InsecureNoPassword: true,
|
||||
}, []string{})
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyAdd(ctx, gopts, KeyAddOptions{
|
||||
NewPasswordFile: "some-file",
|
||||
InsecureNoPassword: true,
|
||||
}, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.Assert(t, strings.Contains(err.Error(), "only either"), "unexpected error message, got %q", err)
|
||||
|
||||
pwfile := filepath.Join(t.TempDir(), "pwfile")
|
||||
rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666))
|
||||
|
||||
err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
||||
NewPasswordFile: pwfile,
|
||||
}, []string{})
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyAdd(ctx, gopts, KeyAddOptions{
|
||||
NewPasswordFile: pwfile,
|
||||
}, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err)
|
||||
}
|
||||
|
||||
func TestKeyAddEmpty(t *testing.T) {
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
// must list keys more than once
|
||||
env.gopts.backendTestHook = nil
|
||||
env.gopts.BackendTestHook = nil
|
||||
defer cleanup()
|
||||
testRunInit(t, env.gopts)
|
||||
|
||||
rtest.OK(t, runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
||||
InsecureNoPassword: true,
|
||||
}, []string{}))
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyAdd(ctx, gopts, KeyAddOptions{
|
||||
InsecureNoPassword: true,
|
||||
}, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
env.gopts.password = ""
|
||||
env.gopts.Password = ""
|
||||
env.gopts.InsecureNoPassword = true
|
||||
|
||||
testRunCheck(t, env.gopts)
|
||||
@@ -161,7 +220,7 @@ func TestKeyProblems(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
testRunInit(t, env.gopts)
|
||||
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
||||
env.gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
||||
return &emptySaveBackend{r}, nil
|
||||
}
|
||||
|
||||
@@ -170,16 +229,23 @@ func TestKeyProblems(t *testing.T) {
|
||||
testKeyNewPassword = ""
|
||||
}()
|
||||
|
||||
err := runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{})
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.Term)
|
||||
})
|
||||
t.Log(err)
|
||||
rtest.Assert(t, err != nil, "expected passwd change to fail")
|
||||
|
||||
err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{})
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.Term)
|
||||
})
|
||||
t.Log(err)
|
||||
rtest.Assert(t, err != nil, "expected key adding to fail")
|
||||
|
||||
t.Logf("testing access with initial password %q\n", env.gopts.password)
|
||||
rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{}))
|
||||
t.Logf("testing access with initial password %q\n", env.gopts.Password)
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyList(ctx, gopts, []string{}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
testRunCheck(t, env.gopts)
|
||||
}
|
||||
|
||||
@@ -188,27 +254,37 @@ func TestKeyCommandInvalidArguments(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
testRunInit(t, env.gopts)
|
||||
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
||||
env.gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
||||
return &emptySaveBackend{r}, nil
|
||||
}
|
||||
|
||||
err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{"johndoe"})
|
||||
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{"johndoe"}, gopts.Term)
|
||||
})
|
||||
t.Log(err)
|
||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key add: %v", err)
|
||||
|
||||
err = runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{"johndoe"})
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{"johndoe"}, gopts.Term)
|
||||
})
|
||||
t.Log(err)
|
||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key passwd: %v", err)
|
||||
|
||||
err = runKeyList(context.TODO(), env.gopts, []string{"johndoe"})
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyList(ctx, gopts, []string{"johndoe"}, gopts.Term)
|
||||
})
|
||||
t.Log(err)
|
||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key list: %v", err)
|
||||
|
||||
err = runKeyRemove(context.TODO(), env.gopts, []string{})
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyRemove(ctx, gopts, []string{}, gopts.Term)
|
||||
})
|
||||
t.Log(err)
|
||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
|
||||
|
||||
err = runKeyRemove(context.TODO(), env.gopts, []string{"john", "doe"})
|
||||
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runKeyRemove(ctx, gopts, []string{"john", "doe"}, gopts.Term)
|
||||
})
|
||||
t.Log(err)
|
||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
|
||||
}
|
||||
|
||||
+15
-11
@@ -6,18 +6,21 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/restic/restic/internal/ui/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newKeyListCommand() *cobra.Command {
|
||||
func newKeyListCommand(globalOptions *global.Options) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List keys (passwords)",
|
||||
Long: `
|
||||
The "list" sub-command lists all the keys (passwords) associated with the repository.
|
||||
The "key list" 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
|
||||
used to access the repository.
|
||||
|
||||
@@ -32,27 +35,28 @@ Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runKeyList(cmd.Context(), globalOptions, args)
|
||||
return runKeyList(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
func runKeyList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
return listKeys(ctx, repo, gopts)
|
||||
return listKeys(ctx, repo, gopts, printer)
|
||||
}
|
||||
|
||||
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error {
|
||||
func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer progress.Printer) error {
|
||||
type keyInfo struct {
|
||||
Current bool `json:"current"`
|
||||
ID string `json:"id"`
|
||||
@@ -68,7 +72,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
||||
err := restic.ParallelList(ctx, s, restic.KeyFile, s.Connections(), func(ctx context.Context, id restic.ID, _ int64) error {
|
||||
k, err := repository.LoadKey(ctx, s, id)
|
||||
if err != nil {
|
||||
Warnf("LoadKey() failed: %v\n", err)
|
||||
printer.E("LoadKey() failed: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,7 +82,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
||||
ShortID: id.Str(),
|
||||
UserName: k.Username,
|
||||
HostName: k.Hostname,
|
||||
Created: k.Created.Local().Format(TimeFormat),
|
||||
Created: k.Created.Local().Format(global.TimeFormat),
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
@@ -92,7 +96,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
||||
}
|
||||
|
||||
if gopts.JSON {
|
||||
return json.NewEncoder(globalOptions.stdout).Encode(keys)
|
||||
return json.NewEncoder(gopts.Term.OutputWriter()).Encode(keys)
|
||||
}
|
||||
|
||||
tab := table.New()
|
||||
@@ -105,5 +109,5 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
||||
tab.AddRow(key)
|
||||
}
|
||||
|
||||
return tab.Write(globalOptions.stdout)
|
||||
return tab.Write(gopts.Term.OutputWriter())
|
||||
}
|
||||
|
||||
@@ -5,20 +5,23 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func newKeyPasswdCommand() *cobra.Command {
|
||||
func newKeyPasswdCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts KeyPasswdOptions
|
||||
|
||||
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.
|
||||
Returns the new key ID.
|
||||
The "key passwd" command creates a new key, validates the key and removes the old key ID.
|
||||
Returns the new key ID.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -31,7 +34,7 @@ Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runKeyPasswd(cmd.Context(), globalOptions, opts, args)
|
||||
return runKeyPasswd(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,29 +50,30 @@ func (opts *KeyPasswdOptions) AddFlags(flags *pflag.FlagSet) {
|
||||
opts.KeyAddOptions.Add(flags)
|
||||
}
|
||||
|
||||
func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error {
|
||||
func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptions, args []string, term ui.Terminal) error {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
return changePassword(ctx, repo, gopts, opts)
|
||||
return changePassword(ctx, repo, gopts, opts, printer)
|
||||
}
|
||||
|
||||
func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyPasswdOptions) error {
|
||||
func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer progress.Printer) error {
|
||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := repository.AddKey(ctx, repo, pw, "", "", repo.Key())
|
||||
id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key())
|
||||
if err != nil {
|
||||
return errors.Fatalf("creating new key failed: %v\n", err)
|
||||
return errors.Fatalf("creating new key failed: %v", err)
|
||||
}
|
||||
oldID := repo.KeyID()
|
||||
|
||||
@@ -83,7 +87,7 @@ func changePassword(ctx context.Context, repo *repository.Repository, gopts Glob
|
||||
return err
|
||||
}
|
||||
|
||||
Verbosef("saved new key as %s\n", id)
|
||||
printer.P("saved new key as %s", id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,18 +5,21 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/ui/progress"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newKeyRemoveCommand() *cobra.Command {
|
||||
func newKeyRemoveCommand(globalOptions *global.Options) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove [ID]",
|
||||
Short: "Remove key ID (password) from the repository.",
|
||||
Short: "Remove key ID (password) from the repository",
|
||||
Long: `
|
||||
The "remove" sub-command removes the selected key ID. The "remove" command does not allow
|
||||
removing the current key being used to access the repository.
|
||||
The "key remove" command removes the selected key ID. It does not allow removing the
|
||||
current key being used to access the repository.
|
||||
|
||||
EXIT STATUS
|
||||
===========
|
||||
@@ -29,27 +32,28 @@ Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runKeyRemove(cmd.Context(), globalOptions, args)
|
||||
return runKeyRemove(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("key remove expects one argument as the key id")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
return deleteKey(ctx, repo, args[0])
|
||||
return deleteKey(ctx, repo, args[0], printer)
|
||||
}
|
||||
|
||||
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string) error {
|
||||
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer progress.Printer) error {
|
||||
id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,6 +68,6 @@ func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string
|
||||
return err
|
||||
}
|
||||
|
||||
Verbosef("removed key %v\n", id)
|
||||
printer.P("removed key %v", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
+16
-8
@@ -5,13 +5,15 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/repository/index"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newListCommand() *cobra.Command {
|
||||
func newListCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var listAllowedArgs = []string{"blobs", "packs", "index", "snapshots", "keys", "locks"}
|
||||
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
|
||||
|
||||
@@ -33,7 +35,7 @@ Exit status is 12 if the password is incorrect.
|
||||
DisableAutoGenTag: true,
|
||||
GroupID: cmdGroupDefault,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(cmd.Context(), globalOptions, args)
|
||||
return runList(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
ValidArgs: listAllowedArgs,
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
@@ -41,12 +43,14 @@ Exit status is 12 if the password is incorrect.
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.Fatal("type not specified")
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks")
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks", printer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -69,16 +73,20 @@ func runList(ctx context.Context, gopts GlobalOptions, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return idx.Each(ctx, func(blobs restic.PackedBlob) {
|
||||
Printf("%v %v\n", blobs.Type, blobs.ID)
|
||||
})
|
||||
for blobs := range idx.Values() {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
printer.S("%v %v", blobs.Type, blobs.ID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
return errors.Fatal("invalid type")
|
||||
}
|
||||
|
||||
return repo.List(ctx, t, func(id restic.ID, _ int64) error {
|
||||
Printf("%s\n", id)
|
||||
printer.S("%s", id)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,15 +4,19 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
rtest "github.com/restic/restic/internal/test"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
)
|
||||
|
||||
func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs {
|
||||
buf, err := withCaptureStdout(func() error {
|
||||
return runList(context.TODO(), opts, []string{tpe})
|
||||
func testRunList(t testing.TB, gopts global.Options, tpe string) restic.IDs {
|
||||
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
return runList(ctx, gopts, []string{tpe}, gopts.Term)
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
return parseIDsFromReader(t, buf)
|
||||
@@ -24,21 +28,77 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs {
|
||||
sc := bufio.NewScanner(rd)
|
||||
|
||||
for sc.Scan() {
|
||||
id, err := restic.ParseID(sc.Text())
|
||||
if err != nil {
|
||||
t.Logf("parse id %v: %v", sc.Text(), err)
|
||||
continue
|
||||
if len(sc.Text()) == 64 {
|
||||
id, err := restic.ParseID(sc.Text())
|
||||
if err != nil {
|
||||
t.Logf("parse id %v: %v", sc.Text(), err)
|
||||
continue
|
||||
}
|
||||
IDs = append(IDs, id)
|
||||
} else {
|
||||
// 'list blobs' is different because it lists the blobs together with the blob type
|
||||
// e.g. "tree ac08ce34ba4f8123618661bef2425f7028ffb9ac740578a3ee88684d2523fee8"
|
||||
parts := strings.Split(sc.Text(), " ")
|
||||
id, err := restic.ParseID(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
t.Logf("parse id %v: %v", sc.Text(), err)
|
||||
continue
|
||||
}
|
||||
IDs = append(IDs, id)
|
||||
}
|
||||
|
||||
IDs = append(IDs, id)
|
||||
}
|
||||
|
||||
return IDs
|
||||
}
|
||||
|
||||
func testListSnapshots(t testing.TB, opts GlobalOptions, expected int) restic.IDs {
|
||||
func testListSnapshots(t testing.TB, gopts global.Options, expected int) restic.IDs {
|
||||
t.Helper()
|
||||
snapshotIDs := testRunList(t, "snapshots", opts)
|
||||
snapshotIDs := testRunList(t, gopts, "snapshots")
|
||||
rtest.Assert(t, len(snapshotIDs) == expected, "expected %v snapshot, got %v", expected, snapshotIDs)
|
||||
return snapshotIDs
|
||||
}
|
||||
|
||||
// extract blob set from repository index
|
||||
func testListBlobs(t testing.TB, gopts global.Options) (blobSetFromIndex restic.IDSet) {
|
||||
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||
rtest.OK(t, err)
|
||||
defer unlock()
|
||||
|
||||
// make sure the index is loaded
|
||||
rtest.OK(t, repo.LoadIndex(ctx, nil))
|
||||
|
||||
// get blobs from index
|
||||
blobSetFromIndex = restic.NewIDSet()
|
||||
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
||||
blobSetFromIndex.Insert(blob.ID)
|
||||
}))
|
||||
return nil
|
||||
})
|
||||
rtest.OK(t, err)
|
||||
|
||||
return blobSetFromIndex
|
||||
}
|
||||
|
||||
func TestListBlobs(t *testing.T) {
|
||||
|
||||
env, cleanup := withTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
testSetupBackupData(t, env)
|
||||
opts := BackupOptions{}
|
||||
|
||||
// first backup
|
||||
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
||||
testListSnapshots(t, env.gopts, 1)
|
||||
|
||||
// run the `list blobs` command
|
||||
resticIDs := testRunList(t, env.gopts, "blobs")
|
||||
|
||||
// convert to set
|
||||
testIDSet := restic.NewIDSet(resticIDs...)
|
||||
blobSetFromIndex := testListBlobs(t, env.gopts)
|
||||
|
||||
rtest.Assert(t, blobSetFromIndex.Equals(testIDSet), "the set of restic.ID s should be equal")
|
||||
}
|
||||
|
||||
+42
-36
@@ -15,13 +15,16 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/restic/restic/internal/data"
|
||||
"github.com/restic/restic/internal/errors"
|
||||
"github.com/restic/restic/internal/fs"
|
||||
"github.com/restic/restic/internal/global"
|
||||
"github.com/restic/restic/internal/restic"
|
||||
"github.com/restic/restic/internal/ui"
|
||||
"github.com/restic/restic/internal/walker"
|
||||
)
|
||||
|
||||
func newLsCommand() *cobra.Command {
|
||||
func newLsCommand(globalOptions *global.Options) *cobra.Command {
|
||||
var opts LsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -59,7 +62,8 @@ Exit status is 12 if the password is incorrect.
|
||||
DisableAutoGenTag: true,
|
||||
GroupID: cmdGroupDefault,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runLs(cmd.Context(), opts, globalOptions, args)
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runLs(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||
},
|
||||
}
|
||||
opts.AddFlags(cmd.Flags())
|
||||
@@ -69,7 +73,7 @@ Exit status is 12 if the password is incorrect.
|
||||
// LsOptions collects all options for the ls command.
|
||||
type LsOptions struct {
|
||||
ListLong bool
|
||||
restic.SnapshotFilter
|
||||
data.SnapshotFilter
|
||||
Recursive bool
|
||||
HumanReadable bool
|
||||
Ncdu bool
|
||||
@@ -88,8 +92,8 @@ func (opts *LsOptions) AddFlags(f *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
type lsPrinter interface {
|
||||
Snapshot(sn *restic.Snapshot) error
|
||||
Node(path string, node *restic.Node, isPrefixDirectory bool) error
|
||||
Snapshot(sn *data.Snapshot) error
|
||||
Node(path string, node *data.Node, isPrefixDirectory bool) error
|
||||
LeaveDir(path string) error
|
||||
Close() error
|
||||
}
|
||||
@@ -98,9 +102,9 @@ type jsonLsPrinter struct {
|
||||
enc *json.Encoder
|
||||
}
|
||||
|
||||
func (p *jsonLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
||||
func (p *jsonLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||
type lsSnapshot struct {
|
||||
*restic.Snapshot
|
||||
*data.Snapshot
|
||||
ID *restic.ID `json:"id"`
|
||||
ShortID string `json:"short_id"` // deprecated
|
||||
MessageType string `json:"message_type"` // "snapshot"
|
||||
@@ -117,14 +121,14 @@ func (p *jsonLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
||||
}
|
||||
|
||||
// Node formats node in our custom JSON format, followed by a newline.
|
||||
func (p *jsonLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error {
|
||||
func (p *jsonLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||
if isPrefixDirectory {
|
||||
return nil
|
||||
}
|
||||
return lsNodeJSON(p.enc, path, node)
|
||||
}
|
||||
|
||||
func lsNodeJSON(enc *json.Encoder, path string, node *restic.Node) error {
|
||||
func lsNodeJSON(enc *json.Encoder, path string, node *data.Node) error {
|
||||
n := &struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
@@ -160,7 +164,7 @@ func lsNodeJSON(enc *json.Encoder, path string, node *restic.Node) error {
|
||||
}
|
||||
// Always print size for regular files, even when empty,
|
||||
// but never for other types.
|
||||
if node.Type == restic.NodeTypeFile {
|
||||
if node.Type == data.NodeTypeFile {
|
||||
n.Size = &n.size
|
||||
}
|
||||
|
||||
@@ -178,7 +182,7 @@ type ncduLsPrinter struct {
|
||||
// Snapshot prints a restic snapshot in Ncdu save format.
|
||||
// It opens the JSON list. Nodes are added with lsNodeNcdu and the list is closed by lsCloseNcdu.
|
||||
// Format documentation: https://dev.yorhel.nl/ncdu/jsonfmt
|
||||
func (p *ncduLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
||||
func (p *ncduLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||
const NcduMajorVer = 1
|
||||
const NcduMinorVer = 2
|
||||
|
||||
@@ -191,7 +195,7 @@ func (p *ncduLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
|
||||
func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
|
||||
type NcduNode struct {
|
||||
Name string `json:"name"`
|
||||
Asize uint64 `json:"asize"`
|
||||
@@ -216,7 +220,7 @@ func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
|
||||
Dev: node.DeviceID,
|
||||
Ino: node.Inode,
|
||||
NLink: node.Links,
|
||||
NotReg: node.Type != restic.NodeTypeDir && node.Type != restic.NodeTypeFile,
|
||||
NotReg: node.Type != data.NodeTypeDir && node.Type != data.NodeTypeFile,
|
||||
UID: node.UID,
|
||||
GID: node.GID,
|
||||
Mode: uint16(node.Mode & os.ModePerm),
|
||||
@@ -240,13 +244,13 @@ func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
|
||||
return json.Marshal(outNode)
|
||||
}
|
||||
|
||||
func (p *ncduLsPrinter) Node(path string, node *restic.Node, _ bool) error {
|
||||
func (p *ncduLsPrinter) Node(path string, node *data.Node, _ bool) error {
|
||||
out, err := lsNcduNode(path, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if node.Type == restic.NodeTypeDir {
|
||||
if node.Type == data.NodeTypeDir {
|
||||
_, err = fmt.Fprintf(p.out, ",\n%s[\n%s%s", strings.Repeat(" ", p.depth), strings.Repeat(" ", p.depth+1), string(out))
|
||||
p.depth++
|
||||
} else {
|
||||
@@ -270,15 +274,19 @@ type textLsPrinter struct {
|
||||
dirs []string
|
||||
ListLong bool
|
||||
HumanReadable bool
|
||||
termPrinter interface {
|
||||
P(msg string, args ...interface{})
|
||||
S(msg string, args ...interface{})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *textLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
||||
Verbosef("%v filtered by %v:\n", sn, p.dirs)
|
||||
func (p *textLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||
p.termPrinter.P("%v filtered by %v:", sn, p.dirs)
|
||||
return nil
|
||||
}
|
||||
func (p *textLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error {
|
||||
func (p *textLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||
if !isPrefixDirectory {
|
||||
Printf("%s\n", formatNode(path, node, p.ListLong, p.HumanReadable))
|
||||
p.termPrinter.S("%s", formatNode(path, node, p.ListLong, p.HumanReadable))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -293,10 +301,12 @@ func (p *textLsPrinter) Close() error {
|
||||
// for ls -l output sorting
|
||||
type toSortOutput struct {
|
||||
nodepath string
|
||||
node *restic.Node
|
||||
node *data.Node
|
||||
}
|
||||
|
||||
func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []string) error {
|
||||
func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||
termPrinter := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||
|
||||
if len(args) == 0 {
|
||||
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
|
||||
}
|
||||
@@ -355,7 +365,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
||||
return false
|
||||
}
|
||||
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, termPrinter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -366,8 +376,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
||||
return err
|
||||
}
|
||||
|
||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
||||
if err = repo.LoadIndex(ctx, bar); err != nil {
|
||||
if err = repo.LoadIndex(ctx, termPrinter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -375,17 +384,18 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
||||
|
||||
if gopts.JSON {
|
||||
printer = &jsonLsPrinter{
|
||||
enc: json.NewEncoder(globalOptions.stdout),
|
||||
enc: json.NewEncoder(gopts.Term.OutputWriter()),
|
||||
}
|
||||
} else if opts.Ncdu {
|
||||
printer = &ncduLsPrinter{
|
||||
out: globalOptions.stdout,
|
||||
out: gopts.Term.OutputWriter(),
|
||||
}
|
||||
} else {
|
||||
printer = &textLsPrinter{
|
||||
dirs: dirs,
|
||||
ListLong: opts.ListLong,
|
||||
HumanReadable: opts.HumanReadable,
|
||||
termPrinter: termPrinter,
|
||||
}
|
||||
}
|
||||
if opts.Sort != SortModeName || opts.Reverse {
|
||||
@@ -396,16 +406,12 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
||||
}
|
||||
}
|
||||
|
||||
sn, subfolder, err := (&restic.SnapshotFilter{
|
||||
Hosts: opts.Hosts,
|
||||
Paths: opts.Paths,
|
||||
Tags: opts.Tags,
|
||||
}).FindLatest(ctx, snapshotLister, repo, args[0])
|
||||
sn, subfolder, err := opts.SnapshotFilter.FindLatest(ctx, snapshotLister, repo, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sn.Tree, err = restic.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
|
||||
sn.Tree, err = data.FindTreeDirectory(ctx, repo, sn.Tree, subfolder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -414,7 +420,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
||||
return err
|
||||
}
|
||||
|
||||
processNode := func(_ restic.ID, nodepath string, node *restic.Node, err error) error {
|
||||
processNode := func(_ restic.ID, nodepath string, node *data.Node, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -449,7 +455,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
||||
|
||||
// otherwise, signal the walker to not walk recursively into any
|
||||
// subdirs
|
||||
if node.Type == restic.NodeTypeDir {
|
||||
if node.Type == data.NodeTypeDir {
|
||||
// immediately generate leaveDir if the directory is skipped
|
||||
if printedDir {
|
||||
if err := printer.LeaveDir(nodepath); err != nil {
|
||||
@@ -486,10 +492,10 @@ type sortedPrinter struct {
|
||||
reverse bool
|
||||
}
|
||||
|
||||
func (p *sortedPrinter) Snapshot(sn *restic.Snapshot) error {
|
||||
func (p *sortedPrinter) Snapshot(sn *data.Snapshot) error {
|
||||
return p.printer.Snapshot(sn)
|
||||
}
|
||||
func (p *sortedPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error {
|
||||
func (p *sortedPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||
if !isPrefixDirectory {
|
||||
p.collector = append(p.collector, toSortOutput{path, node})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user