mirror of
https://github.com/restic/restic.git
synced 2026-04-04 20:28:52 +00:00
Compare commits
1058 Commits
v0.17.3
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3190d6a3c0 | ||
|
|
54347eb6fa | ||
|
|
271d622823 | ||
|
|
541eb5cfd8 | ||
|
|
f9b63050eb | ||
|
|
49c988be07 | ||
|
|
9b1d45935a | ||
|
|
102ea6da2b | ||
|
|
4e71921a17 | ||
|
|
c5e09ae9b1 | ||
|
|
1f329cd933 | ||
|
|
a8f0ad5cc4 | ||
|
|
4c56384481 | ||
|
|
8b567a9270 | ||
|
|
27c560b371 | ||
|
|
66d915ef79 | ||
|
|
7077500a3b | ||
|
|
6566f786e9 | ||
|
|
d1937a530b | ||
|
|
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 | ||
|
|
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 | ||
|
|
7d0aa7f2e3 | ||
|
|
18f18b7f99 | ||
|
|
426b71e3e5 | ||
|
|
4871390a81 | ||
|
|
65b21e3348 | ||
|
|
4a7b122fb6 | ||
|
|
86ddee8518 | ||
|
|
2fe271980f | ||
|
|
4f1390436d | ||
|
|
2d7611373e | ||
|
|
f71278138f | ||
|
|
7d5ebdd0b3 | ||
|
|
d6c75ba2dc | ||
|
|
2a9105c050 | ||
|
|
b7bb697cf7 | ||
|
|
b12a638322 | ||
|
|
4e0135e628 | ||
|
|
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 | ||
|
|
9ef8e13102 | ||
|
|
4940e330c0 | ||
|
|
3a63430b07 | ||
|
|
a5e814bd8d | ||
|
|
398862c5c8 | ||
|
|
b47c67fd90 | ||
|
|
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 | ||
|
|
6d7e37edce | ||
|
|
4998fd68a7 | ||
|
|
06cc6017b8 | ||
|
|
37851827c5 | ||
|
|
b75f80ae5f | ||
|
|
31f87b6188 | ||
|
|
b67b88a0c0 | ||
|
|
d57b01d6eb | ||
|
|
fc81df3f54 | ||
|
|
73995b818a | ||
|
|
49abea6952 | ||
|
|
f18b8ad425 | ||
|
|
0a6296bfde | ||
|
|
2403d1f139 | ||
|
|
86a453200a | ||
|
|
518fbbcdc2 | ||
|
|
c62f523e6d | ||
|
|
91e9f65991 | ||
|
|
d839850ed4 | ||
|
|
ac051c3dcd | ||
|
|
20f472a67f | ||
|
|
7b986795de | ||
|
|
4f03e03b2c | ||
|
|
242b607bf6 | ||
|
|
22bbbf42f5 | ||
|
|
3c8fc9d9bc | ||
|
|
5070e62b18 | ||
|
|
d64bad1a90 | ||
|
|
6bdca9a7d5 | ||
|
|
91d582a667 | ||
|
|
ef1e137e7a | ||
|
|
81ac49f59d | ||
|
|
ba2b0b2cc7 | ||
|
|
37a4235e4d | ||
|
|
04898e41d1 | ||
|
|
07e4a78e46 | ||
|
|
236f81758e | ||
|
|
16850c61fa | ||
|
|
67a572fa0d | ||
|
|
4686a12a2d | ||
|
|
4dbed5f905 | ||
|
|
d708c5ea73 | ||
|
|
ee0cb7d1aa | ||
|
|
590dc82719 | ||
|
|
72d70d94f9 | ||
|
|
aaa48e765a | ||
|
|
f61cf4a1e5 | ||
|
|
a22b9d5735 | ||
|
|
e9ae67c968 | ||
|
|
1fe6fbc4b8 | ||
|
|
3d4fb876f4 | ||
|
|
5d182ed1ab | ||
|
|
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 | ||
|
|
d401ad6c1e | ||
|
|
ab024e6a51 | ||
|
|
0e5f41c842 | ||
|
|
321ac6c1c9 | ||
|
|
94b1af580b | ||
|
|
cc6fbbe6ad | ||
|
|
3f70485671 | ||
|
|
d4772aa469 | ||
|
|
13cb90b83a | ||
|
|
823cc3d93a | ||
|
|
9eee32131a | ||
|
|
5e519a25f7 | ||
|
|
c4eb2be31f | ||
|
|
0b22d8dc64 | ||
|
|
2b65ef5710 | ||
|
|
ccb92f5bf0 | ||
|
|
37aa4f824f | ||
|
|
47b048f437 | ||
|
|
cd7f384d77 | ||
|
|
9d58a27428 | ||
|
|
9aad8e9ea5 | ||
|
|
3adf7d4efb | ||
|
|
66ec735ac2 | ||
|
|
63a71f70e3 | ||
|
|
e3ddc8a463 | ||
|
|
66a8e897a9 | ||
|
|
ffd63f893a | ||
|
|
ec19d67512 | ||
|
|
ef18feaeeb | ||
|
|
171f303399 | ||
|
|
dda652614e | ||
|
|
784097a4f8 | ||
|
|
f5989964ed | ||
|
|
cfa3c5884d | ||
|
|
d60acc5697 | ||
|
|
2240d1801c | ||
|
|
99fdb00d39 | ||
|
|
2409078d55 | ||
|
|
0b6c355678 | ||
|
|
f7f48b3026 | ||
|
|
1221453d08 | ||
|
|
4b975bda37 | ||
|
|
f8b481fd9b | ||
|
|
f88d5adaa2 | ||
|
|
89909d41aa | ||
|
|
06535e62c1 | ||
|
|
c99c76ada8 | ||
|
|
4350b95d27 | ||
|
|
2e58561ad6 | ||
|
|
17b585f7c7 | ||
|
|
4640b3c41a | ||
|
|
c36970074d | ||
|
|
15e90b7a4c | ||
|
|
8d2d50d095 | ||
|
|
62453f9356 | ||
|
|
6caad10840 | ||
|
|
4420fde378 | ||
|
|
a389977bd7 | ||
|
|
6e45c51509 | ||
|
|
5e7333d28d | ||
|
|
c617364d15 | ||
|
|
e2ccb18e22 | ||
|
|
d2c5241961 | ||
|
|
f238f81ba6 | ||
|
|
3788605127 | ||
|
|
29b4680873 | ||
|
|
092899df8b | ||
|
|
2099ec1cd6 | ||
|
|
1daf5317f8 | ||
|
|
db8daeb192 | ||
|
|
ef692991a4 | ||
|
|
062cfc549d | ||
|
|
3e58b15ace | ||
|
|
69249372bf | ||
|
|
445477312c | ||
|
|
cc4712f8e9 | ||
|
|
c405e9e748 | ||
|
|
5f40e4b7c5 | ||
|
|
0b0987233f | ||
|
|
d66e9cfff5 | ||
|
|
e40996f0f1 | ||
|
|
818cb386a5 | ||
|
|
9f724f7dc5 | ||
|
|
3f42c0ad96 | ||
|
|
794341a494 | ||
|
|
74b76ca0df | ||
|
|
3b21c7da3d | ||
|
|
f838bf1056 | ||
|
|
664971eb1d | ||
|
|
de9a040d27 | ||
|
|
89826ef5ce | ||
|
|
a2a1309fd9 | ||
|
|
6309952a82 | ||
|
|
5e7ce45ede | ||
|
|
cb8575f001 | ||
|
|
8d1185b3b8 | ||
|
|
c970e58739 | ||
|
|
5ddda7f5e9 | ||
|
|
8c12291f56 | ||
|
|
5190933561 | ||
|
|
00e69f242e | ||
|
|
00628e952f | ||
|
|
39e63ee4e3 | ||
|
|
3b8d15d651 | ||
|
|
2fd8a3865c | ||
|
|
0c4e65228a | ||
|
|
120bd08c0d | ||
|
|
d378a171c8 | ||
|
|
c752867f0a | ||
|
|
412d6d9ec5 | ||
|
|
5497217018 | ||
|
|
aa9cdf93cf | ||
|
|
aacd6a47e3 | ||
|
|
dc9b6378f3 | ||
|
|
4e58902de6 | ||
|
|
39823c5f6c | ||
|
|
421842f41f | ||
|
|
59b7007534 | ||
|
|
da47967316 | ||
|
|
49a411f7ac | ||
|
|
7cc1aa0cd4 | ||
|
|
a58a8f2ce0 | ||
|
|
79d435efb1 | ||
|
|
9cdf91b406 | ||
|
|
4104a8e6a5 | ||
|
|
6cc06e0812 | ||
|
|
c32613a624 | ||
|
|
1807627dda | ||
|
|
993eb112cd | ||
|
|
36d8916354 | ||
|
|
060a44202f | ||
|
|
d79681b987 | ||
|
|
90e2c419e4 | ||
|
|
7ab5bb6df4 | ||
|
|
efd2ec086f | ||
|
|
8d970e36cf | ||
|
|
58f58a995d | ||
|
|
d71ddfb89b | ||
|
|
536ebefff4 | ||
|
|
9566e2db4a | ||
|
|
7829728182 | ||
|
|
72b343fe5a | ||
|
|
9c8c59c889 | ||
|
|
c4d988faf8 | ||
|
|
080c8de1a9 | ||
|
|
c1781e0abb | ||
|
|
2b9113721c | ||
|
|
afe4fcc0d9 | ||
|
|
c2e404a0ee | ||
|
|
c4be05dbc2 | ||
|
|
d0d887138c | ||
|
|
8eaa4b6602 | ||
|
|
e77681f2cd | ||
|
|
a63500663a | ||
|
|
fde64133df | ||
|
|
6301250d83 | ||
|
|
9331461a13 | ||
|
|
ed3922ac82 | ||
|
|
8b63e1cd72 | ||
|
|
5e8654c71d | ||
|
|
d5a94583ed | ||
|
|
115ecb3c92 | ||
|
|
e6f9cfb8c8 | ||
|
|
b7ff8ea9cd | ||
|
|
99e105eeb6 | ||
|
|
5bf0204caf | ||
|
|
14d02df8bb | ||
|
|
bd4ce8aac1 | ||
|
|
da71e77b28 | ||
|
|
27189e03ee | ||
|
|
4e1eeeb721 | ||
|
|
3b37983a60 | ||
|
|
99646fdf62 | ||
|
|
0331891545 | ||
|
|
2b45c004be | ||
|
|
44cef25077 | ||
|
|
cd84fe0853 | ||
|
|
3ac697d03d | ||
|
|
24422e20a6 | ||
|
|
f457b16b23 | ||
|
|
af839f9548 | ||
|
|
bbb492ee65 | ||
|
|
01405f1e1b | ||
|
|
caa59bb81b | ||
|
|
de3acd7937 | ||
|
|
9e85119d73 | ||
|
|
37969ae8e3 | ||
|
|
6808004ad1 | ||
|
|
8d45a4b283 | ||
|
|
4fb9aa4351 | ||
|
|
d422e75e08 | ||
|
|
144221b430 | ||
|
|
d7d9af4c9f | ||
|
|
2f0049cd6c | ||
|
|
72c02fa759 | ||
|
|
770841f95d | ||
|
|
5e0a045481 | ||
|
|
3fecddafe8 | ||
|
|
40987a5f80 | ||
|
|
875976f4a8 | ||
|
|
2dc00cfd36 | ||
|
|
45d2b4cd3c | ||
|
|
a4d776ec8f | ||
|
|
098db935f7 | ||
|
|
ead57ec501 | ||
|
|
8f9d755b44 | ||
|
|
1062546563 | ||
|
|
0bf8af7188 | ||
|
|
9a674ecc34 | ||
|
|
9a99141a5f | ||
|
|
847b2efba2 | ||
|
|
641390103d | ||
|
|
806fa534ce | ||
|
|
5df6bf80b1 | ||
|
|
dc89aad722 | ||
|
|
3c0ceda536 | ||
|
|
c5fb46da53 | ||
|
|
8642049532 | ||
|
|
8644bb145b | ||
|
|
0997f26461 | ||
|
|
a5c49e5340 | ||
|
|
b51bf0c0c4 | ||
|
|
6cb19e0190 | ||
|
|
d7f4b9db60 | ||
|
|
087f95a298 | ||
|
|
6084848e5a | ||
|
|
48dbefc37e | ||
|
|
2f2ce9add2 | ||
|
|
623ba92b98 | ||
|
|
b402e8a6fc | ||
|
|
548fa07577 | ||
|
|
f8031561f2 | ||
|
|
49ef3ebec3 | ||
|
|
dfbd4fb983 | ||
|
|
1133498ef8 | ||
|
|
9c758313e3 | ||
|
|
82c5043fc9 | ||
|
|
a73ae7ba1a | ||
|
|
bd16804812 | ||
|
|
e2a98aa955 | ||
|
|
408ec41a1d | ||
|
|
270e7b7679 | ||
|
|
97f3e15039 | ||
|
|
d5bd3fcda5 | ||
|
|
f9a90aae89 | ||
|
|
289159beaf | ||
|
|
4052a5927c | ||
|
|
d3c3390a51 | ||
|
|
569a117a1d | ||
|
|
41fa41b28b | ||
|
|
3eb9556f6a | ||
|
|
f5b1f9c8b1 | ||
|
|
e65f4e2231 | ||
|
|
bcf5fbe498 | ||
|
|
ded9fc7690 | ||
|
|
b3b173a47c | ||
|
|
e18a2a0072 | ||
|
|
1eea41c49e | ||
|
|
71c185313e | ||
|
|
868efe4968 | ||
|
|
3be2b8a54b | ||
|
|
b5bc76cdc7 | ||
|
|
58dc4a6892 | ||
|
|
74c783b850 | ||
|
|
fc92a04284 | ||
|
|
2f698d1cff | ||
|
|
d8bf327d8b | ||
|
|
2b3672198c | ||
|
|
de847a48bf | ||
|
|
d1d8ae7368 | ||
|
|
a32c98a39c | ||
|
|
53cb6200fa | ||
|
|
ae9268dadf | ||
|
|
a494bf661d | ||
|
|
51cd1c847b | ||
|
|
14370fbf9e | ||
|
|
62af5f0b4a | ||
|
|
cb9247530e | ||
|
|
1d0d5d87bc | ||
|
|
03aad742d3 | ||
|
|
15b7fb784f | ||
|
|
33da501c35 | ||
|
|
cd44b2bf8b | ||
|
|
1f0f6ad63d | ||
|
|
ca4bd1b8ca | ||
|
|
e320edd416 | ||
|
|
821000cb68 | ||
|
|
db686592a1 | ||
|
|
bff3341d10 | ||
|
|
5fe6607127 | ||
|
|
8f20d5dcd5 | ||
|
|
f967a33ccc | ||
|
|
ec43594003 | ||
|
|
e1faf7b18c | ||
|
|
fc6f1b4b06 | ||
|
|
9f206601af | ||
|
|
ca79cb92e3 | ||
|
|
352605d9f0 | ||
|
|
26b77a543d | ||
|
|
b988754a6d | ||
|
|
60960d2405 | ||
|
|
7c02141548 | ||
|
|
b434f560cc | ||
|
|
7bdfcf13fb | ||
|
|
2e704c69ac | ||
|
|
5838896962 | ||
|
|
bcd5ac34bb | ||
|
|
618f306f13 | ||
|
|
75711446e1 | ||
|
|
c3b3120e10 | ||
|
|
e29d38f8bf | ||
|
|
da3c02405b | ||
|
|
55c150054d | ||
|
|
012cb06fe9 | ||
|
|
f44b7cdf8c | ||
|
|
e91a456656 | ||
|
|
e21496f217 | ||
|
|
0c0d8b8cfd | ||
|
|
60cba55647 | ||
|
|
221fa0fa7c | ||
|
|
7cfd8a6715 | ||
|
|
0ada0b56b6 | ||
|
|
7c12bd59a0 | ||
|
|
888abff7e0 | ||
|
|
783901726e | ||
|
|
eac00eb933 | ||
|
|
96c1c1a0fc | ||
|
|
8d7f4574b4 | ||
|
|
ddf65b04f3 | ||
|
|
2b609d3e77 | ||
|
|
19653f9e06 | ||
|
|
e10e2bb50f | ||
|
|
b5c28a7ba2 | ||
|
|
f3f629bb69 | ||
|
|
e90085b375 | ||
|
|
3f08dee685 | ||
|
|
8c7a6daa47 | ||
|
|
3d976562fa | ||
|
|
1a7fafc7eb | ||
|
|
4469fe1575 | ||
|
|
bad6c54a33 | ||
|
|
7680f48258 | ||
|
|
efec1a5e96 | ||
|
|
bd2c986592 | ||
|
|
cab6b15603 | ||
|
|
4105e4a356 | ||
|
|
ccf5be235a | ||
|
|
5ce6ca2219 | ||
|
|
51173c5003 | ||
|
|
e9940f39dc | ||
|
|
6ec2b62ec5 | ||
|
|
4795143d6d | ||
|
|
a84e65b7f9 | ||
|
|
6f08dbb2d7 | ||
|
|
c1532179d4 | ||
|
|
34fe73ea42 | ||
|
|
37d5bd61a0 | ||
|
|
7b1a15916d | ||
|
|
113439c69b | ||
|
|
5468e85222 | ||
|
|
b69c6408a6 | ||
|
|
d656a50852 | ||
|
|
87f30bc787 | ||
|
|
4f0affd4f7 | ||
|
|
3df8337d63 | ||
|
|
00ca0b371b | ||
|
|
8a0edde407 | ||
|
|
0a225049d8 | ||
|
|
3023b2f566 | ||
|
|
a6490feab2 | ||
|
|
daa6448a77 | ||
|
|
07a8b73f25 | ||
|
|
9a6059eb71 | ||
|
|
790dbd442b | ||
|
|
daf156a76a | ||
|
|
154ca4d9e8 | ||
|
|
ebd8f0c74a | ||
|
|
6f9513d88c | ||
|
|
d8be8f1e06 | ||
|
|
b91ef3f1ff | ||
|
|
e2bce1b9ee | ||
|
|
ebdd946ac1 | ||
|
|
2aa1e2615b | ||
|
|
6c16733dfd | ||
|
|
f0329bb4e6 | ||
|
|
6d3a5260d3 | ||
|
|
cf051e777a | ||
|
|
cc7f99125a | ||
|
|
65a7157383 | ||
|
|
24f4e780f1 | ||
|
|
ca1e5e10b6 | ||
|
|
3b438e5c7c | ||
|
|
7bb92dc7bd | ||
|
|
e79dca644e | ||
|
|
70fbad6623 | ||
|
|
6fd5d5f2d5 | ||
|
|
f1585af0f2 | ||
|
|
5d58945718 | ||
|
|
41c031a19e | ||
|
|
f9dbcd2531 | ||
|
|
c6fae0320e | ||
|
|
e5cdae9c84 | ||
|
|
507842b614 | ||
|
|
263709da8c | ||
|
|
80ed863aab | ||
|
|
0ddb4441d7 | ||
|
|
fc549c9462 | ||
|
|
b9b32e5647 | ||
|
|
a2e54eac64 | ||
|
|
5644079707 | ||
|
|
3e0c081bed | ||
|
|
97f696b937 | ||
|
|
af989aab4e | ||
|
|
6024597028 | ||
|
|
943b6ccfba | ||
|
|
a5533344f9 | ||
|
|
ddf35a60ad | ||
|
|
4fcedb4bae | ||
|
|
a0f2dfbc19 | ||
|
|
0aadfe32bb | ||
|
|
dab3e549af | ||
|
|
5c238ea359 | ||
|
|
2c85d2468a | ||
|
|
7bbf75237d | ||
|
|
dd90e1926b | ||
|
|
d19f706d50 | ||
|
|
8eff4e0e5c | ||
|
|
45d05eb691 | ||
|
|
9c70794886 | ||
|
|
6fbfccc2d3 | ||
|
|
1931beab8e | ||
|
|
2296fdf668 | ||
|
|
89d216ca76 | ||
|
|
5cffd40002 | ||
|
|
e24dd5a162 | ||
|
|
2063bf5de4 | ||
|
|
36c4475ad9 | ||
|
|
dc5d3fc473 | ||
|
|
05077eaa20 | ||
|
|
908d097904 | ||
|
|
828c8bc1e8 | ||
|
|
b8f409723d | ||
|
|
8a8f5f3986 | ||
|
|
7de53a51b8 | ||
|
|
9649a9c62b | ||
|
|
354c2c38cc | ||
|
|
ff9ef08f65 | ||
|
|
311b27ced8 | ||
|
|
43b36ad2b0 | ||
|
|
2e55209b34 | ||
|
|
e7db5febcf | ||
|
|
7739aa685c | ||
|
|
5988d825b7 | ||
|
|
a8efaee03c | ||
|
|
8672cef972 | ||
|
|
551dfee707 | ||
|
|
1b8ca32e7d | ||
|
|
489af2a670 | ||
|
|
97df01b9b8 | ||
|
|
68f7abcff1 | ||
|
|
ceb45d9816 | ||
|
|
5cca6e66be | ||
|
|
c9097994b9 | ||
|
|
c636ad51a8 | ||
|
|
88174cd0a4 | ||
|
|
b7d014b685 | ||
|
|
56f28c9bd5 | ||
|
|
7462471c6b | ||
|
|
74d3f92cc7 | ||
|
|
80f24584a5 | ||
|
|
8e00158c34 | ||
|
|
36b5580c1c | ||
|
|
19f487750e | ||
|
|
f1407afd1f | ||
|
|
4401265e36 | ||
|
|
5fd984ba6f | ||
|
|
506e07127f | ||
|
|
720609f8ba | ||
|
|
a23e7bfb82 | ||
|
|
f66624f5bf | ||
|
|
d3f9c05312 | ||
|
|
6283915f86 | ||
|
|
2d250a9135 | ||
|
|
33c670dd7a | ||
|
|
849c441455 | ||
|
|
b5b5c1fe8e | ||
|
|
1d392a36f9 | ||
|
|
049186371f | ||
|
|
910f64ce47 | ||
|
|
b3b71e78cd | ||
|
|
f2e2e5f5ab | ||
|
|
ecd03b4fc6 | ||
|
|
3f5e2160de | ||
|
|
400ae55940 | ||
|
|
84c79f1456 | ||
|
|
0b19f6cf5a | ||
|
|
fbecc9db66 | ||
|
|
ad48751adb | ||
|
|
853a686994 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -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 hygenic build.
|
||||||
|
*
|
||||||
|
!/*.go
|
||||||
|
!/go.*
|
||||||
|
!/cmd/*
|
||||||
|
!/docker/entrypoint.sh
|
||||||
|
!/internal/*
|
||||||
|
!/helpers/*
|
||||||
|
!/VERSION
|
||||||
|
!/.git/
|
||||||
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -28,13 +28,15 @@ Checklist
|
|||||||
You do not need to check all the boxes below all at once. Feel free to take
|
You do not need to check all the boxes below all at once. Feel free to take
|
||||||
your time and add more commits. If you're done and ready for review, please
|
your time and add more commits. If you're done and ready for review, please
|
||||||
check the last box. Enable a checkbox by replacing [ ] with [x].
|
check the last box. Enable a checkbox by replacing [ ] with [x].
|
||||||
|
|
||||||
|
Please always follow these steps:
|
||||||
|
- Read the [contribution guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches).
|
||||||
|
- Enable [maintainer edits](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
|
||||||
|
- Run `gofmt` on the code in all commits.
|
||||||
|
- Format all commit messages in the same style as [the other commits in the repository](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I have read the [contribution guidelines](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#providing-patches).
|
- [ ] I have added tests for all code changes, see [writing tests](https://restic.readthedocs.io/en/stable/090_participating.html#writing-tests)
|
||||||
- [ ] I have [enabled maintainer edits](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork).
|
|
||||||
- [ ] I have added tests for all code changes.
|
|
||||||
- [ ] I have added documentation for relevant changes (in the manual).
|
- [ ] I have added documentation for relevant changes (in the manual).
|
||||||
- [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/restic/blob/master/changelog/TEMPLATE)).
|
- [ ] There's a new file in `changelog/unreleased/` that describes the changes for our users (see [template](https://github.com/restic/restic/blob/master/changelog/TEMPLATE)).
|
||||||
- [ ] I have run `gofmt` on the code in all commits.
|
|
||||||
- [ ] All commit messages are formatted in the same style as [the other commits in the repo](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
|
|
||||||
- [ ] I'm done! This pull request is ready for review.
|
- [ ] I'm done! This pull request is ready for review.
|
||||||
|
|||||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -5,6 +5,10 @@ updates:
|
|||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "monthly"
|
||||||
|
groups:
|
||||||
|
golang-x-deps:
|
||||||
|
patterns:
|
||||||
|
- "golang.org/x/*"
|
||||||
|
|
||||||
# Dependencies listed in .github/workflows/*.yml
|
# Dependencies listed in .github/workflows/*.yml
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
|
|||||||
35
.github/workflows/docker.yml
vendored
35
.github/workflows/docker.yml
vendored
@@ -20,12 +20,16 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
image: ${{ steps.image.outputs.image }}
|
||||||
|
digest: ${{ steps.build-and-push.outputs.digest }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -37,6 +41,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
|
type=sha
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
@@ -45,7 +50,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3
|
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
|
||||||
|
|
||||||
- name: Ensure consistent binaries
|
- name: Ensure consistent binaries
|
||||||
run: |
|
run: |
|
||||||
@@ -55,6 +60,7 @@ jobs:
|
|||||||
if: github.ref != 'refs/heads/master'
|
if: github.ref != 'refs/heads/master'
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
|
id: build-and-push
|
||||||
uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1
|
uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
@@ -64,3 +70,26 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Output image
|
||||||
|
id: image
|
||||||
|
run: |
|
||||||
|
# NOTE: Set the image as an output because the `env` context is not
|
||||||
|
# available to the inputs of a reusable workflow call.
|
||||||
|
image_name="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
echo "image=$image_name" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
provenance:
|
||||||
|
needs: [build-and-push-image]
|
||||||
|
permissions:
|
||||||
|
actions: read # for detecting the Github Actions environment.
|
||||||
|
id-token: write # for creating OIDC tokens for signing.
|
||||||
|
packages: write # for uploading attestations.
|
||||||
|
if: github.repository == 'restic/restic'
|
||||||
|
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
|
||||||
|
with:
|
||||||
|
image: ${{ needs.build-and-push-image.outputs.image }}
|
||||||
|
digest: ${{ needs.build-and-push-image.outputs.digest }}
|
||||||
|
registry-username: ${{ github.actor }}
|
||||||
|
secrets:
|
||||||
|
registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
45
.github/workflows/tests.yml
vendored
45
.github/workflows/tests.yml
vendored
@@ -13,7 +13,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
latest_go: "1.22.x"
|
latest_go: "1.25.x"
|
||||||
GO111MODULE: on
|
GO111MODULE: on
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -23,39 +23,29 @@ jobs:
|
|||||||
# list of jobs to run:
|
# list of jobs to run:
|
||||||
include:
|
include:
|
||||||
- job_name: Windows
|
- job_name: Windows
|
||||||
go: 1.22.x
|
go: 1.25.x
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
|
|
||||||
- job_name: macOS
|
- job_name: macOS
|
||||||
go: 1.22.x
|
go: 1.25.x
|
||||||
os: macOS-latest
|
os: macOS-latest
|
||||||
test_fuse: false
|
test_fuse: false
|
||||||
|
|
||||||
- job_name: Linux
|
- job_name: Linux
|
||||||
go: 1.22.x
|
go: 1.25.x
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
test_cloud_backends: true
|
test_cloud_backends: true
|
||||||
test_fuse: true
|
test_fuse: true
|
||||||
check_changelog: true
|
check_changelog: true
|
||||||
|
|
||||||
- job_name: Linux (race)
|
- job_name: Linux (race)
|
||||||
go: 1.22.x
|
go: 1.25.x
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
test_fuse: true
|
test_fuse: true
|
||||||
test_opts: "-race"
|
test_opts: "-race"
|
||||||
|
|
||||||
- job_name: Linux
|
- job_name: Linux
|
||||||
go: 1.21.x
|
go: 1.24.x
|
||||||
os: ubuntu-latest
|
|
||||||
test_fuse: true
|
|
||||||
|
|
||||||
- job_name: Linux
|
|
||||||
go: 1.20.x
|
|
||||||
os: ubuntu-latest
|
|
||||||
test_fuse: true
|
|
||||||
|
|
||||||
- job_name: Linux
|
|
||||||
go: 1.19.x
|
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
test_fuse: true
|
test_fuse: true
|
||||||
|
|
||||||
@@ -67,10 +57,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go ${{ matrix.go }}
|
- name: Set up Go ${{ matrix.go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
@@ -195,7 +185,7 @@ jobs:
|
|||||||
# prepare credentials for Google Cloud Storage tests in a temp file
|
# prepare credentials for Google Cloud Storage tests in a temp file
|
||||||
export GOOGLE_APPLICATION_CREDENTIALS=$(mktemp --tmpdir restic-gcs-auth-XXXXXXX)
|
export GOOGLE_APPLICATION_CREDENTIALS=$(mktemp --tmpdir restic-gcs-auth-XXXXXXX)
|
||||||
echo $RESTIC_TEST_GS_APPLICATION_CREDENTIALS_B64 | base64 -d > $GOOGLE_APPLICATION_CREDENTIALS
|
echo $RESTIC_TEST_GS_APPLICATION_CREDENTIALS_B64 | base64 -d > $GOOGLE_APPLICATION_CREDENTIALS
|
||||||
go test -cover -parallel 4 ./internal/backend/...
|
go test -cover -parallel 5 -timeout 15m ./internal/backend/...
|
||||||
|
|
||||||
# only run cloud backend tests for pull requests from and pushes to our
|
# only run cloud backend tests for pull requests from and pushes to our
|
||||||
# own repo, otherwise the secrets are not available
|
# own repo, otherwise the secrets are not available
|
||||||
@@ -214,7 +204,6 @@ jobs:
|
|||||||
|
|
||||||
cross_compile:
|
cross_compile:
|
||||||
strategy:
|
strategy:
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
# run cross-compile in three batches parallel so the overall tests run faster
|
# run cross-compile in three batches parallel so the overall tests run faster
|
||||||
subset:
|
subset:
|
||||||
@@ -231,10 +220,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go ${{ env.latest_go }}
|
- name: Set up Go ${{ env.latest_go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.latest_go }}
|
go-version: ${{ env.latest_go }}
|
||||||
|
|
||||||
@@ -253,18 +242,18 @@ jobs:
|
|||||||
checks: write
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go ${{ env.latest_go }}
|
- name: Set up Go ${{ env.latest_go }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.latest_go }}
|
go-version: ${{ env.latest_go }}
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||||
version: v1.57.1
|
version: v2.4.0
|
||||||
args: --verbose --timeout 5m
|
args: --verbose --timeout 5m
|
||||||
|
|
||||||
# only run golangci-lint for pull requests, otherwise ALL hints get
|
# only run golangci-lint for pull requests, otherwise ALL hints get
|
||||||
@@ -298,7 +287,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -321,7 +310,7 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
|
|||||||
136
.golangci.yml
136
.golangci.yml
@@ -1,69 +1,95 @@
|
|||||||
# This is the configuration for golangci-lint for the restic project.
|
version: "2"
|
||||||
#
|
|
||||||
# A sample config with all settings is here:
|
|
||||||
# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
# only enable the linters listed below
|
# only enable the linters listed below
|
||||||
disable-all: true
|
default: none
|
||||||
enable:
|
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
|
# make sure all errors returned by functions are handled
|
||||||
- errcheck
|
- 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
|
# examine code and report suspicious constructs, such as Printf calls whose
|
||||||
# arguments do not align with the format string
|
# arguments do not align with the format string
|
||||||
- govet
|
- govet
|
||||||
|
# consistent imports
|
||||||
# make sure names and comments are used according to the conventions
|
- importas
|
||||||
- revive
|
|
||||||
|
|
||||||
# detect when assignments to existing variables are not used
|
# detect when assignments to existing variables are not used
|
||||||
- ineffassign
|
- ineffassign
|
||||||
|
- nolintlint
|
||||||
|
# make sure names and comments are used according to the conventions
|
||||||
|
- revive
|
||||||
# run static analysis and find errors
|
# run static analysis and find errors
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
|
||||||
# find unused variables, functions, structs, types, etc.
|
# find unused variables, functions, structs, types, etc.
|
||||||
- unused
|
- unused
|
||||||
|
settings:
|
||||||
# parse and typecheck code
|
depguard:
|
||||||
- typecheck
|
rules:
|
||||||
|
# Prevent backend packages from importing the internal/restic package to keep the architectural layers intact.
|
||||||
# ensure that http response bodies are closed
|
backend-imports:
|
||||||
- bodyclose
|
files:
|
||||||
|
- "**/internal/backend/**"
|
||||||
- importas
|
- "!**/internal/backend/cache/**"
|
||||||
|
- "!**/internal/backend/test/**"
|
||||||
issues:
|
- "!**/*_test.go"
|
||||||
# don't use the default exclude rules, this hides (among others) ignored
|
deny:
|
||||||
# errors from Close() calls
|
- pkg: "github.com/restic/restic/internal/restic"
|
||||||
exclude-use-default: false
|
desc: "internal/restic should not be imported to keep the architectural layers intact"
|
||||||
|
- pkg: "github.com/restic/restic/internal/repository"
|
||||||
# list of things to not warn about
|
desc: "internal/repository should not be imported to keep the architectural layers intact"
|
||||||
exclude:
|
importas:
|
||||||
# revive: do not warn about missing comments for exported stuff
|
alias:
|
||||||
- exported (function|method|var|type|const) .* should have comment or be unexported
|
- pkg: github.com/restic/restic/internal/test
|
||||||
# revive: ignore constants in all caps
|
alias: rtest
|
||||||
- don't use ALL_CAPS in Go names; use CamelCase
|
staticcheck:
|
||||||
# revive: lots of packages don't have such a comment
|
checks:
|
||||||
- "package-comments: should have a package comment"
|
# default
|
||||||
# staticcheck: there's no easy way to replace these packages
|
- "all"
|
||||||
- "SA1019: \"golang.org/x/crypto/poly1305\" is deprecated"
|
- "-ST1000"
|
||||||
- "SA1019: \"golang.org/x/crypto/openpgp\" is deprecated"
|
- "-ST1003"
|
||||||
|
- "-ST1016"
|
||||||
exclude-rules:
|
- "-ST1020"
|
||||||
# revive: ignore unused parameters in tests
|
- "-ST1021"
|
||||||
- path: (_test\.go|testing\.go|backend/.*/tests\.go)
|
- "-ST1022"
|
||||||
text: "unused-parameter:"
|
# extra disables
|
||||||
|
- "-QF1008" # don't warn about specifing name of embedded field on access
|
||||||
linters-settings:
|
exclusions:
|
||||||
importas:
|
rules:
|
||||||
alias:
|
# revive: ignore unused parameters in tests
|
||||||
- pkg: github.com/restic/restic/internal/test
|
- path: (_test\.go|testing\.go|backend/.*/tests\.go)
|
||||||
alias: rtest
|
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$
|
||||||
|
|||||||
481
CHANGELOG.md
481
CHANGELOG.md
@@ -1,5 +1,7 @@
|
|||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
|
* [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)
|
* [Changelog for 0.17.3](#changelog-for-restic-0173-2024-11-08)
|
||||||
* [Changelog for 0.17.2](#changelog-for-restic-0172-2024-10-27)
|
* [Changelog for 0.17.2](#changelog-for-restic-0172-2024-10-27)
|
||||||
* [Changelog for 0.17.1](#changelog-for-restic-0171-2024-09-05)
|
* [Changelog for 0.17.1](#changelog-for-restic-0171-2024-09-05)
|
||||||
@@ -38,6 +40,485 @@
|
|||||||
* [Changelog for 0.6.0](#changelog-for-restic-060-2017-05-29)
|
* [Changelog for 0.6.0](#changelog-for-restic-060-2017-05-29)
|
||||||
|
|
||||||
|
|
||||||
|
# Changelog for restic 0.18.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.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
* Fix #5324: Correctly handle `backup --stdin-filename` with directory paths
|
||||||
|
* Fix #5325: Accept `RESTIC_HOST` environment variable in `forget` command
|
||||||
|
* Fix #5342: Ignore "chmod not supported" errors when writing files
|
||||||
|
* Fix #5344: Ignore `EOPNOTSUPP` errors for extended attributes
|
||||||
|
* Fix #5421: Fix rare crash if directory is removed during backup
|
||||||
|
* Fix #5429: Stop retrying uploads when rest-server runs out of space
|
||||||
|
* Fix #5467: Improve handling of download retries in `check` command
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
|
* Bugfix #5324: Correctly handle `backup --stdin-filename` with directory paths
|
||||||
|
|
||||||
|
In restic 0.18.0, the `backup` command failed if a filename that includes at
|
||||||
|
least a directory was passed to `--stdin-filename`. For example,
|
||||||
|
`--stdin-filename /foo/bar` resulted in the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
Fatal: unable to save snapshot: open /foo: no such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5324
|
||||||
|
https://github.com/restic/restic/pull/5356
|
||||||
|
|
||||||
|
* Bugfix #5325: Accept `RESTIC_HOST` environment variable in `forget` command
|
||||||
|
|
||||||
|
The `forget` command did not use the host name from the `RESTIC_HOST`
|
||||||
|
environment variable when filtering snapshots. This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5325
|
||||||
|
https://github.com/restic/restic/pull/5327
|
||||||
|
|
||||||
|
* Bugfix #5342: Ignore "chmod not supported" errors when writing files
|
||||||
|
|
||||||
|
Restic 0.18.0 introduced a bug that caused `chmod xxx: operation not supported`
|
||||||
|
errors to appear when writing to a local file repository that did not support
|
||||||
|
chmod (like CIFS or WebDAV mounted via FUSE). Restic now ignores those errors.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5342
|
||||||
|
|
||||||
|
* Bugfix #5344: Ignore `EOPNOTSUPP` errors for extended attributes
|
||||||
|
|
||||||
|
Restic 0.18.0 added extended attribute support for NetBSD 10+, but not all
|
||||||
|
NetBSD filesystems support extended attributes. Other BSD systems can likewise
|
||||||
|
return `EOPNOTSUPP`, so restic now ignores these errors.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5344
|
||||||
|
|
||||||
|
* Bugfix #5421: Fix rare crash if directory is removed during backup
|
||||||
|
|
||||||
|
In restic 0.18.0, the `backup` command could crash if a directory was removed
|
||||||
|
between reading its metadata and listing its directory content. This has now
|
||||||
|
been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5421
|
||||||
|
|
||||||
|
* Bugfix #5429: Stop retrying uploads when rest-server runs out of space
|
||||||
|
|
||||||
|
When rest-server returns a `507 Insufficient Storage` error, it indicates that
|
||||||
|
no more storage capacity is available. Restic now correctly stops retrying
|
||||||
|
uploads in this case.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5429
|
||||||
|
https://github.com/restic/restic/pull/5452
|
||||||
|
|
||||||
|
* Bugfix #5467: Improve handling of download retries in `check` command
|
||||||
|
|
||||||
|
In very rare cases, the `check` command could unnecessarily report repository
|
||||||
|
damage if the backend returned incomplete, corrupted data on the first download
|
||||||
|
try which is afterwards resolved by a download retry.
|
||||||
|
|
||||||
|
This could result in an error output like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
Load(<data/34567890ab>, 33918928, 0) returned error, retrying after 871.35598ms: readFull: unexpected EOF
|
||||||
|
Load(<data/34567890ab>, 33918928, 0) operation successful after 1 retries
|
||||||
|
check successful on second attempt, original error pack 34567890ab[...] contains 6 errors: [blob 12345678[...]: decrypting blob <data/12345678> from 34567890 failed: ciphertext verification failed ...]
|
||||||
|
[...]
|
||||||
|
Fatal: repository contains errors
|
||||||
|
```
|
||||||
|
|
||||||
|
This fix only applies to a very specific case where the log shows `operation
|
||||||
|
successful after 1 retries` followed by a `check successful on second attempt,
|
||||||
|
original error` that only reports `ciphertext verification failed` errors in the
|
||||||
|
pack file. If any other errors are reported in the pack file, then the
|
||||||
|
repository still has to be considered as damaged.
|
||||||
|
|
||||||
|
Now, only the check result of the last download retry is reported as intended.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5467
|
||||||
|
https://github.com/restic/restic/pull/5495
|
||||||
|
|
||||||
|
|
||||||
|
# Changelog for restic 0.18.0 (2025-03-27)
|
||||||
|
The following sections list the changes in restic 0.18.0 relevant to
|
||||||
|
restic users. The changes are ordered by importance.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
* Sec #5291: Mitigate attack on content-defined chunking algorithm
|
||||||
|
* Fix #1843: Correctly restore long filepaths' timestamp on old Windows
|
||||||
|
* Fix #2165: Ignore disappeared backup source files
|
||||||
|
* Fix #5153: Include root tree when searching using `find --tree`
|
||||||
|
* Fix #5169: Prevent Windows VSS event log 8194 warnings for backup with fs snapshot
|
||||||
|
* Fix #5212: Fix duplicate data handling in `prune --max-unused`
|
||||||
|
* Fix #5249: Fix creation of oversized index by `repair index --read-all-packs`
|
||||||
|
* Fix #5259: Fix rare crash in command output
|
||||||
|
* Chg #4938: Update dependencies and require Go 1.23 or newer
|
||||||
|
* Chg #5162: Graduate feature flags
|
||||||
|
* Enh #1378: Add JSON support to `check` command
|
||||||
|
* Enh #2511: Support generating shell completions to stdout
|
||||||
|
* Enh #3697: Allow excluding online-only cloud files (e.g. OneDrive)
|
||||||
|
* Enh #4179: Add `sort` option to `ls` command
|
||||||
|
* Enh #4433: Change default sort order for `find` output
|
||||||
|
* Enh #4521: Add support for Microsoft Blob Storage access tiers
|
||||||
|
* Enh #4942: Add snapshot summary statistics to rewritten snapshots
|
||||||
|
* Enh #4948: Format exit errors as JSON when requested
|
||||||
|
* Enh #4983: Add SLSA provenance to GHCR container images
|
||||||
|
* Enh #5054: Enable compression for ZIP archives in `dump` command
|
||||||
|
* Enh #5081: Add retry mechanism for loading repository config
|
||||||
|
* Enh #5089: Allow including/excluding extended file attributes during `restore`
|
||||||
|
* Enh #5092: Show count of deleted files and directories during `restore`
|
||||||
|
* Enh #5109: Make small pack size configurable for `prune`
|
||||||
|
* Enh #5119: Add start and end timestamps to `backup` JSON output
|
||||||
|
* Enh #5131: Add DragonFlyBSD support
|
||||||
|
* Enh #5137: Make `tag` command print which snapshots were modified
|
||||||
|
* Enh #5141: Provide clear error message if AZURE_ACCOUNT_NAME is not set
|
||||||
|
* Enh #5173: Add experimental S3 cold storage support
|
||||||
|
* Enh #5174: Add xattr support for NetBSD 10+
|
||||||
|
* Enh #5251: Improve retry handling for flaky `rclone` backends
|
||||||
|
* Enh #5287: Make `recover` automatically rebuild index when needed
|
||||||
|
|
||||||
|
## Details
|
||||||
|
|
||||||
|
* Security #5291: Mitigate attack on content-defined chunking algorithm
|
||||||
|
|
||||||
|
Restic uses [Rabin
|
||||||
|
Fingerprints](https://restic.net/blog/2015-09-12/restic-foundation1-cdc/) for
|
||||||
|
its content-defined chunker. The algorithm relies on a secret polynomial to
|
||||||
|
split files into chunks.
|
||||||
|
|
||||||
|
As shown in the paper "[Chunking Attacks on File Backup Services using
|
||||||
|
Content-Defined Chunking](https://eprint.iacr.org/2025/532.pdf)" by Boris
|
||||||
|
Alexeev, Colin Percival and Yan X Zhang, an attacker that can observe chunk
|
||||||
|
sizes for a known file can derive the secret polynomial. Knowledge of the
|
||||||
|
polynomial might in some cases allow an attacker to check whether certain large
|
||||||
|
files are stored in a repository.
|
||||||
|
|
||||||
|
A practical attack is nevertheless hard as restic merges multiple chunks into
|
||||||
|
opaque pack files and by default processes multiple files in parallel. This
|
||||||
|
likely prevents an attacker from matching pack files to the attacker-known file
|
||||||
|
and thereby prevents the attack.
|
||||||
|
|
||||||
|
Despite the low chances of a practical attack, restic now has added mitigation
|
||||||
|
that randomizes how chunks are assembled into pack files. This prevents
|
||||||
|
attackers from guessing which chunks are part of a pack file and thereby
|
||||||
|
prevents learning the chunk sizes.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5291
|
||||||
|
https://github.com/restic/restic/pull/5295
|
||||||
|
|
||||||
|
* Bugfix #1843: Correctly restore long filepaths' timestamp on old Windows
|
||||||
|
|
||||||
|
The `restore` command now correctly restores timestamps for files with paths
|
||||||
|
longer than 256 characters on Windows versions prior to Windows 10 1607.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/1843
|
||||||
|
https://github.com/restic/restic/pull/5061
|
||||||
|
|
||||||
|
* Bugfix #2165: Ignore disappeared backup source files
|
||||||
|
|
||||||
|
The `backup` command now quietly skips files that are removed between directory
|
||||||
|
listing and backup, instead of printing errors like:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lstat /some/file/name: no such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2165
|
||||||
|
https://github.com/restic/restic/issues/3098
|
||||||
|
https://github.com/restic/restic/pull/5143
|
||||||
|
https://github.com/restic/restic/pull/5145
|
||||||
|
|
||||||
|
* Bugfix #5153: Include root tree when searching using `find --tree`
|
||||||
|
|
||||||
|
The `restic find --tree` command did not find trees referenced by `restic
|
||||||
|
snapshot --json`. It now correctly includes the root tree when searching.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5153
|
||||||
|
|
||||||
|
* Bugfix #5169: Prevent Windows VSS event log 8194 warnings for backup with fs snapshot
|
||||||
|
|
||||||
|
When running `backup` with the `--use-fs-snapshot` option in Windows with admin
|
||||||
|
rights, event logs like
|
||||||
|
|
||||||
|
```
|
||||||
|
Volume Shadow Copy Service error: Unexpected error querying for the IVssWriterCallback interface. hr = 0x80070005, Access is denied.
|
||||||
|
. This is often caused by incorrect security settings in either the writer or requester process.
|
||||||
|
|
||||||
|
Operation:
|
||||||
|
Gathering Writer Data
|
||||||
|
|
||||||
|
Context:
|
||||||
|
Writer Class Id: {e8132975-6f93-4464-a53e-1050253ae220}
|
||||||
|
Writer Name: System Writer
|
||||||
|
Writer Instance ID: {54b151ac-d27d-4628-9cb0-2bc40959f50f}
|
||||||
|
```
|
||||||
|
|
||||||
|
Are created several times even though the backup itself succeeds. This has now
|
||||||
|
been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5169
|
||||||
|
https://github.com/restic/restic/pull/5170
|
||||||
|
https://forum.restic.net/t/windows-shadow-copy-snapshot-vss-unexpected-provider-error/3674/2
|
||||||
|
|
||||||
|
* Bugfix #5212: Fix duplicate data handling in `prune --max-unused`
|
||||||
|
|
||||||
|
The `prune --max-unused size` command did not correctly account for duplicate
|
||||||
|
data. If a repository contained a large amount of duplicate data, this could
|
||||||
|
previously result in pruning too little data. This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5212
|
||||||
|
https://forum.restic.net/t/restic-not-obeying-max-unused-parameter-on-prune/8879
|
||||||
|
|
||||||
|
* Bugfix #5249: Fix creation of oversized index by `repair index --read-all-packs`
|
||||||
|
|
||||||
|
Since restic 0.17.0, the new index created by `repair index --read-all-packs`
|
||||||
|
was written as a single large index. This significantly increased memory usage
|
||||||
|
while loading the index.
|
||||||
|
|
||||||
|
The index is now correctly split into multiple smaller indexes, and `repair
|
||||||
|
index` now also automatically splits oversized indexes.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5249
|
||||||
|
|
||||||
|
* Bugfix #5259: Fix rare crash in command output
|
||||||
|
|
||||||
|
Some commands could in rare cases crash when trying to print status messages and
|
||||||
|
request retries at the same time, resulting in an error like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
panic: runtime error: slice bounds out of range [468:156]
|
||||||
|
[...]
|
||||||
|
github.com/restic/restic/internal/ui/termstatus.(*lineWriter).Write(...)
|
||||||
|
/restic/internal/ui/termstatus/stdio_wrapper.go:36 +0x136
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5259
|
||||||
|
https://github.com/restic/restic/pull/5300
|
||||||
|
|
||||||
|
* Change #4938: Update dependencies and require Go 1.23 or newer
|
||||||
|
|
||||||
|
We have updated all dependencies. Restic now requires Go 1.23 or newer to build.
|
||||||
|
|
||||||
|
This also disables support for TLS versions older than TLS 1.2. On Windows,
|
||||||
|
restic now requires at least Windows 10 or Windows Server 2016. On macOS, restic
|
||||||
|
now requires at least macOS 11 Big Sur.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4938
|
||||||
|
|
||||||
|
* Change #5162: Graduate feature flags
|
||||||
|
|
||||||
|
The `deprecate-legacy-index`, `deprecate-s3-legacy-layout`,
|
||||||
|
`explicit-s3-anonymous-auth` and `safe-forget-keep-tags` features are now stable
|
||||||
|
and can no longer be disabled. The corresponding feature flags will be removed
|
||||||
|
in restic 0.19.0.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5162
|
||||||
|
|
||||||
|
* Enhancement #1378: Add JSON support to `check` command
|
||||||
|
|
||||||
|
The `check` command now supports the `--json` option to output all statistics in
|
||||||
|
JSON format.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/1378
|
||||||
|
https://github.com/restic/restic/pull/5194
|
||||||
|
|
||||||
|
* Enhancement #2511: Support generating shell completions to stdout
|
||||||
|
|
||||||
|
The `generate` command now supports using `-` as the filename with the
|
||||||
|
`--[shell]-completion` option to write the generated output to stdout.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2511
|
||||||
|
https://github.com/restic/restic/pull/5053
|
||||||
|
|
||||||
|
* Enhancement #3697: Allow excluding online-only cloud files (e.g. OneDrive)
|
||||||
|
|
||||||
|
Restic treated files synced using OneDrive Files On-Demand as though they were
|
||||||
|
regular files. This caused issues with VSS and could cause OneDrive to download
|
||||||
|
all files.
|
||||||
|
|
||||||
|
Restic now allows the user to exclude these files when backing up with the
|
||||||
|
`--exclude-cloud-files` option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3697
|
||||||
|
https://github.com/restic/restic/issues/4935
|
||||||
|
https://github.com/restic/restic/pull/4990
|
||||||
|
|
||||||
|
* Enhancement #4179: Add `sort` option to `ls` command
|
||||||
|
|
||||||
|
The `ls -l` command output can now be sorted using the new `--sort <field>`
|
||||||
|
option for the fields `name`, `size`, `time` (same as `mtime`), `mtime`,
|
||||||
|
`atime`, `ctime` and `extension`. A `--reverse` option is also available.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4179
|
||||||
|
https://github.com/restic/restic/pull/5182
|
||||||
|
|
||||||
|
* Enhancement #4433: Change default sort order for `find` output
|
||||||
|
|
||||||
|
The `find` command now sorts snapshots from newest to oldest by default. The
|
||||||
|
previous oldest-to-newest order can be restored using the new `--reverse`
|
||||||
|
option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4433
|
||||||
|
https://github.com/restic/restic/pull/5184
|
||||||
|
|
||||||
|
* Enhancement #4521: Add support for Microsoft Blob Storage access tiers
|
||||||
|
|
||||||
|
The new `-o azure.access-tier=<tier>` option allows specifying the access tier
|
||||||
|
(`Hot`, `Cool` or `Cold`) for objects created in Microsoft Blob Storage. If
|
||||||
|
unspecified, the storage account's default tier is used.
|
||||||
|
|
||||||
|
There is no official `Archive` storage support in restic, use this option at
|
||||||
|
your own risk. To restore any data, it is necessary to manually warm up the
|
||||||
|
required data in the `Archive` tier.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4521
|
||||||
|
https://github.com/restic/restic/pull/5046
|
||||||
|
|
||||||
|
* Enhancement #4942: Add snapshot summary statistics to rewritten snapshots
|
||||||
|
|
||||||
|
The `rewrite` command now supports a `--snapshot-summary` option to add
|
||||||
|
statistics data to snapshots. Only two fields in the summary will be non-zero:
|
||||||
|
`TotalFilesProcessed` and `TotalBytesProcessed`.
|
||||||
|
|
||||||
|
For snapshots rewritten using the `--exclude` options, the summary statistics
|
||||||
|
are updated accordingly.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4942
|
||||||
|
https://github.com/restic/restic/pull/5185
|
||||||
|
|
||||||
|
* Enhancement #4948: Format exit errors as JSON when requested
|
||||||
|
|
||||||
|
Restic now formats error messages as JSON when the `--json` flag is used.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4948
|
||||||
|
https://github.com/restic/restic/pull/4952
|
||||||
|
|
||||||
|
* Enhancement #4983: Add SLSA provenance to GHCR container images
|
||||||
|
|
||||||
|
Restic's GitHub Container Registry (GHCR) image build workflow now includes SLSA
|
||||||
|
(Supply-chain Levels for Software Artifacts) provenance generation.
|
||||||
|
|
||||||
|
Please see the restic documentation for more information about verifying SLSA
|
||||||
|
provenance.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4983
|
||||||
|
https://github.com/restic/restic/pull/4999
|
||||||
|
|
||||||
|
* Enhancement #5054: Enable compression for ZIP archives in `dump` command
|
||||||
|
|
||||||
|
The `dump` command now compresses ZIP archives using the DEFLATE algorithm,
|
||||||
|
reducing the size of exported archives.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5054
|
||||||
|
|
||||||
|
* Enhancement #5081: Add retry mechanism for loading repository config
|
||||||
|
|
||||||
|
Restic now retries loading the repository config file when opening a repository.
|
||||||
|
The `init` command now also retries backend operations.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5081
|
||||||
|
https://github.com/restic/restic/pull/5095
|
||||||
|
|
||||||
|
* Enhancement #5089: Allow including/excluding extended file attributes during `restore`
|
||||||
|
|
||||||
|
The `restore` command now supports the `--exclude-xattr` and `--include-xattr`
|
||||||
|
options to control which extended file attributes will be restored. By default,
|
||||||
|
all attributes are restored.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5089
|
||||||
|
https://github.com/restic/restic/pull/5129
|
||||||
|
|
||||||
|
* Enhancement #5092: Show count of deleted files and directories during `restore`
|
||||||
|
|
||||||
|
The `restore` command now reports the number of deleted files and directories,
|
||||||
|
both in the regular output and in the `files_deleted` field of the JSON output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5092
|
||||||
|
https://github.com/restic/restic/pull/5100
|
||||||
|
|
||||||
|
* Enhancement #5109: Make small pack size configurable for `prune`
|
||||||
|
|
||||||
|
The `prune` command now supports the `--repack-smaller-than` option that allows
|
||||||
|
repacking pack files smaller than a specified size.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5109
|
||||||
|
https://github.com/restic/restic/pull/5183
|
||||||
|
|
||||||
|
* Enhancement #5119: Add start and end timestamps to `backup` JSON output
|
||||||
|
|
||||||
|
The JSON output of the `backup` command now includes `backup_start` and
|
||||||
|
`backup_end` timestamps, containing the start and end time of the backup.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5119
|
||||||
|
|
||||||
|
* Enhancement #5131: Add DragonFlyBSD support
|
||||||
|
|
||||||
|
Restic can now be compiled on DragonflyBSD.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5131
|
||||||
|
https://github.com/restic/restic/pull/5138
|
||||||
|
|
||||||
|
* Enhancement #5137: Make `tag` command print which snapshots were modified
|
||||||
|
|
||||||
|
The `tag` command now outputs which snapshots were modified along with their new
|
||||||
|
snapshot ID. The command supports the `--json` option for machine-readable
|
||||||
|
output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5137
|
||||||
|
https://github.com/restic/restic/pull/5144
|
||||||
|
|
||||||
|
* Enhancement #5141: Provide clear error message if AZURE_ACCOUNT_NAME is not set
|
||||||
|
|
||||||
|
If `AZURE_ACCOUNT_NAME` was not set, commands related to an Azure repository
|
||||||
|
would result in a misleading networking error. Restic now detect this and
|
||||||
|
provides a clear warning that the variable is not defined.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5141
|
||||||
|
|
||||||
|
* Enhancement #5173: Add experimental S3 cold storage support
|
||||||
|
|
||||||
|
Introduce S3 backend options for transitioning pack files from cold to hot
|
||||||
|
storage on S3 and S3-compatible providers. Note: this only works for the
|
||||||
|
`prune`, `copy` and `restore` commands for now.
|
||||||
|
|
||||||
|
This experimental feature is gated behind the "s3-restore" feature flag.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3202
|
||||||
|
https://github.com/restic/restic/issues/2504
|
||||||
|
https://github.com/restic/restic/pull/5173
|
||||||
|
|
||||||
|
* Enhancement #5174: Add xattr support for NetBSD 10+
|
||||||
|
|
||||||
|
Extended attribute support for `backup` and `restore` operations is now
|
||||||
|
available on NetBSD version 10 and later.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5174
|
||||||
|
https://github.com/restic/restic/pull/5180
|
||||||
|
|
||||||
|
* Enhancement #5251: Improve retry handling for flaky `rclone` backends
|
||||||
|
|
||||||
|
Since restic 0.17.0, the backend retry mechanisms rely on backends correctly
|
||||||
|
reporting when a file does not exist. This is not always the case for some
|
||||||
|
`rclone` backends, which caused restic to stop retrying after the first failure.
|
||||||
|
|
||||||
|
For rclone, failed requests are now retried up to 5 times before giving up.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5251
|
||||||
|
|
||||||
|
* Enhancement #5287: Make `recover` automatically rebuild index when needed
|
||||||
|
|
||||||
|
When trying to recover data from an interrupted snapshot, it was previously
|
||||||
|
necessary to manually run `repair index` before runnning `recover`. This now
|
||||||
|
happens automatically so that only `recover` is necessary.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5287
|
||||||
|
https://github.com/restic/restic/pull/5296
|
||||||
|
|
||||||
|
|
||||||
# Changelog for restic 0.17.3 (2024-11-08)
|
# Changelog for restic 0.17.3 (2024-11-08)
|
||||||
The following sections list the changes in restic 0.17.3 relevant to
|
The following sections list the changes in restic 0.17.3 relevant to
|
||||||
restic users. The changes are ordered by importance.
|
restic users. The changes are ordered by importance.
|
||||||
|
|||||||
@@ -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
|
to be ashamed of. In contrast, that happens regularly for all of us. That's
|
||||||
what the tests are there for.
|
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
|
Git Commits
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|||||||
33
build.go
33
build.go
@@ -36,7 +36,6 @@
|
|||||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
//go:build ignore_build_go
|
//go:build ignore_build_go
|
||||||
// +build ignore_build_go
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -53,12 +52,14 @@ import (
|
|||||||
|
|
||||||
// config contains the configuration for the program to build.
|
// config contains the configuration for the program to build.
|
||||||
var config = Config{
|
var config = Config{
|
||||||
Name: "restic", // name of the program executable and directory
|
Name: "restic", // name of the program executable and directory
|
||||||
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
Namespace: "github.com/restic/restic", // subdir of GOPATH, e.g. "github.com/foo/bar"
|
||||||
Main: "./cmd/restic", // package name for the main package
|
Main: "./cmd/restic", // package name for the main package
|
||||||
DefaultBuildTags: []string{"selfupdate"}, // specify build tags which are always used
|
// disable_grpc_modules is necessary to reduce the binary size since cloud.google.com/go/storage v1.44.0
|
||||||
Tests: []string{"./..."}, // tests to run
|
// see https://github.com/googleapis/google-cloud-go/issues/11448
|
||||||
MinVersion: GoVersion{Major: 1, Minor: 18, Patch: 0}, // minimum Go version supported
|
DefaultBuildTags: []string{"selfupdate", "disable_grpc_modules"}, // specify build tags which are always used
|
||||||
|
Tests: []string{"./..."}, // tests to run
|
||||||
|
MinVersion: GoVersion{Major: 1, Minor: 24, Patch: 0}, // minimum Go version supported
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config configures the build.
|
// Config configures the build.
|
||||||
@@ -298,19 +299,21 @@ func (v GoVersion) AtLeast(other GoVersion) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.Major > other.Major {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if v.Major < other.Major {
|
if v.Major < other.Major {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.Minor > other.Minor {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if v.Minor < other.Minor {
|
if v.Minor < other.Minor {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.Patch < other.Patch {
|
return v.Patch >= other.Patch
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v GoVersion) String() string {
|
func (v GoVersion) String() string {
|
||||||
@@ -380,12 +383,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
solarisMinVersion := GoVersion{Major: 1, Minor: 20, Patch: 0}
|
|
||||||
if env["GOARCH"] == "solaris" && !goVersion.AtLeast(solarisMinVersion) {
|
|
||||||
fmt.Fprintf(os.Stderr, "Detected version %s is too old, restic requires at least %s for Solaris\n", goVersion, solarisMinVersion)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
verbosePrintf("detected Go version %v\n", goVersion)
|
verbosePrintf("detected Go version %v\n", goVersion)
|
||||||
|
|
||||||
preserveSymbols := false
|
preserveSymbols := false
|
||||||
|
|||||||
7
changelog/0.18.0_2025-03-27/issue-1378
Normal file
7
changelog/0.18.0_2025-03-27/issue-1378
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Add JSON support to `check` command
|
||||||
|
|
||||||
|
The `check` command now supports the `--json` option to output all statistics in
|
||||||
|
JSON format.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/1378
|
||||||
|
https://github.com/restic/restic/pull/5194
|
||||||
7
changelog/0.18.0_2025-03-27/issue-1843
Normal file
7
changelog/0.18.0_2025-03-27/issue-1843
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Correctly restore long filepaths' timestamp on old Windows
|
||||||
|
|
||||||
|
The `restore` command now correctly restores timestamps for files with paths longer
|
||||||
|
than 256 characters on Windows versions prior to Windows 10 1607.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/1843
|
||||||
|
https://github.com/restic/restic/pull/5061
|
||||||
13
changelog/0.18.0_2025-03-27/issue-2165
Normal file
13
changelog/0.18.0_2025-03-27/issue-2165
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
Bugfix: Ignore disappeared backup source files
|
||||||
|
|
||||||
|
The `backup` command now quietly skips files that are removed between directory
|
||||||
|
listing and backup, instead of printing errors like:
|
||||||
|
|
||||||
|
```
|
||||||
|
error: lstat /some/file/name: no such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2165
|
||||||
|
https://github.com/restic/restic/issues/3098
|
||||||
|
https://github.com/restic/restic/pull/5143
|
||||||
|
https://github.com/restic/restic/pull/5145
|
||||||
7
changelog/0.18.0_2025-03-27/issue-2511
Normal file
7
changelog/0.18.0_2025-03-27/issue-2511
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Support generating shell completions to stdout
|
||||||
|
|
||||||
|
The `generate` command now supports using `-` as the filename with the
|
||||||
|
`--[shell]-completion` option to write the generated output to stdout.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/2511
|
||||||
|
https://github.com/restic/restic/pull/5053
|
||||||
11
changelog/0.18.0_2025-03-27/issue-3202
Normal file
11
changelog/0.18.0_2025-03-27/issue-3202
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Enhancement: Add experimental S3 cold storage support
|
||||||
|
|
||||||
|
Introduce S3 backend options for transitioning pack files from cold to hot storage
|
||||||
|
on S3 and S3-compatible providers. Note: this only works for the `prune`, `copy`
|
||||||
|
and `restore` commands for now.
|
||||||
|
|
||||||
|
This experimental feature is gated behind the "s3-restore" feature flag.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5173
|
||||||
|
https://github.com/restic/restic/issues/3202
|
||||||
|
https://github.com/restic/restic/issues/2504
|
||||||
12
changelog/0.18.0_2025-03-27/issue-3697
Normal file
12
changelog/0.18.0_2025-03-27/issue-3697
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Enhancement: Allow excluding online-only cloud files (e.g. OneDrive)
|
||||||
|
|
||||||
|
Restic treated files synced using OneDrive Files On-Demand as though they
|
||||||
|
were regular files. This caused issues with VSS and could cause OneDrive to
|
||||||
|
download all files.
|
||||||
|
|
||||||
|
Restic now allows the user to exclude these files when backing up with
|
||||||
|
the `--exclude-cloud-files` option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3697
|
||||||
|
https://github.com/restic/restic/issues/4935
|
||||||
|
https://github.com/restic/restic/pull/4990
|
||||||
8
changelog/0.18.0_2025-03-27/issue-4179
Normal file
8
changelog/0.18.0_2025-03-27/issue-4179
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Add `sort` option to `ls` command
|
||||||
|
|
||||||
|
The `ls -l` command output can now be sorted using the new `--sort <field>`
|
||||||
|
option for the fields `name`, `size`, `time` (same as `mtime`), `mtime`,
|
||||||
|
`atime`, `ctime` and `extension`. A `--reverse` option is also available.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4179
|
||||||
|
https://github.com/restic/restic/pull/5182
|
||||||
7
changelog/0.18.0_2025-03-27/issue-4433
Normal file
7
changelog/0.18.0_2025-03-27/issue-4433
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Change default sort order for `find` output
|
||||||
|
|
||||||
|
The `find` command now sorts snapshots from newest to oldest by default. The
|
||||||
|
previous oldest-to-newest order can be restored using the new `--reverse` option.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4433
|
||||||
|
https://github.com/restic/restic/pull/5184
|
||||||
12
changelog/0.18.0_2025-03-27/issue-4521
Normal file
12
changelog/0.18.0_2025-03-27/issue-4521
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Enhancement: Add support for Microsoft Blob Storage access tiers
|
||||||
|
|
||||||
|
The new `-o azure.access-tier=<tier>` option allows specifying the access tier
|
||||||
|
(`Hot`, `Cool` or `Cold`) for objects created in Microsoft Blob Storage. If
|
||||||
|
unspecified, the storage account's default tier is used.
|
||||||
|
|
||||||
|
There is no official `Archive` storage support in restic, use this option at
|
||||||
|
your own risk. To restore any data, it is necessary to manually warm up the
|
||||||
|
required data in the `Archive` tier.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4521
|
||||||
|
https://github.com/restic/restic/pull/5046
|
||||||
11
changelog/0.18.0_2025-03-27/issue-4942
Normal file
11
changelog/0.18.0_2025-03-27/issue-4942
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Enhancement: Add snapshot summary statistics to rewritten snapshots
|
||||||
|
|
||||||
|
The `rewrite` command now supports a `--snapshot-summary` option to add
|
||||||
|
statistics data to snapshots. Only two fields in the summary will be non-zero:
|
||||||
|
`TotalFilesProcessed` and `TotalBytesProcessed`.
|
||||||
|
|
||||||
|
For snapshots rewritten using the `--exclude` options, the summary
|
||||||
|
statistics are updated accordingly.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4942
|
||||||
|
https://github.com/restic/restic/pull/5185
|
||||||
6
changelog/0.18.0_2025-03-27/issue-4948
Normal file
6
changelog/0.18.0_2025-03-27/issue-4948
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Format exit errors as JSON when requested
|
||||||
|
|
||||||
|
Restic now formats error messages as JSON when the `--json` flag is used.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4948
|
||||||
|
https://github.com/restic/restic/pull/4952
|
||||||
10
changelog/0.18.0_2025-03-27/issue-4983
Normal file
10
changelog/0.18.0_2025-03-27/issue-4983
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Enhancement: Add SLSA provenance to GHCR container images
|
||||||
|
|
||||||
|
Restic's GitHub Container Registry (GHCR) image build workflow now includes
|
||||||
|
SLSA (Supply-chain Levels for Software Artifacts) provenance generation.
|
||||||
|
|
||||||
|
Please see the restic documentation for more information about verifying SLSA
|
||||||
|
provenance.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4983
|
||||||
|
https://github.com/restic/restic/pull/4999
|
||||||
7
changelog/0.18.0_2025-03-27/issue-5081
Normal file
7
changelog/0.18.0_2025-03-27/issue-5081
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Add retry mechanism for loading repository config
|
||||||
|
|
||||||
|
Restic now retries loading the repository config file when opening a repository.
|
||||||
|
The `init` command now also retries backend operations.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5081
|
||||||
|
https://github.com/restic/restic/pull/5095
|
||||||
8
changelog/0.18.0_2025-03-27/issue-5089
Normal file
8
changelog/0.18.0_2025-03-27/issue-5089
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Allow including/excluding extended file attributes during `restore`
|
||||||
|
|
||||||
|
The `restore` command now supports the `--exclude-xattr` and `--include-xattr`
|
||||||
|
options to control which extended file attributes will be restored. By default,
|
||||||
|
all attributes are restored.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5089
|
||||||
|
https://github.com/restic/restic/pull/5129
|
||||||
7
changelog/0.18.0_2025-03-27/issue-5092
Normal file
7
changelog/0.18.0_2025-03-27/issue-5092
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Show count of deleted files and directories during `restore`
|
||||||
|
|
||||||
|
The `restore` command now reports the number of deleted files and directories,
|
||||||
|
both in the regular output and in the `files_deleted` field of the JSON output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5092
|
||||||
|
https://github.com/restic/restic/pull/5100
|
||||||
7
changelog/0.18.0_2025-03-27/issue-5109
Normal file
7
changelog/0.18.0_2025-03-27/issue-5109
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Make small pack size configurable for `prune`
|
||||||
|
|
||||||
|
The `prune` command now supports the `--repack-smaller-than` option that
|
||||||
|
allows repacking pack files smaller than a specified size.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5109
|
||||||
|
https://github.com/restic/restic/pull/5183
|
||||||
6
changelog/0.18.0_2025-03-27/issue-5131
Normal file
6
changelog/0.18.0_2025-03-27/issue-5131
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Add DragonFlyBSD support
|
||||||
|
|
||||||
|
Restic can now be compiled on DragonflyBSD.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5131
|
||||||
|
https://github.com/restic/restic/pull/5138
|
||||||
8
changelog/0.18.0_2025-03-27/issue-5137
Normal file
8
changelog/0.18.0_2025-03-27/issue-5137
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Make `tag` command print which snapshots were modified
|
||||||
|
|
||||||
|
The `tag` command now outputs which snapshots were modified along with their
|
||||||
|
new snapshot ID. The command supports the `--json` option for machine-readable
|
||||||
|
output.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5137
|
||||||
|
https://github.com/restic/restic/pull/5144
|
||||||
7
changelog/0.18.0_2025-03-27/issue-5174
Normal file
7
changelog/0.18.0_2025-03-27/issue-5174
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Add xattr support for NetBSD 10+
|
||||||
|
|
||||||
|
Extended attribute support for `backup` and `restore` operations
|
||||||
|
is now available on NetBSD version 10 and later.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5174
|
||||||
|
https://github.com/restic/restic/pull/5180
|
||||||
16
changelog/0.18.0_2025-03-27/issue-5259
Normal file
16
changelog/0.18.0_2025-03-27/issue-5259
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
Bugfix: Fix rare crash in command output
|
||||||
|
|
||||||
|
Some commands could in rare cases crash when trying to print status messages
|
||||||
|
and request retries at the same time, resulting in an error like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
panic: runtime error: slice bounds out of range [468:156]
|
||||||
|
[...]
|
||||||
|
github.com/restic/restic/internal/ui/termstatus.(*lineWriter).Write(...)
|
||||||
|
/restic/internal/ui/termstatus/stdio_wrapper.go:36 +0x136
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5259
|
||||||
|
https://github.com/restic/restic/pull/5300
|
||||||
8
changelog/0.18.0_2025-03-27/issue-5287
Normal file
8
changelog/0.18.0_2025-03-27/issue-5287
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Make `recover` automatically rebuild index when needed
|
||||||
|
|
||||||
|
When trying to recover data from an interrupted snapshot, it was previously
|
||||||
|
necessary to manually run `repair index` before runnning `recover`. This now
|
||||||
|
happens automatically so that only `recover` is necessary.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5287
|
||||||
|
https://github.com/restic/restic/pull/5296
|
||||||
24
changelog/0.18.0_2025-03-27/issue-5291
Normal file
24
changelog/0.18.0_2025-03-27/issue-5291
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Security: Mitigate attack on content-defined chunking algorithm
|
||||||
|
|
||||||
|
Restic uses [Rabin Fingerprints](https://restic.net/blog/2015-09-12/restic-foundation1-cdc/)
|
||||||
|
for its content-defined chunker. The algorithm relies on a secret polynomial
|
||||||
|
to split files into chunks.
|
||||||
|
|
||||||
|
As shown in the paper "[Chunking Attacks on File Backup Services using Content-Defined Chunking](https://eprint.iacr.org/2025/532.pdf)"
|
||||||
|
by Boris Alexeev, Colin Percival and Yan X Zhang, an
|
||||||
|
attacker that can observe chunk sizes for a known file can derive the secret
|
||||||
|
polynomial. Knowledge of the polynomial might in some cases allow an attacker
|
||||||
|
to check whether certain large files are stored in a repository.
|
||||||
|
|
||||||
|
A practical attack is nevertheless hard as restic merges multiple chunks into
|
||||||
|
opaque pack files and by default processes multiple files in parallel. This
|
||||||
|
likely prevents an attacker from matching pack files to the attacker-known file
|
||||||
|
and thereby prevents the attack.
|
||||||
|
|
||||||
|
Despite the low chances of a practical attack, restic now has added mitigation
|
||||||
|
that randomizes how chunks are assembled into pack files. This prevents attackers
|
||||||
|
from guessing which chunks are part of a pack file and thereby prevents learning
|
||||||
|
the chunk sizes.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5291
|
||||||
|
https://github.com/restic/restic/pull/5295
|
||||||
9
changelog/0.18.0_2025-03-27/pull-4938
Normal file
9
changelog/0.18.0_2025-03-27/pull-4938
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Change: Update dependencies and require Go 1.23 or newer
|
||||||
|
|
||||||
|
We have updated all dependencies. Restic now requires Go 1.23 or newer to build.
|
||||||
|
|
||||||
|
This also disables support for TLS versions older than TLS 1.2. On Windows,
|
||||||
|
restic now requires at least Windows 10 or Windows Server 2016. On macOS,
|
||||||
|
restic now requires at least macOS 11 Big Sur.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4938
|
||||||
6
changelog/0.18.0_2025-03-27/pull-5054
Normal file
6
changelog/0.18.0_2025-03-27/pull-5054
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Enable compression for ZIP archives in `dump` command
|
||||||
|
|
||||||
|
The `dump` command now compresses ZIP archives using the DEFLATE algorithm,
|
||||||
|
reducing the size of exported archives.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5054
|
||||||
6
changelog/0.18.0_2025-03-27/pull-5119
Normal file
6
changelog/0.18.0_2025-03-27/pull-5119
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Add start and end timestamps to `backup` JSON output
|
||||||
|
|
||||||
|
The JSON output of the `backup` command now includes `backup_start` and
|
||||||
|
`backup_end` timestamps, containing the start and end time of the backup.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5119
|
||||||
7
changelog/0.18.0_2025-03-27/pull-5141
Normal file
7
changelog/0.18.0_2025-03-27/pull-5141
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Provide clear error message if AZURE_ACCOUNT_NAME is not set
|
||||||
|
|
||||||
|
If `AZURE_ACCOUNT_NAME` was not set, commands related to an Azure repository
|
||||||
|
would result in a misleading networking error. Restic now detect this and
|
||||||
|
provides a clear warning that the variable is not defined.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5141
|
||||||
7
changelog/0.18.0_2025-03-27/pull-5153
Normal file
7
changelog/0.18.0_2025-03-27/pull-5153
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Include root tree when searching using `find --tree`
|
||||||
|
|
||||||
|
The `restic find --tree` command did not find trees referenced by
|
||||||
|
`restic snapshot --json`. It now correctly includes the root tree
|
||||||
|
when searching.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5153
|
||||||
8
changelog/0.18.0_2025-03-27/pull-5162
Normal file
8
changelog/0.18.0_2025-03-27/pull-5162
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Change: Graduate feature flags
|
||||||
|
|
||||||
|
The `deprecate-legacy-index`, `deprecate-s3-legacy-layout`,
|
||||||
|
`explicit-s3-anonymous-auth` and `safe-forget-keep-tags` features are
|
||||||
|
now stable and can no longer be disabled. The corresponding feature flags
|
||||||
|
will be removed in restic 0.19.0.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5162
|
||||||
22
changelog/0.18.0_2025-03-27/pull-5170
Normal file
22
changelog/0.18.0_2025-03-27/pull-5170
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Bugfix: Prevent Windows VSS event log 8194 warnings for backup with fs snapshot
|
||||||
|
|
||||||
|
When running `backup` with the `--use-fs-snapshot` option in Windows with admin rights, event logs like
|
||||||
|
|
||||||
|
```
|
||||||
|
Volume Shadow Copy Service error: Unexpected error querying for the IVssWriterCallback interface. hr = 0x80070005, Access is denied.
|
||||||
|
. This is often caused by incorrect security settings in either the writer or requester process.
|
||||||
|
|
||||||
|
Operation:
|
||||||
|
Gathering Writer Data
|
||||||
|
|
||||||
|
Context:
|
||||||
|
Writer Class Id: {e8132975-6f93-4464-a53e-1050253ae220}
|
||||||
|
Writer Name: System Writer
|
||||||
|
Writer Instance ID: {54b151ac-d27d-4628-9cb0-2bc40959f50f}
|
||||||
|
```
|
||||||
|
|
||||||
|
are created several times even though the backup itself succeeds. This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5169
|
||||||
|
https://github.com/restic/restic/pull/5170
|
||||||
|
https://forum.restic.net/t/windows-shadow-copy-snapshot-vss-unexpected-provider-error/3674/2
|
||||||
8
changelog/0.18.0_2025-03-27/pull-5212
Normal file
8
changelog/0.18.0_2025-03-27/pull-5212
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: Fix duplicate data handling in `prune --max-unused`
|
||||||
|
|
||||||
|
The `prune --max-unused size` command did not correctly account for duplicate
|
||||||
|
data. If a repository contained a large amount of duplicate data, this could
|
||||||
|
previously result in pruning too little data. This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5212
|
||||||
|
https://forum.restic.net/t/restic-not-obeying-max-unused-parameter-on-prune/8879
|
||||||
10
changelog/0.18.0_2025-03-27/pull-5249
Normal file
10
changelog/0.18.0_2025-03-27/pull-5249
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Bugfix: Fix creation of oversized index by `repair index --read-all-packs`
|
||||||
|
|
||||||
|
Since restic 0.17.0, the new index created by `repair index --read-all-packs` was
|
||||||
|
written as a single large index. This significantly increased memory usage while
|
||||||
|
loading the index.
|
||||||
|
|
||||||
|
The index is now correctly split into multiple smaller indexes, and `repair index`
|
||||||
|
now also automatically splits oversized indexes.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5249
|
||||||
9
changelog/0.18.0_2025-03-27/pull-5251
Normal file
9
changelog/0.18.0_2025-03-27/pull-5251
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Enhancement: Improve retry handling for flaky `rclone` backends
|
||||||
|
|
||||||
|
Since restic 0.17.0, the backend retry mechanisms rely on backends correctly
|
||||||
|
reporting when a file does not exist. This is not always the case for some
|
||||||
|
`rclone` backends, which caused restic to stop retrying after the first failure.
|
||||||
|
|
||||||
|
For rclone, failed requests are now retried up to 5 times before giving up.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5251
|
||||||
14
changelog/0.18.1_2025-09-21/issue-5324
Normal file
14
changelog/0.18.1_2025-09-21/issue-5324
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Bugfix: Correctly handle `backup --stdin-filename` with directory paths
|
||||||
|
|
||||||
|
In restic 0.18.0, the `backup` command failed if a filename that includes
|
||||||
|
at least a directory was passed to `--stdin-filename`. For example,
|
||||||
|
`--stdin-filename /foo/bar` resulted in the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
Fatal: unable to save snapshot: open /foo: no such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5324
|
||||||
|
https://github.com/restic/restic/pull/5356
|
||||||
7
changelog/0.18.1_2025-09-21/issue-5325
Normal file
7
changelog/0.18.1_2025-09-21/issue-5325
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Accept `RESTIC_HOST` environment variable in `forget` command
|
||||||
|
|
||||||
|
The `forget` command did not use the host name from the `RESTIC_HOST`
|
||||||
|
environment variable when filtering snapshots. This has now been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5325
|
||||||
|
https://github.com/restic/restic/pull/5327
|
||||||
7
changelog/0.18.1_2025-09-21/issue-5342
Normal file
7
changelog/0.18.1_2025-09-21/issue-5342
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Ignore "chmod not supported" errors when writing files
|
||||||
|
|
||||||
|
Restic 0.18.0 introduced a bug that caused `chmod xxx: operation not supported`
|
||||||
|
errors to appear when writing to a local file repository that did not support
|
||||||
|
chmod (like CIFS or WebDAV mounted via FUSE). Restic now ignores those errors.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5342
|
||||||
7
changelog/0.18.1_2025-09-21/issue-5344
Normal file
7
changelog/0.18.1_2025-09-21/issue-5344
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Ignore `EOPNOTSUPP` errors for extended attributes
|
||||||
|
|
||||||
|
Restic 0.18.0 added extended attribute support for NetBSD 10+, but not all
|
||||||
|
NetBSD filesystems support extended attributes. Other BSD systems can
|
||||||
|
likewise return `EOPNOTSUPP`, so restic now ignores these errors.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5344
|
||||||
8
changelog/0.18.1_2025-09-21/issue-5429
Normal file
8
changelog/0.18.1_2025-09-21/issue-5429
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: Stop retrying uploads when rest-server runs out of space
|
||||||
|
|
||||||
|
When rest-server returns a `507 Insufficient Storage` error, it indicates
|
||||||
|
that no more storage capacity is available. Restic now correctly stops
|
||||||
|
retrying uploads in this case.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5429
|
||||||
|
https://github.com/restic/restic/pull/5452
|
||||||
27
changelog/0.18.1_2025-09-21/issue-5467
Normal file
27
changelog/0.18.1_2025-09-21/issue-5467
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Bugfix: Improve handling of download retries in `check` command
|
||||||
|
|
||||||
|
In very rare cases, the `check` command could unnecessarily report repository
|
||||||
|
damage if the backend returned incomplete, corrupted data on the first download
|
||||||
|
try which is afterwards resolved by a download retry.
|
||||||
|
|
||||||
|
This could result in an error output like the following:
|
||||||
|
|
||||||
|
```
|
||||||
|
Load(<data/34567890ab>, 33918928, 0) returned error, retrying after 871.35598ms: readFull: unexpected EOF
|
||||||
|
Load(<data/34567890ab>, 33918928, 0) operation successful after 1 retries
|
||||||
|
check successful on second attempt, original error pack 34567890ab[...] contains 6 errors: [blob 12345678[...]: decrypting blob <data/12345678> from 34567890 failed: ciphertext verification failed ...]
|
||||||
|
[...]
|
||||||
|
Fatal: repository contains errors
|
||||||
|
```
|
||||||
|
|
||||||
|
This fix only applies to a very specific case where the log shows
|
||||||
|
`operation successful after 1 retries` followed by a
|
||||||
|
`check successful on second attempt, original error` that only reports
|
||||||
|
`ciphertext verification failed` errors in the pack file. If any other errors
|
||||||
|
are reported in the pack file, then the repository still has to be considered
|
||||||
|
as damaged.
|
||||||
|
|
||||||
|
Now, only the check result of the last download retry is reported as intended.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5467
|
||||||
|
https://github.com/restic/restic/pull/5495
|
||||||
7
changelog/0.18.1_2025-09-21/pull-5421
Normal file
7
changelog/0.18.1_2025-09-21/pull-5421
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Fix rare crash if directory is removed during backup
|
||||||
|
|
||||||
|
In restic 0.18.0, the `backup` command could crash if a directory was removed
|
||||||
|
between reading its metadata and listing its directory content. This has now
|
||||||
|
been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5421
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
# The first line must start with Bugfix:, Enhancement: or Change:,
|
# The first line must start with Bugfix:, Enhancement: or Change:,
|
||||||
# including the colon. Use present tense and the imperative mood. Remove
|
# including the colon. 'Change:' is for breaking changes only.
|
||||||
# lines starting with '#' from this template.
|
# Documentation-only changes do not get a changelog entry.
|
||||||
Enhancement: Allow custom bar in the foo command
|
# 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
|
# Describe the problem in the past tense, the new behavior in the present
|
||||||
# tense. Mention the affected commands, backends, operating systems, etc.
|
# tense. Mention the affected commands, backends, operating systems, etc.
|
||||||
# Focus on user-facing behavior, not the implementation.
|
# If the problem description just says that a feature was missing, then
|
||||||
|
# only explain the new behavior. Aim for a short and concise description.
|
||||||
# Use "Restic now ..." instead of "We have changed ...".
|
# 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
|
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`
|
item in the `baz` backend. It now permits selecting the bar with `--bar`
|
||||||
|
|||||||
9
changelog/unreleased/issue-3326
Normal file
9
changelog/unreleased/issue-3326
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Enhancement: `restic check` for specified snapshot(s) via snapshot filtering
|
||||||
|
|
||||||
|
Snapshots can now be specified for the command `restic check` on the command line
|
||||||
|
via the standard snapshot filter, (`--tag`, `--host`, `--path` or specifying
|
||||||
|
snapshot IDs directly) and will be used for checking the packfiles used by these snapshots.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3326
|
||||||
|
https://github.com/restic/restic/pull/5469
|
||||||
|
https://github.com/restic/restic/pull/5644
|
||||||
9
changelog/unreleased/issue-3572
Normal file
9
changelog/unreleased/issue-3572
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Enhancement: Support restoring ownership by name on UNIX systems
|
||||||
|
|
||||||
|
Restic restore used to restore file ownership on UNIX systems by UID and GID.
|
||||||
|
It now allows restoring the file ownership by user name and group name with `--ownership-by-name`.
|
||||||
|
This allows restoring snapshots on a system where the UID/GID are not the same as they were on the system where the snapshot was created.
|
||||||
|
However it does not include support for POSIX ACLs, which are still restored by their numeric value.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3572
|
||||||
|
https://github.com/restic/restic/pull/5449
|
||||||
8
changelog/unreleased/issue-3738
Normal file
8
changelog/unreleased/issue-3738
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Allow Github personal access token to be specified for `self-update`
|
||||||
|
|
||||||
|
`restic self-update` previously only used unauthenticated GitHub API requests when checking for the latest release. This caused some users sharing IP addresses to hit the GitHub rate limit, resulting in a 403 Forbidden error and preventing updates.
|
||||||
|
|
||||||
|
Restic still uses unauthenticated requests by default, but it now optionally supports authenticated GitHub API requests during `self-update`. Users can set the `$GITHUB_ACCESS_TOKEN` environment variable to use a [personal access token](https://github.com/settings/tokens) for this effect, avoiding update failures due to rate limiting.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/3738
|
||||||
|
https://github.com/restic/restic/pull/5568
|
||||||
12
changelog/unreleased/issue-4278
Normal file
12
changelog/unreleased/issue-4278
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Enhancement: Support include filters in `rewrite` command
|
||||||
|
|
||||||
|
The enhancement enables the standard include filter options
|
||||||
|
--iinclude pattern same as --include pattern but ignores the casing of filenames
|
||||||
|
--iinclude-file file same as --include-file but ignores casing of filenames in patterns
|
||||||
|
-i, --include pattern include a pattern (can be specified multiple times)
|
||||||
|
--include-file file read include patterns from a file (can be specified multiple times)
|
||||||
|
|
||||||
|
The exclusion or inclusion of filter parameters is exclusive, as in other commands.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4278
|
||||||
|
https://github.com/restic/restic/pull/5191
|
||||||
11
changelog/unreleased/issue-4467
Normal file
11
changelog/unreleased/issue-4467
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Bugfix: Exit with code 3 when some `backup` source files do not exist
|
||||||
|
|
||||||
|
Restic used to exit with code 0 even when some backup sources did not exist. Restic
|
||||||
|
would exit with code 3 only when child directories or files did not exist. This
|
||||||
|
could cause confusion and unexpected behavior in scripts that relied on the exit
|
||||||
|
code to determine if the backup was successful.
|
||||||
|
|
||||||
|
Restic now exits with code 3 when some backup sources do not exist.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4467
|
||||||
|
https://github.com/restic/restic/pull/5347
|
||||||
7
changelog/unreleased/issue-4728
Normal file
7
changelog/unreleased/issue-4728
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: Added support for zstd compression levels `fastest` and `better`
|
||||||
|
|
||||||
|
Restic now supports the zstd compression modes `fastest` and `better`. Set the
|
||||||
|
environment variable `RESTIC_COMPRESSION` to `fastest` or `better` to use these
|
||||||
|
compression levels. This can also be set with the `--compression` flag.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4728
|
||||||
14
changelog/unreleased/issue-4868
Normal file
14
changelog/unreleased/issue-4868
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Enhancement: Include repository id in filesystem name used by `mount`
|
||||||
|
|
||||||
|
The filesystem created by restic's `mount` command now includes the repository
|
||||||
|
id in the filesystem name. The repository id is printed by restic when opening
|
||||||
|
a repository or can be looked up using `restic cat config`.
|
||||||
|
|
||||||
|
```
|
||||||
|
[restic-user@hostname restic]$ df ./test-mount/
|
||||||
|
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||||
|
restic:d3b07384d1 0 0 0 - /mnt/my-restic-repo
|
||||||
|
```
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4868
|
||||||
|
https://github.com/restic/restic/pull/5243
|
||||||
8
changelog/unreleased/issue-5233
Normal file
8
changelog/unreleased/issue-5233
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: forget command returns exit code 3 on partial removal of snapshots
|
||||||
|
|
||||||
|
The `forget` command now returns exit code 3 when it fails to remove one or
|
||||||
|
more snapshots. Previously, it returned exit code 0, which could lead to
|
||||||
|
confusion if the command was used in a script.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5233
|
||||||
|
https://github.com/restic/restic/pull/5322
|
||||||
7
changelog/unreleased/issue-5258
Normal file
7
changelog/unreleased/issue-5258
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Exit with correct code on SIGINT
|
||||||
|
|
||||||
|
Restic previously returned exit code 1 on SIGINT, which is incorrect.
|
||||||
|
Restic now returns 130 on SIGINT.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5258
|
||||||
|
https://github.com/restic/restic/pull/5363
|
||||||
7
changelog/unreleased/issue-5280
Normal file
7
changelog/unreleased/issue-5280
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: `restic find` now checks for correct ordering of time related options
|
||||||
|
|
||||||
|
`restic find` now immediately fails with an error if both `--oldest` and `--newest` are specified
|
||||||
|
and `--oldest` is a timestamp after `--newest`.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5280
|
||||||
|
https://github.com/restic/restic/pull/5310
|
||||||
11
changelog/unreleased/issue-5352
Normal file
11
changelog/unreleased/issue-5352
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Enhancement: Add support for --exclude-cloud-files on macOS (e.g. iCloud drive)
|
||||||
|
|
||||||
|
Restic treated files stored in iCloud drive as though they were regular files.
|
||||||
|
This caused restic to download all files (including files marked as cloud only) while iterating over them.
|
||||||
|
|
||||||
|
Restic now allows the user to exclude these files when backing up with the `--exclude-cloud-files` option.
|
||||||
|
|
||||||
|
Works from Sonoma (macOS 14.0) onwards. Older macOS versions materialize files when `stat` is called on the file.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/4990
|
||||||
|
https://github.com/restic/restic/issues/5352
|
||||||
14
changelog/unreleased/issue-5354
Normal file
14
changelog/unreleased/issue-5354
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Bugfix: Allow use of rclone/sftp backend when running restic in background
|
||||||
|
|
||||||
|
When starting restic in the background, this could result in unexpected behavior
|
||||||
|
when using the rclone or sftp backend.
|
||||||
|
|
||||||
|
For example running `restic -r rclone:./example --insecure-no-password init &`
|
||||||
|
could cause the calling `bash` shell to exit unexpectedly.
|
||||||
|
|
||||||
|
This has 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
|
||||||
10
changelog/unreleased/issue-5383
Normal file
10
changelog/unreleased/issue-5383
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Enhancement: Reduce progress bar refresh rates to reduce energy usage
|
||||||
|
|
||||||
|
Progress bars were updated with 60fps which can cause high CPU or GPU usage
|
||||||
|
for some terminal emulators. Reduce it to 10fps to conserve energy.
|
||||||
|
In addition, this lower frequency seem to be necessary to allow selecting
|
||||||
|
anything in the terminal with certain terminal emulators.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5383
|
||||||
|
https://github.com/restic/restic/pull/5551
|
||||||
|
https://github.com/restic/restic/pull/5626
|
||||||
12
changelog/unreleased/issue-5440
Normal file
12
changelog/unreleased/issue-5440
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Enhancement: Allow overriding RESTIC_HOST environment variable with --host flag
|
||||||
|
|
||||||
|
When the `RESTIC_HOST` environment variable was set, there was no way to list or
|
||||||
|
operate on snapshots from all hosts, as the environment variable would always
|
||||||
|
filter to that specific host. Restic now allows overriding `RESTIC_HOST` by
|
||||||
|
explicitly providing the `--host` flag with an empty string (e.g., `--host=""` or
|
||||||
|
`--host=`), which will show snapshots from all hosts. This works for all commands
|
||||||
|
that support snapshot filtering: `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
|
||||||
10
changelog/unreleased/issue-5453
Normal file
10
changelog/unreleased/issue-5453
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Enhancement: `copy` copies snapshots in batches
|
||||||
|
|
||||||
|
The `copy` command used to copy snapshots individually, even if this resulted in creating pack files
|
||||||
|
smaller than the target pack size. In particular, this resulted in many small files
|
||||||
|
when copying small incremental snapshots.
|
||||||
|
|
||||||
|
Now, `copy` copies multiple snapshots at once to avoid creating small files.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5175
|
||||||
|
https://github.com/restic/restic/pull/5464
|
||||||
7
changelog/unreleased/issue-5477
Normal file
7
changelog/unreleased/issue-5477
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Password prompt was sometimes not shown
|
||||||
|
|
||||||
|
The password prompt for a repository was sometimes not shown when running
|
||||||
|
the `backup -v` command. This has been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5477
|
||||||
|
https://github.com/restic/restic/pull/5554
|
||||||
8
changelog/unreleased/issue-5487
Normal file
8
changelog/unreleased/issue-5487
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: Mark files as readonly when using the SFTP backend
|
||||||
|
|
||||||
|
Files created by the SFTP backend previously allowed writes to those files.
|
||||||
|
Restic now restricts the file permissions on SFTP backend to readonly.
|
||||||
|
This change only has an effect for sftp servers with support for the chmod operation.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5487
|
||||||
|
https://github.com/restic/restic/pull/5497
|
||||||
15
changelog/unreleased/issue-5531
Normal file
15
changelog/unreleased/issue-5531
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Enhancement: Reduce Azure storage costs by optimizing upload method
|
||||||
|
|
||||||
|
Restic previously used Azure's PutBlock and PutBlockList APIs for all file
|
||||||
|
uploads, which resulted in two transactions per file and doubled the storage
|
||||||
|
operation costs. For backups with many pack files, this could lead to
|
||||||
|
significant Azure storage transaction fees.
|
||||||
|
|
||||||
|
Restic now uses the more efficient PutBlob API for files up to 256 MiB,
|
||||||
|
requiring only a single transaction per file. This reduces Azure storage
|
||||||
|
operation costs by approximately 50% for typical backup workloads. Files
|
||||||
|
larger than 256 MiB continue to use the block-based upload method as required
|
||||||
|
by Azure's API limits.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5531
|
||||||
|
https://github.com/restic/restic/pull/5544
|
||||||
7
changelog/unreleased/issue-5586
Normal file
7
changelog/unreleased/issue-5586
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: correctly handle `snapshots --group-by` in combination with `--latest`
|
||||||
|
|
||||||
|
For the `snapshots` command, the `--latest` option did not correctly handle the
|
||||||
|
case where an non-default value was passed to `--group-by`. This has been fixed.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5586
|
||||||
|
https://github.com/restic/restic/pull/5601
|
||||||
8
changelog/unreleased/issue-5595
Normal file
8
changelog/unreleased/issue-5595
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Bugfix: Fix "chmod not supported" errors when unlocking
|
||||||
|
|
||||||
|
Restic 0.18.0 introduced a bug that caused "chmod xxx: operation not supported"
|
||||||
|
errors to appear when unlocking with a stale lock, on a local file repository
|
||||||
|
that did not support chmod (like CIFS or WebDAV mounted via FUSE). Restic now
|
||||||
|
just doesn't bother calling chmod in that case on Unix, as it is unnecessary.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5595
|
||||||
5
changelog/unreleased/pull-4938
Normal file
5
changelog/unreleased/pull-4938
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Change: Update dependencies and require Go 1.24 or newer
|
||||||
|
|
||||||
|
We have updated all dependencies. Restic now requires Go 1.24 or newer to build.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5619
|
||||||
9
changelog/unreleased/pull-5319
Normal file
9
changelog/unreleased/pull-5319
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Enhancement: add more status counters to `restic copy`
|
||||||
|
|
||||||
|
`restic copy` now produces more status counters in text format. The new counters
|
||||||
|
are the number of blobs to copy, their size on disk and the number of packfiles
|
||||||
|
used from the source repository. The additional statistics is only produced when
|
||||||
|
the `--verbose` option is specified.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5175
|
||||||
|
https://github.com/restic/restic/pull/5319
|
||||||
11
changelog/unreleased/pull-5424
Normal file
11
changelog/unreleased/pull-5424
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Enhancement: Enable file system privileges on Windows before access
|
||||||
|
|
||||||
|
Restic attempted to enable Windows file system privileges when
|
||||||
|
reading or writing security descriptors - after potentially being wholly
|
||||||
|
denied access to previous items. It also read file extended attributes without
|
||||||
|
using the privilege, possibly missing them and producing errors.
|
||||||
|
|
||||||
|
Restic now attempts to enable all file system privileges before any file
|
||||||
|
access. It also requests extended attribute reads use the backup privilege.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5424
|
||||||
11
changelog/unreleased/pull-5448
Normal file
11
changelog/unreleased/pull-5448
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Enhancement: Allow nice and ionice configuration for restic containers
|
||||||
|
|
||||||
|
The official restic docker now supports the following environment variables:
|
||||||
|
|
||||||
|
`NICE`: set the desired nice scheduling. See `man nice`.
|
||||||
|
`IONICE_CLASS`: set the desired I/O scheduling class. See `man ionice`. Note that real time support requires the invoker to manually add the `SYS_NICE` capability.
|
||||||
|
`IONICE_PRIORITY`: set the prioritization for ionice in the given `IONICE_CLASS`. This does nothing without `IONICE_CLASS`, but defaults to `4` (no priority, no penalties).
|
||||||
|
|
||||||
|
See https://restic.readthedocs.io/en/stable/020_installation.html#docker-container for further details.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5448
|
||||||
10
changelog/unreleased/pull-5465
Normal file
10
changelog/unreleased/pull-5465
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Bugfix: Correctly restore ACL inheritance state on Windows
|
||||||
|
|
||||||
|
Since the introduction of Security Descriptor backups in restic 0.17.0, the inheritance property of Access Control Entries (ACEs) was not restored correctly. This resulted in all restored permissions being marked as explicit (IsInherited: False), even if they were originally inherited from a parent folder.
|
||||||
|
|
||||||
|
The issue was caused by sending conflicting inheritance flags (PROTECTED_... and UNPROTECTED_...) to the Windows API during the restore process. The API would default to the more restrictive PROTECTED state, effectively disabling inheritance.
|
||||||
|
|
||||||
|
This has been fixed by ensuring that only the correct, non-conflicting inheritance flag is used when applying the security descriptor, preserving the original permission structure from the backup.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5465
|
||||||
|
https://github.com/restic/restic/issues/5427
|
||||||
6
changelog/unreleased/pull-5523
Normal file
6
changelog/unreleased/pull-5523
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Enhancement: Add OpenContainers labels to Dockerfile.release
|
||||||
|
|
||||||
|
The restic Docker image now includes labels from the OpenContainers Annotations Spec.
|
||||||
|
This information can be used by third party services.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5523
|
||||||
10
changelog/unreleased/pull-5588
Normal file
10
changelog/unreleased/pull-5588
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
Enhancement: Display timezone information in snapshots output
|
||||||
|
|
||||||
|
The `snapshots` command now displays which timezone is being used to show
|
||||||
|
timestamps. Since snapshots can be created in different timezones but are
|
||||||
|
always displayed in the local timezone, a footer line is now shown indicating
|
||||||
|
the timezone used for display (e.g., "Timestamps shown in CET timezone").
|
||||||
|
This helps prevent confusion when comparing snapshots in a multi-user
|
||||||
|
environment.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5588
|
||||||
7
changelog/unreleased/pull-5592
Normal file
7
changelog/unreleased/pull-5592
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Bugfix: Return error if `RESTIC_PACK_SIZE` contains invalid value
|
||||||
|
|
||||||
|
If the environment variable `RESTIC_PACK_SIZE` could not be parsed, then
|
||||||
|
restic ignored its value. Now, the restic commands fail with an error, unless
|
||||||
|
the command-line option `--pack-size` was specified.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5592
|
||||||
7
changelog/unreleased/pull-5610
Normal file
7
changelog/unreleased/pull-5610
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Enhancement: reduce memory usage of check/copy/diff/stats commands
|
||||||
|
|
||||||
|
We have optimized the memory usage of the `check`, `copy`, `diff` and
|
||||||
|
`stats` commands. These now require less memory when processing large
|
||||||
|
snapshots.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5610
|
||||||
6
changelog/unreleased/pull-5664
Normal file
6
changelog/unreleased/pull-5664
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Bugfix: restic find --pack <tree-pack> did not produce output for tree packs
|
||||||
|
|
||||||
|
`restic find --pack` now produces output for a tree related packfile.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/5280
|
||||||
|
https://github.com/restic/restic/pull/5664
|
||||||
9
changelog/unreleased/pull-5718
Normal file
9
changelog/unreleased/pull-5718
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Enhancement: stricter early mountpoint validation in `mount`
|
||||||
|
|
||||||
|
`restic mount` accepted parameters that would lead to a FUSE mount operation
|
||||||
|
failing after having done computationally intensive work to prepare the mount.
|
||||||
|
The `mountpoint` argument supplied must now refer to the name of a directory
|
||||||
|
that the current user can access and write to, otherwise `restic mount` will
|
||||||
|
exit with an error before interacting with the repository.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/pull/5718
|
||||||
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -9,26 +11,27 @@ import (
|
|||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createGlobalContext() context.Context {
|
func createGlobalContext(stderr io.Writer) context.Context {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
ch := make(chan os.Signal, 1)
|
ch := make(chan os.Signal, 1)
|
||||||
go cleanupHandler(ch, cancel)
|
go cleanupHandler(ch, cancel, stderr)
|
||||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanupHandler handles the SIGINT and SIGTERM signals.
|
// 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
|
s := <-c
|
||||||
debug.Log("signal %v received, cleaning up", s)
|
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 != "" {
|
if val, _ := os.LookupEnv("RESTIC_DEBUG_STACKTRACE_SIGINT"); val != "" {
|
||||||
_, _ = os.Stderr.WriteString("\n--- STACKTRACE START ---\n\n")
|
_, _ = stderr.Write([]byte("\n--- STACKTRACE START ---\n\n"))
|
||||||
_, _ = os.Stderr.WriteString(debug.DumpStacktrace())
|
_, _ = stderr.Write([]byte(debug.DumpStacktrace()))
|
||||||
_, _ = os.Stderr.WriteString("\n--- STACKTRACE END ---\n")
|
_, _ = stderr.Write([]byte("\n--- STACKTRACE END ---\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
|||||||
@@ -15,23 +15,30 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/archiver"
|
"github.com/restic/restic/internal/archiver"
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/filter"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/textfile"
|
"github.com/restic/restic/internal/textfile"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/backup"
|
"github.com/restic/restic/internal/ui/backup"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdBackup = &cobra.Command{
|
func newBackupCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "backup [flags] [FILE/DIR] ...",
|
var opts BackupOptions
|
||||||
Short: "Create a new backup of files and/or directories",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "backup [flags] [FILE/DIR] ...",
|
||||||
|
Short: "Create a new backup of files and/or directories",
|
||||||
|
Long: `
|
||||||
The "backup" command creates a new snapshot and saves the files and directories
|
The "backup" command creates a new snapshot and saves the files and directories
|
||||||
given as the arguments.
|
given as the arguments.
|
||||||
|
|
||||||
@@ -45,40 +52,43 @@ Exit status is 10 if the repository does not exist.
|
|||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
PreRun: func(_ *cobra.Command, _ []string) {
|
PreRun: func(_ *cobra.Command, _ []string) {
|
||||||
if backupOptions.Host == "" {
|
if opts.Host == "" {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.Log("os.Hostname() returned err: %v", err)
|
debug.Log("os.Hostname() returned err: %v", err)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
opts.Host = hostname
|
||||||
}
|
}
|
||||||
backupOptions.Host = hostname
|
},
|
||||||
}
|
GroupID: cmdGroupDefault,
|
||||||
},
|
DisableAutoGenTag: true,
|
||||||
GroupID: cmdGroupDefault,
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
DisableAutoGenTag: true,
|
return runBackup(cmd.Context(), opts, *globalOptions, globalOptions.Term, args)
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
},
|
||||||
term, cancel := setupTermstatus()
|
}
|
||||||
defer cancel()
|
|
||||||
return runBackup(cmd.Context(), backupOptions, globalOptions, term, args)
|
opts.AddFlags(cmd.Flags())
|
||||||
},
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackupOptions bundles all options for the backup command.
|
// BackupOptions bundles all options for the backup command.
|
||||||
type BackupOptions struct {
|
type BackupOptions struct {
|
||||||
excludePatternOptions
|
filter.ExcludePatternOptions
|
||||||
|
|
||||||
Parent string
|
Parent string
|
||||||
GroupBy restic.SnapshotGroupByOptions
|
GroupBy data.SnapshotGroupByOptions
|
||||||
Force bool
|
Force bool
|
||||||
ExcludeOtherFS bool
|
ExcludeOtherFS bool
|
||||||
ExcludeIfPresent []string
|
ExcludeIfPresent []string
|
||||||
ExcludeCaches bool
|
ExcludeCaches bool
|
||||||
ExcludeLargerThan string
|
ExcludeLargerThan string
|
||||||
|
ExcludeCloudFiles bool
|
||||||
Stdin bool
|
Stdin bool
|
||||||
StdinFilename string
|
StdinFilename string
|
||||||
StdinCommand bool
|
StdinCommand bool
|
||||||
Tags restic.TagLists
|
Tags data.TagLists
|
||||||
Host string
|
Host string
|
||||||
FilesFrom []string
|
FilesFrom []string
|
||||||
FilesFromVerbatim []string
|
FilesFromVerbatim []string
|
||||||
@@ -94,70 +104,72 @@ type BackupOptions struct {
|
|||||||
SkipIfUnchanged bool
|
SkipIfUnchanged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupOptions BackupOptions
|
func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
var backupFSTestHook func(fs fs.FS) fs.FS
|
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 = 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)`)
|
||||||
|
|
||||||
// ErrInvalidSourceData is used to report an incomplete backup
|
opts.ExcludePatternOptions.Add(f)
|
||||||
var ErrInvalidSourceData = errors.New("at least one source file could not be read")
|
|
||||||
|
|
||||||
func init() {
|
f.BoolVarP(&opts.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems, don't cross filesystem boundaries and subvolumes")
|
||||||
cmdRoot.AddCommand(cmdBackup)
|
f.StringArrayVar(&opts.ExcludeIfPresent, "exclude-if-present", nil, "takes `filename[:header]`, exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
||||||
|
f.BoolVar(&opts.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
|
||||||
f := cmdBackup.Flags()
|
f.StringVar(&opts.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
|
||||||
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)")
|
f.BoolVar(&opts.Stdin, "stdin", false, "read backup from stdin")
|
||||||
backupOptions.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
f.StringVar(&opts.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
|
||||||
f.VarP(&backupOptions.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
|
f.BoolVar(&opts.StdinCommand, "stdin-from-command", false, "interpret arguments as command to execute and store its stdout")
|
||||||
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the source files/directories (overrides the "parent" flag)`)
|
f.Var(&opts.Tags, "tag", "add `tags` for the new snapshot in the format `tag[,tag,...]` (can be specified multiple times)")
|
||||||
|
f.UintVar(&opts.ReadConcurrency, "read-concurrency", 0, "read `n` files concurrently (default: $RESTIC_READ_CONCURRENCY or 2)")
|
||||||
initExcludePatternOptions(f, &backupOptions.excludePatternOptions)
|
f.StringVarP(&opts.Host, "host", "H", "", "set the `hostname` for the snapshot manually (default: $RESTIC_HOST). To prevent an expensive rescan use the \"parent\" flag")
|
||||||
|
f.StringVar(&opts.Host, "hostname", "", "set the `hostname` for the snapshot manually")
|
||||||
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems, don't cross filesystem boundaries and subvolumes")
|
|
||||||
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes `filename[:header]`, exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
|
|
||||||
f.BoolVar(&backupOptions.ExcludeCaches, "exclude-caches", false, `excludes cache directories that are marked with a CACHEDIR.TAG file. See https://bford.info/cachedir/ for the Cache Directory Tagging Standard`)
|
|
||||||
f.StringVar(&backupOptions.ExcludeLargerThan, "exclude-larger-than", "", "max `size` of the files to be backed up (allowed suffixes: k/K, m/M, g/G, t/T)")
|
|
||||||
f.BoolVar(&backupOptions.Stdin, "stdin", false, "read backup from stdin")
|
|
||||||
f.StringVar(&backupOptions.StdinFilename, "stdin-filename", "stdin", "`filename` to use when reading from stdin")
|
|
||||||
f.BoolVar(&backupOptions.StdinCommand, "stdin-from-command", false, "interpret arguments as command to execute and store its stdout")
|
|
||||||
f.Var(&backupOptions.Tags, "tag", "add `tags` for the new snapshot in the format `tag[,tag,...]` (can be specified multiple times)")
|
|
||||||
f.UintVar(&backupOptions.ReadConcurrency, "read-concurrency", 0, "read `n` files concurrently (default: $RESTIC_READ_CONCURRENCY or 2)")
|
|
||||||
f.StringVarP(&backupOptions.Host, "host", "H", "", "set the `hostname` for the snapshot manually (default: $RESTIC_HOST). To prevent an expensive rescan use the \"parent\" flag")
|
|
||||||
f.StringVar(&backupOptions.Host, "hostname", "", "set the `hostname` for the snapshot manually")
|
|
||||||
err := f.MarkDeprecated("hostname", "use --host")
|
err := f.MarkDeprecated("hostname", "use --host")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// MarkDeprecated only returns an error when the flag could not be found
|
// MarkDeprecated only returns an error when the flag could not be found
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
f.StringArrayVar(&backupOptions.FilesFrom, "files-from", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
f.StringArrayVar(&opts.FilesFrom, "files-from", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
||||||
f.StringArrayVar(&backupOptions.FilesFromVerbatim, "files-from-verbatim", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
f.StringArrayVar(&opts.FilesFromVerbatim, "files-from-verbatim", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
||||||
f.StringArrayVar(&backupOptions.FilesFromRaw, "files-from-raw", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
f.StringArrayVar(&opts.FilesFromRaw, "files-from-raw", nil, "read the files to backup from `file` (can be combined with file args; can be specified multiple times)")
|
||||||
f.StringVar(&backupOptions.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
|
f.StringVar(&opts.TimeStamp, "time", "", "`time` of the backup (ex. '2012-11-01 22:08:41') (default: now)")
|
||||||
f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories")
|
f.BoolVar(&opts.WithAtime, "with-atime", false, "store the atime for all files and directories")
|
||||||
f.BoolVar(&backupOptions.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files")
|
f.BoolVar(&opts.IgnoreInode, "ignore-inode", false, "ignore inode number and ctime changes when checking for modified files")
|
||||||
f.BoolVar(&backupOptions.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files")
|
f.BoolVar(&opts.IgnoreCtime, "ignore-ctime", false, "ignore ctime changes when checking for modified files")
|
||||||
f.BoolVarP(&backupOptions.DryRun, "dry-run", "n", false, "do not upload or write any data, just show what would be done")
|
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not upload or write any data, just show what would be done")
|
||||||
f.BoolVar(&backupOptions.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
|
f.BoolVar(&opts.NoScan, "no-scan", false, "do not run scanner to estimate size of backup")
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
f.BoolVar(&backupOptions.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
|
f.BoolVar(&opts.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
|
||||||
}
|
}
|
||||||
f.BoolVar(&backupOptions.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot")
|
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
|
// parse read concurrency from env, on error the default value will be used
|
||||||
readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32)
|
readConcurrency, _ := strconv.ParseUint(os.Getenv("RESTIC_READ_CONCURRENCY"), 10, 32)
|
||||||
backupOptions.ReadConcurrency = uint(readConcurrency)
|
opts.ReadConcurrency = uint(readConcurrency)
|
||||||
|
|
||||||
// parse host from env, if not exists or empty the default value will be used
|
// parse host from env, if not exists or empty the default value will be used
|
||||||
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||||
backupOptions.Host = host
|
opts.Host = host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var backupFSTestHook func(fs fs.FS) fs.FS
|
||||||
|
|
||||||
|
// ErrInvalidSourceData is used to report an incomplete backup
|
||||||
|
var ErrInvalidSourceData = errors.New("at least one source file could not be read")
|
||||||
|
|
||||||
|
// ErrNoSourceData is used to report that no source data was found
|
||||||
|
var ErrNoSourceData = errors.Fatal("all source directories/files do not exist")
|
||||||
|
|
||||||
// filterExisting returns a slice of all existing items, or an error if no
|
// filterExisting returns a slice of all existing items, or an error if no
|
||||||
// items exist at all.
|
// items exist at all.
|
||||||
func filterExisting(items []string) (result []string, err error) {
|
func filterExisting(items []string, warnf func(msg string, args ...interface{})) (result []string, err error) {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
_, err := fs.Lstat(item)
|
_, err := fs.Lstat(item)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
Warnf("%v does not exist, skipping\n", item)
|
warnf("%v does not exist, skipping\n", item)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +177,12 @@ func filterExisting(items []string) (result []string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(result) == 0 {
|
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
|
// readLines reads all lines from the named file and returns them as a
|
||||||
@@ -177,7 +191,7 @@ func filterExisting(items []string) (result []string, err error) {
|
|||||||
// If filename is empty, readPatternsFromFile returns an empty slice.
|
// If filename is empty, readPatternsFromFile returns an empty slice.
|
||||||
// If filename is a dash (-), readPatternsFromFile will read the lines from the
|
// If filename is a dash (-), readPatternsFromFile will read the lines from the
|
||||||
// standard input.
|
// standard input.
|
||||||
func readLines(filename string) ([]string, error) {
|
func readLines(filename string, stdin io.ReadCloser) ([]string, error) {
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -188,7 +202,7 @@ func readLines(filename string) ([]string, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if filename == "-" {
|
if filename == "-" {
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(stdin)
|
||||||
} else {
|
} else {
|
||||||
data, err = textfile.Read(filename)
|
data, err = textfile.Read(filename)
|
||||||
}
|
}
|
||||||
@@ -213,8 +227,8 @@ func readLines(filename string) ([]string, error) {
|
|||||||
// readFilenamesFromFileRaw reads a list of filenames from the given file,
|
// readFilenamesFromFileRaw reads a list of filenames from the given file,
|
||||||
// or stdin if filename is "-". Each filename is terminated by a zero byte,
|
// or stdin if filename is "-". Each filename is terminated by a zero byte,
|
||||||
// which is stripped off.
|
// which is stripped off.
|
||||||
func readFilenamesFromFileRaw(filename string) (names []string, err error) {
|
func readFilenamesFromFileRaw(filename string, stdin io.ReadCloser) (names []string, err error) {
|
||||||
f := os.Stdin
|
f := stdin
|
||||||
if filename != "-" {
|
if filename != "-" {
|
||||||
if f, err = os.Open(filename); err != nil {
|
if f, err = os.Open(filename); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -263,8 +277,8 @@ func readFilenamesRaw(r io.Reader) (names []string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check returns an error when an invalid combination of options was set.
|
// Check returns an error when an invalid combination of options was set.
|
||||||
func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
func (opts BackupOptions) Check(gopts global.Options, args []string) error {
|
||||||
if gopts.password == "" && !gopts.InsecureNoPassword {
|
if gopts.Password == "" && !gopts.InsecureNoPassword {
|
||||||
if opts.Stdin {
|
if opts.Stdin {
|
||||||
return errors.Fatal("cannot read both password and data from stdin")
|
return errors.Fatal("cannot read both password and data from stdin")
|
||||||
}
|
}
|
||||||
@@ -298,9 +312,9 @@ func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
// collectRejectByNameFuncs returns a list of all functions which may reject data
|
||||||
// from being saved in a snapshot based on path only
|
// from being saved in a snapshot based on path only
|
||||||
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (fs []RejectByNameFunc, err error) {
|
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...interface{})) (fs []archiver.RejectByNameFunc, err error) {
|
||||||
// exclude restic cache
|
// exclude restic cache
|
||||||
if repo.Cache != nil {
|
if repo.Cache() != nil {
|
||||||
f, err := rejectResticCache(repo)
|
f, err := rejectResticCache(repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -309,23 +323,12 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
|
|||||||
fs = append(fs, f)
|
fs = append(fs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fsPatterns, err := opts.excludePatternOptions.CollectPatterns()
|
fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(warnf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fs = append(fs, fsPatterns...)
|
for _, pat := range fsPatterns {
|
||||||
|
fs = append(fs, archiver.RejectByNameFunc(pat))
|
||||||
if opts.ExcludeCaches {
|
|
||||||
opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, spec := range opts.ExcludeIfPresent {
|
|
||||||
f, err := rejectIfPresent(spec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fs = append(fs, f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs, nil
|
return fs, nil
|
||||||
@@ -333,35 +336,61 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
|
|||||||
|
|
||||||
// collectRejectFuncs returns a list of all functions which may reject data
|
// collectRejectFuncs returns a list of all functions which may reject data
|
||||||
// from being saved in a snapshot based on path and file info
|
// from being saved in a snapshot based on path and file info
|
||||||
func collectRejectFuncs(opts BackupOptions, targets []string) (fs []RejectFunc, err error) {
|
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...interface{})) (funcs []archiver.RejectFunc, err error) {
|
||||||
// allowed devices
|
// allowed devices
|
||||||
if opts.ExcludeOtherFS && !opts.Stdin {
|
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
|
||||||
f, err := rejectByDevice(targets)
|
f, err := archiver.RejectByDevice(targets, fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fs = append(fs, f)
|
funcs = append(funcs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opts.ExcludeLargerThan) != 0 && !opts.Stdin {
|
if len(opts.ExcludeLargerThan) != 0 && !opts.Stdin && !opts.StdinCommand {
|
||||||
f, err := rejectBySize(opts.ExcludeLargerThan)
|
maxSize, err := ui.ParseBytes(opts.ExcludeLargerThan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fs = append(fs, f)
|
|
||||||
|
f, err := archiver.RejectBySize(maxSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
funcs = append(funcs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs, nil
|
if opts.ExcludeCloudFiles && !opts.Stdin && !opts.StdinCommand {
|
||||||
|
f, err := archiver.RejectCloudFiles(warnf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
funcs = append(funcs, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ExcludeCaches {
|
||||||
|
opts.ExcludeIfPresent = append(opts.ExcludeIfPresent, "CACHEDIR.TAG:Signature: 8a477f597d28d172789f06886806bc55")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, spec := range opts.ExcludeIfPresent {
|
||||||
|
f, err := archiver.RejectIfPresent(spec, warnf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
funcs = append(funcs, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return funcs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectTargets returns a list of target files/dirs from several sources.
|
// collectTargets returns a list of target files/dirs from several sources.
|
||||||
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 {
|
if opts.Stdin || opts.StdinCommand {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range opts.FilesFrom {
|
for _, file := range opts.FilesFrom {
|
||||||
fromfile, err := readLines(file)
|
fromfile, err := readLines(file, stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -379,14 +408,14 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
|
|||||||
return nil, fmt.Errorf("pattern: %s: %w", line, err)
|
return nil, fmt.Errorf("pattern: %s: %w", line, err)
|
||||||
}
|
}
|
||||||
if len(expanded) == 0 {
|
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...)
|
targets = append(targets, expanded...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range opts.FilesFromVerbatim {
|
for _, file := range opts.FilesFromVerbatim {
|
||||||
fromfile, err := readLines(file)
|
fromfile, err := readLines(file, stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -399,7 +428,7 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range opts.FilesFromRaw {
|
for _, file := range opts.FilesFromRaw {
|
||||||
fromfile, err := readFilenamesFromFileRaw(file)
|
fromfile, err := readFilenamesFromFileRaw(file, stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -413,17 +442,12 @@ func collectTargets(opts BackupOptions, args []string) (targets []string, err er
|
|||||||
return nil, errors.Fatal("nothing to backup, please specify source files/dirs")
|
return nil, errors.Fatal("nothing to backup, please specify source files/dirs")
|
||||||
}
|
}
|
||||||
|
|
||||||
targets, err = filterExisting(targets)
|
return filterExisting(targets, warnf)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return targets, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parent returns the ID of the parent snapshot. If there is none, nil is
|
// parent returns the ID of the parent snapshot. If there is none, nil is
|
||||||
// returned.
|
// 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 {
|
if opts.Force {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -432,7 +456,7 @@ func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, o
|
|||||||
if snName == "" {
|
if snName == "" {
|
||||||
snName = "latest"
|
snName = "latest"
|
||||||
}
|
}
|
||||||
f := restic.SnapshotFilter{TimestampLimit: timeStampLimit}
|
f := data.SnapshotFilter{TimestampLimit: timeStampLimit}
|
||||||
if opts.GroupBy.Host {
|
if opts.GroupBy.Host {
|
||||||
f.Hosts = []string{opts.Host}
|
f.Hosts = []string{opts.Host}
|
||||||
}
|
}
|
||||||
@@ -440,23 +464,29 @@ func findParentSnapshot(ctx context.Context, repo restic.ListerLoaderUnpacked, o
|
|||||||
f.Paths = targets
|
f.Paths = targets
|
||||||
}
|
}
|
||||||
if opts.GroupBy.Tag {
|
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)
|
sn, _, err := f.FindLatest(ctx, repo, repo, snName)
|
||||||
// Snapshot not found is ok if no explicit parent was set
|
// 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
|
err = nil
|
||||||
}
|
}
|
||||||
return sn, err
|
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 vsscfg fs.VSSConfig
|
||||||
var err error
|
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 runtime.GOOS == "windows" {
|
||||||
if vsscfg, err = fs.ParseVSSConfig(gopts.extended); err != nil {
|
if vsscfg, err = fs.ParseVSSConfig(gopts.Extended); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,53 +496,46 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
targets, err := collectTargets(opts, args)
|
success := true
|
||||||
|
targets, err := collectTargets(opts, args, printer.E, term.InputRaw())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if errors.Is(err, ErrInvalidSourceData) {
|
||||||
|
success = false
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeStamp := time.Now()
|
timeStamp := time.Now()
|
||||||
backupStart := timeStamp
|
backupStart := timeStamp
|
||||||
if opts.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 {
|
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 {
|
if gopts.Verbosity >= 2 && !gopts.JSON {
|
||||||
Verbosef("open repository\n")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
var progressPrinter backup.ProgressPrinter
|
progressReporter := backup.NewProgress(printer,
|
||||||
if gopts.JSON {
|
ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
|
||||||
progressPrinter = backup.NewJSONProgress(term, gopts.verbosity)
|
|
||||||
} else {
|
|
||||||
progressPrinter = backup.NewTextProgress(term, gopts.verbosity)
|
|
||||||
}
|
|
||||||
progressReporter := backup.NewProgress(progressPrinter,
|
|
||||||
calculateProgressInterval(!gopts.Quiet, gopts.JSON))
|
|
||||||
defer progressReporter.Done()
|
defer progressReporter.Done()
|
||||||
|
|
||||||
// rejectByNameFuncs collect functions that can reject items from the backup based on path only
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
var parentSnapshot *data.Snapshot
|
||||||
rejectFuncs, err := collectRejectFuncs(opts, targets)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var parentSnapshot *restic.Snapshot
|
|
||||||
if !opts.Stdin {
|
if !opts.Stdin {
|
||||||
parentSnapshot, err = findParentSnapshot(ctx, repo, opts, targets, timeStamp)
|
parentSnapshot, err = findParentSnapshot(ctx, repo, opts, targets, timeStamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -521,42 +544,22 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
|
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
if parentSnapshot != nil {
|
if parentSnapshot != nil {
|
||||||
progressPrinter.P("using parent snapshot %v\n", parentSnapshot.ID().Str())
|
printer.P("using parent snapshot %v\n", parentSnapshot.ID().Str())
|
||||||
} else {
|
} 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 {
|
if !gopts.JSON {
|
||||||
progressPrinter.V("load index files")
|
printer.V("load index files")
|
||||||
}
|
}
|
||||||
|
|
||||||
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
err = repo.LoadIndex(ctx, printer)
|
||||||
|
|
||||||
err = repo.LoadIndex(ctx, bar)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
selectByNameFilter := func(item string) bool {
|
|
||||||
for _, reject := range rejectByNameFuncs {
|
|
||||||
if reject(item) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
selectFilter := func(item string, fi os.FileInfo) bool {
|
|
||||||
for _, reject := range rejectFuncs {
|
|
||||||
if reject(item, fi) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetFS fs.FS = fs.Local{}
|
var targetFS fs.FS = fs.Local{}
|
||||||
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
|
if runtime.GOOS == "windows" && opts.UseFsSnapshot {
|
||||||
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
|
if err = fs.HasSufficientPrivilegesForVSS(); err != nil {
|
||||||
@@ -569,7 +572,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
|
|
||||||
messageHandler := func(msg string, args ...interface{}) {
|
messageHandler := func(msg string, args ...interface{}) {
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
progressPrinter.P(msg, args...)
|
printer.P(msg, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,21 +583,22 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
|
|
||||||
if opts.Stdin || opts.StdinCommand {
|
if opts.Stdin || opts.StdinCommand {
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
progressPrinter.V("read data from stdin")
|
printer.V("read data from stdin")
|
||||||
}
|
}
|
||||||
filename := path.Join("/", opts.StdinFilename)
|
filename := path.Join("/", opts.StdinFilename)
|
||||||
var source io.ReadCloser = os.Stdin
|
source := term.InputRaw()
|
||||||
if opts.StdinCommand {
|
if opts.StdinCommand {
|
||||||
source, err = fs.NewCommandReader(ctx, args, globalOptions.stderr)
|
source, err = fs.NewCommandReader(ctx, args, printer.E)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
targetFS = &fs.Reader{
|
targetFS, err = fs.NewReader(filename, source, fs.ReaderOptions{
|
||||||
ModTime: timeStamp,
|
ModTime: timeStamp,
|
||||||
Name: filename,
|
Mode: 0644,
|
||||||
Mode: 0644,
|
})
|
||||||
ReadCloser: source,
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to backup from stdin: %w", err)
|
||||||
}
|
}
|
||||||
targets = []string{filename}
|
targets = []string{filename}
|
||||||
}
|
}
|
||||||
@@ -603,6 +607,15 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
targetFS = backupFSTestHook(targetFS)
|
targetFS = backupFSTestHook(targetFS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
||||||
|
rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS, printer.E)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
selectByNameFilter := archiver.CombineRejectByNames(rejectByNameFuncs)
|
||||||
|
selectFilter := archiver.CombineRejects(rejectFuncs)
|
||||||
|
|
||||||
wg, wgCtx := errgroup.WithContext(ctx)
|
wg, wgCtx := errgroup.WithContext(ctx)
|
||||||
cancelCtx, cancel := context.WithCancel(wgCtx)
|
cancelCtx, cancel := context.WithCancel(wgCtx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -611,11 +624,11 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
sc := archiver.NewScanner(targetFS)
|
sc := archiver.NewScanner(targetFS)
|
||||||
sc.SelectByName = selectByNameFilter
|
sc.SelectByName = selectByNameFilter
|
||||||
sc.Select = selectFilter
|
sc.Select = selectFilter
|
||||||
sc.Error = progressPrinter.ScannerError
|
sc.Error = printer.ScannerError
|
||||||
sc.Result = progressReporter.ReportTotal
|
sc.Result = progressReporter.ReportTotal
|
||||||
|
|
||||||
if !gopts.JSON {
|
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) })
|
wg.Go(func() error { return sc.Scan(cancelCtx, targets) })
|
||||||
}
|
}
|
||||||
@@ -624,7 +637,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
arch.SelectByName = selectByNameFilter
|
arch.SelectByName = selectByNameFilter
|
||||||
arch.Select = selectFilter
|
arch.Select = selectFilter
|
||||||
arch.WithAtime = opts.WithAtime
|
arch.WithAtime = opts.WithAtime
|
||||||
success := true
|
|
||||||
arch.Error = func(item string, err error) error {
|
arch.Error = func(item string, err error) error {
|
||||||
success = false
|
success = false
|
||||||
reterr := progressReporter.Error(item, err)
|
reterr := progressReporter.Error(item, err)
|
||||||
@@ -655,12 +668,12 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
Time: timeStamp,
|
Time: timeStamp,
|
||||||
Hostname: opts.Host,
|
Hostname: opts.Host,
|
||||||
ParentSnapshot: parentSnapshot,
|
ParentSnapshot: parentSnapshot,
|
||||||
ProgramVersion: "restic " + version,
|
ProgramVersion: "restic " + global.Version,
|
||||||
SkipIfUnchanged: opts.SkipIfUnchanged,
|
SkipIfUnchanged: opts.SkipIfUnchanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gopts.JSON {
|
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)
|
_, id, summary, err := arch.Snapshot(ctx, targets, snapshotOpts)
|
||||||
|
|
||||||
|
|||||||
@@ -3,35 +3,36 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/fs"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) error {
|
func testRunBackupAssumeFailure(t testing.TB, dir string, target []string, opts BackupOptions, gopts global.Options) error {
|
||||||
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
return withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
t.Logf("backing up %v in %v", target, dir)
|
t.Logf("backing up %v in %v", target, dir)
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
cleanup := rtest.Chdir(t, dir)
|
cleanup := rtest.Chdir(t, dir)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
opts.GroupBy = data.SnapshotGroupByOptions{Host: true, Path: true}
|
||||||
return runBackup(ctx, opts, gopts, term, target)
|
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)
|
err := testRunBackupAssumeFailure(t, dir, target, opts, gopts)
|
||||||
rtest.Assert(t, err == nil, "Error while backing up")
|
rtest.Assert(t, err == nil, "Error while backing up: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackup(t *testing.T) {
|
func TestBackup(t *testing.T) {
|
||||||
@@ -56,13 +57,13 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
|||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
stat1 := dirStats(env.repo)
|
stat1 := dirStats(t, env.repo)
|
||||||
|
|
||||||
// second backup, implicit incremental
|
// second backup, implicit incremental
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
snapshotIDs := testListSnapshots(t, env.gopts, 2)
|
snapshotIDs := testListSnapshots(t, env.gopts, 2)
|
||||||
|
|
||||||
stat2 := dirStats(env.repo)
|
stat2 := dirStats(t, env.repo)
|
||||||
if stat2.size > stat1.size+stat1.size/10 {
|
if stat2.size > stat1.size+stat1.size/10 {
|
||||||
t.Error("repository size has grown by more than 10 percent")
|
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)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
snapshotIDs = testListSnapshots(t, env.gopts, 3)
|
snapshotIDs = testListSnapshots(t, env.gopts, 3)
|
||||||
|
|
||||||
stat3 := dirStats(env.repo)
|
stat3 := dirStats(t, env.repo)
|
||||||
if stat3.size > stat1.size+stat1.size/10 {
|
if stat3.size > stat1.size+stat1.size/10 {
|
||||||
t.Error("repository size has grown by more than 10 percent")
|
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))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID.String()+":"+toPathInSnapshot(filepath.Dir(env.testdata)))
|
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)
|
rtest.Assert(t, diff == "", "directories are not equal: %v", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ type vssDeleteOriginalFS struct {
|
|||||||
hasRemoved bool
|
hasRemoved bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *vssDeleteOriginalFS) Lstat(name string) (os.FileInfo, error) {
|
func (f *vssDeleteOriginalFS) Lstat(name string) (*fs.ExtendedFileInfo, error) {
|
||||||
if !f.hasRemoved {
|
if !f.hasRemoved {
|
||||||
// call Lstat to trigger snapshot creation
|
// call Lstat to trigger snapshot creation
|
||||||
_, _ = f.FS.Lstat(name)
|
_, _ = f.FS.Lstat(name)
|
||||||
@@ -218,41 +219,41 @@ func TestDryRunBackup(t *testing.T) {
|
|||||||
// dry run before first backup
|
// dry run before first backup
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
||||||
snapshotIDs := testListSnapshots(t, env.gopts, 0)
|
snapshotIDs := testListSnapshots(t, env.gopts, 0)
|
||||||
packIDs := testRunList(t, "packs", env.gopts)
|
packIDs := testRunList(t, env.gopts, "packs")
|
||||||
rtest.Assert(t, len(packIDs) == 0,
|
rtest.Assert(t, len(packIDs) == 0,
|
||||||
"expected no data, got %v", snapshotIDs)
|
"expected no data, got %v", snapshotIDs)
|
||||||
indexIDs := testRunList(t, "index", env.gopts)
|
indexIDs := testRunList(t, env.gopts, "index")
|
||||||
rtest.Assert(t, len(indexIDs) == 0,
|
rtest.Assert(t, len(indexIDs) == 0,
|
||||||
"expected no index, got %v", snapshotIDs)
|
"expected no index, got %v", snapshotIDs)
|
||||||
|
|
||||||
// first backup
|
// first backup
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
||||||
snapshotIDs = testListSnapshots(t, env.gopts, 1)
|
snapshotIDs = testListSnapshots(t, env.gopts, 1)
|
||||||
packIDs = testRunList(t, "packs", env.gopts)
|
packIDs = testRunList(t, env.gopts, "packs")
|
||||||
indexIDs = testRunList(t, "index", env.gopts)
|
indexIDs = testRunList(t, env.gopts, "index")
|
||||||
|
|
||||||
// dry run between backups
|
// dry run between backups
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
||||||
snapshotIDsAfter := testListSnapshots(t, env.gopts, 1)
|
snapshotIDsAfter := testListSnapshots(t, env.gopts, 1)
|
||||||
rtest.Equals(t, snapshotIDs, snapshotIDsAfter)
|
rtest.Equals(t, snapshotIDs, snapshotIDsAfter)
|
||||||
dataIDsAfter := testRunList(t, "packs", env.gopts)
|
dataIDsAfter := testRunList(t, env.gopts, "packs")
|
||||||
rtest.Equals(t, packIDs, dataIDsAfter)
|
rtest.Equals(t, packIDs, dataIDsAfter)
|
||||||
indexIDsAfter := testRunList(t, "index", env.gopts)
|
indexIDsAfter := testRunList(t, env.gopts, "index")
|
||||||
rtest.Equals(t, indexIDs, indexIDsAfter)
|
rtest.Equals(t, indexIDs, indexIDsAfter)
|
||||||
|
|
||||||
// second backup, implicit incremental
|
// second backup, implicit incremental
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
||||||
snapshotIDs = testListSnapshots(t, env.gopts, 2)
|
snapshotIDs = testListSnapshots(t, env.gopts, 2)
|
||||||
packIDs = testRunList(t, "packs", env.gopts)
|
packIDs = testRunList(t, env.gopts, "packs")
|
||||||
indexIDs = testRunList(t, "index", env.gopts)
|
indexIDs = testRunList(t, env.gopts, "index")
|
||||||
|
|
||||||
// another dry run
|
// another dry run
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, dryOpts, env.gopts)
|
||||||
snapshotIDsAfter = testListSnapshots(t, env.gopts, 2)
|
snapshotIDsAfter = testListSnapshots(t, env.gopts, 2)
|
||||||
rtest.Equals(t, snapshotIDs, snapshotIDsAfter)
|
rtest.Equals(t, snapshotIDs, snapshotIDsAfter)
|
||||||
dataIDsAfter = testRunList(t, "packs", env.gopts)
|
dataIDsAfter = testRunList(t, env.gopts, "packs")
|
||||||
rtest.Equals(t, packIDs, dataIDsAfter)
|
rtest.Equals(t, packIDs, dataIDsAfter)
|
||||||
indexIDsAfter = testRunList(t, "index", env.gopts)
|
indexIDsAfter = testRunList(t, env.gopts, "index")
|
||||||
rtest.Equals(t, indexIDs, indexIDsAfter)
|
rtest.Equals(t, indexIDs, indexIDsAfter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,22 +263,27 @@ func TestBackupNonExistingFile(t *testing.T) {
|
|||||||
|
|
||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
|
|
||||||
_ = withRestoreGlobalOptions(func() error {
|
p := filepath.Join(env.testdata, "0", "0", "9")
|
||||||
globalOptions.stderr = io.Discard
|
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")
|
opts := BackupOptions{}
|
||||||
dirs := []string{
|
|
||||||
filepath.Join(p, "0"),
|
|
||||||
filepath.Join(p, "1"),
|
|
||||||
filepath.Join(p, "nonexisting"),
|
|
||||||
filepath.Join(p, "5"),
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := BackupOptions{}
|
// mix of existing and non-existing files
|
||||||
|
err := testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts)
|
||||||
testRunBackup(t, "", dirs, opts, env.gopts)
|
rtest.Assert(t, err != nil, "expected error for non-existing file")
|
||||||
return nil
|
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) {
|
func TestBackupSelfHealing(t *testing.T) {
|
||||||
@@ -365,12 +371,7 @@ func TestBackupExclude(t *testing.T) {
|
|||||||
for _, filename := range backupExcludeFilenames {
|
for _, filename := range backupExcludeFilenames {
|
||||||
fp := filepath.Join(datadir, filename)
|
fp := filepath.Join(datadir, filename)
|
||||||
rtest.OK(t, os.MkdirAll(filepath.Dir(fp), 0755))
|
rtest.OK(t, os.MkdirAll(filepath.Dir(fp), 0755))
|
||||||
|
rtest.OK(t, os.WriteFile(fp, []byte(filename), 0o666))
|
||||||
f, err := os.Create(fp)
|
|
||||||
rtest.OK(t, err)
|
|
||||||
|
|
||||||
fmt.Fprint(f, filename)
|
|
||||||
rtest.OK(t, f.Close())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshots := make(map[string]struct{})
|
snapshots := make(map[string]struct{})
|
||||||
@@ -443,13 +444,13 @@ func TestIncrementalBackup(t *testing.T) {
|
|||||||
|
|
||||||
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
stat1 := dirStats(env.repo)
|
stat1 := dirStats(t, env.repo)
|
||||||
|
|
||||||
rtest.OK(t, appendRandomData(testfile, incrementalSecondWrite))
|
rtest.OK(t, appendRandomData(testfile, incrementalSecondWrite))
|
||||||
|
|
||||||
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
stat2 := dirStats(env.repo)
|
stat2 := dirStats(t, env.repo)
|
||||||
if stat2.size-stat1.size > incrementalFirstWrite {
|
if stat2.size-stat1.size > incrementalFirstWrite {
|
||||||
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
|
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
|
||||||
}
|
}
|
||||||
@@ -459,14 +460,13 @@ func TestIncrementalBackup(t *testing.T) {
|
|||||||
|
|
||||||
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
testRunBackup(t, "", []string{datadir}, opts, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
stat3 := dirStats(env.repo)
|
stat3 := dirStats(t, env.repo)
|
||||||
if stat3.size-stat2.size > incrementalFirstWrite {
|
if stat3.size-stat2.size > incrementalFirstWrite {
|
||||||
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
|
t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite)
|
||||||
}
|
}
|
||||||
t.Logf("repository grown by %d bytes", stat3.size-stat2.size)
|
t.Logf("repository grown by %d bytes", stat3.size-stat2.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: staticcheck // false positive nil pointer dereference check
|
|
||||||
func TestBackupTags(t *testing.T) {
|
func TestBackupTags(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -486,7 +486,7 @@ func TestBackupTags(t *testing.T) {
|
|||||||
"expected no tags, got %v", newest.Tags)
|
"expected no tags, got %v", newest.Tags)
|
||||||
parent := newest
|
parent := newest
|
||||||
|
|
||||||
opts.Tags = restic.TagLists{[]string{"NL"}}
|
opts.Tags = data.TagLists{[]string{"NL"}}
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
newest, _ = testRunSnapshots(t, env.gopts)
|
newest, _ = testRunSnapshots(t, env.gopts)
|
||||||
@@ -502,7 +502,6 @@ func TestBackupTags(t *testing.T) {
|
|||||||
"expected parent to be %v, got %v", parent.ID, newest.Parent)
|
"expected parent to be %v, got %v", parent.ID, newest.Parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint: staticcheck // false positive nil pointer dereference check
|
|
||||||
func TestBackupProgramVersion(t *testing.T) {
|
func TestBackupProgramVersion(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -514,7 +513,7 @@ func TestBackupProgramVersion(t *testing.T) {
|
|||||||
if newest == nil {
|
if newest == nil {
|
||||||
t.Fatal("expected a backup, got nil")
|
t.Fatal("expected a backup, got nil")
|
||||||
}
|
}
|
||||||
resticVersion := "restic " + version
|
resticVersion := "restic " + global.Version
|
||||||
rtest.Assert(t, newest.ProgramVersion == resticVersion,
|
rtest.Assert(t, newest.ProgramVersion == resticVersion,
|
||||||
"expected %v, got %v", resticVersion, newest.ProgramVersion)
|
"expected %v, got %v", resticVersion, newest.ProgramVersion)
|
||||||
}
|
}
|
||||||
@@ -572,7 +571,7 @@ func TestHardLink(t *testing.T) {
|
|||||||
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i))
|
||||||
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir)
|
||||||
testRunRestore(t, env.gopts, restoredir, snapshotID.String())
|
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)
|
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
||||||
|
|
||||||
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
|
linkResults := createFileSetPerHardlink(filepath.Join(restoredir, "testdata"))
|
||||||
@@ -637,12 +636,15 @@ func TestStdinFromCommand(t *testing.T) {
|
|||||||
|
|
||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
opts := BackupOptions{
|
opts := BackupOptions{
|
||||||
StdinCommand: true,
|
StdinCommand: true,
|
||||||
StdinFilename: "stdin",
|
// test that subdirectories are handled correctly
|
||||||
|
StdinFilename: "stdin/subdir/file",
|
||||||
}
|
}
|
||||||
|
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"python", "-c", "import sys; print('something'); sys.exit(0)"}, opts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"python", "-c", "import sys; print('something'); sys.exit(0)"}, opts, env.gopts)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
files := testRunLs(t, env.gopts, snapshots[0].String())
|
||||||
|
rtest.Assert(t, includes(files, "/stdin/subdir/file"), "file %q missing from snapshot, got %v", "stdin/subdir/file", files)
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
@@ -705,7 +707,7 @@ func TestBackupEmptyPassword(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
env.gopts.password = ""
|
env.gopts.Password = ""
|
||||||
env.gopts.InsecureNoPassword = true
|
env.gopts.InsecureNoPassword = true
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
|
|||||||
@@ -39,21 +39,24 @@ func TestCollectTargets(t *testing.T) {
|
|||||||
f1, err := os.Create(filepath.Join(dir, "fromfile"))
|
f1, err := os.Create(filepath.Join(dir, "fromfile"))
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
// Empty lines should be ignored. A line starting with '#' is a comment.
|
// Empty lines should be ignored. A line starting with '#' is a comment.
|
||||||
fmt.Fprintf(f1, "\n%s*\n # here's a comment\n", f1.Name())
|
_, err = fmt.Fprintf(f1, "\n%s*\n # here's a comment\n", f1.Name())
|
||||||
|
rtest.OK(t, err)
|
||||||
rtest.OK(t, f1.Close())
|
rtest.OK(t, f1.Close())
|
||||||
|
|
||||||
f2, err := os.Create(filepath.Join(dir, "fromfile-verbatim"))
|
f2, err := os.Create(filepath.Join(dir, "fromfile-verbatim"))
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
for _, filename := range []string{fooSpace, barStar} {
|
for _, filename := range []string{fooSpace, barStar} {
|
||||||
// Empty lines should be ignored. CR+LF is allowed.
|
// Empty lines should be ignored. CR+LF is allowed.
|
||||||
fmt.Fprintf(f2, "%s\r\n\n", filepath.Join(dir, filename))
|
_, err = fmt.Fprintf(f2, "%s\r\n\n", filepath.Join(dir, filename))
|
||||||
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
rtest.OK(t, f2.Close())
|
rtest.OK(t, f2.Close())
|
||||||
|
|
||||||
f3, err := os.Create(filepath.Join(dir, "fromfile-raw"))
|
f3, err := os.Create(filepath.Join(dir, "fromfile-raw"))
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
for _, filename := range []string{"baz", "quux"} {
|
for _, filename := range []string{"baz", "quux"} {
|
||||||
fmt.Fprintf(f3, "%s\x00", filepath.Join(dir, filename))
|
_, err = fmt.Fprintf(f3, "%s\x00", filepath.Join(dir, filename))
|
||||||
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
rtest.OK(t, f3.Close())
|
rtest.OK(t, f3.Close())
|
||||||
@@ -64,10 +67,13 @@ func TestCollectTargets(t *testing.T) {
|
|||||||
FilesFromRaw: []string{f3.Name()},
|
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)
|
rtest.OK(t, err)
|
||||||
sort.Strings(targets)
|
sort.Strings(targets)
|
||||||
rtest.Equals(t, expect, 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 TestReadFilenamesRaw(t *testing.T) {
|
func TestReadFilenamesRaw(t *testing.T) {
|
||||||
|
|||||||
@@ -10,16 +10,20 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/backend/cache"
|
"github.com/restic/restic/internal/backend/cache"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
"github.com/restic/restic/internal/ui/table"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdCache = &cobra.Command{
|
func newCacheCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "cache",
|
var opts CacheOptions
|
||||||
Short: "Operate on local cache directories",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "cache",
|
||||||
|
Short: "Operate on local cache directories",
|
||||||
|
Long: `
|
||||||
The "cache" command allows listing and cleaning local cache directories.
|
The "cache" command allows listing and cleaning local cache directories.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
@@ -28,11 +32,15 @@ EXIT STATUS
|
|||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful.
|
||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
`,
|
`,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(_ *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
return runCache(cacheOptions, globalOptions, args)
|
return runCache(opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheOptions bundles all options for the snapshots command.
|
// CacheOptions bundles all options for the snapshots command.
|
||||||
@@ -42,18 +50,15 @@ type CacheOptions struct {
|
|||||||
NoSize bool
|
NoSize bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheOptions CacheOptions
|
func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&opts.Cleanup, "cleanup", false, "remove old cache directories")
|
||||||
func init() {
|
f.UintVar(&opts.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old")
|
||||||
cmdRoot.AddCommand(cmdCache)
|
f.BoolVar(&opts.NoSize, "no-size", false, "do not output the size of the cache directories")
|
||||||
|
|
||||||
f := cmdCache.Flags()
|
|
||||||
f.BoolVar(&cacheOptions.Cleanup, "cleanup", false, "remove old cache directories")
|
|
||||||
f.UintVar(&cacheOptions.MaxAge, "max-age", 30, "max age in `days` for cache directories to be considered old")
|
|
||||||
f.BoolVar(&cacheOptions.NoSize, "no-size", false, "do not output the size of the cache directories")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
func runCache(opts CacheOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
|
return errors.Fatal("the cache command expects no arguments, only options - please see `restic help cache` for usage and flags")
|
||||||
}
|
}
|
||||||
@@ -81,17 +86,17 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(oldDirs) == 0 {
|
if len(oldDirs) == 0 {
|
||||||
Verbosef("no old cache dirs found\n")
|
printer.P("no old cache dirs found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("remove %d old cache directories\n", len(oldDirs))
|
printer.P("remove %d old cache directories", len(oldDirs))
|
||||||
|
|
||||||
for _, item := range oldDirs {
|
for _, item := range oldDirs {
|
||||||
dir := filepath.Join(cachedir, item.Name())
|
dir := filepath.Join(cachedir, item.Name())
|
||||||
err = fs.RemoveAll(dir)
|
err = os.RemoveAll(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("unable to remove %v: %v\n", dir, err)
|
printer.E("unable to remove %v: %v", dir, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +126,7 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(dirs) == 0 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,8 +162,8 @@ func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = tab.Write(globalOptions.stdout)
|
_ = tab.Write(gopts.Term.OutputWriter())
|
||||||
Printf("%d cache dirs in %s\n", len(dirs), cachedir)
|
printer.S("%d cache dirs in %s", len(dirs), cachedir)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,21 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
|
var catAllowedCmds = []string{"config", "index", "snapshot", "key", "masterkey", "lock", "pack", "blob", "tree"}
|
||||||
|
|
||||||
var cmdCat = &cobra.Command{
|
func newCatCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
|
cmd := &cobra.Command{
|
||||||
Short: "Print internal objects to stdout",
|
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
|
||||||
Long: `
|
Short: "Print internal objects to stdout",
|
||||||
|
Long: `
|
||||||
The "cat" command is used to print internal objects to stdout.
|
The "cat" command is used to print internal objects to stdout.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
@@ -29,16 +33,14 @@ Exit status is 10 if the repository does not exist.
|
|||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runCat(cmd.Context(), globalOptions, args)
|
return runCat(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
ValidArgs: catAllowedCmds,
|
ValidArgs: catAllowedCmds,
|
||||||
}
|
}
|
||||||
|
return cmd
|
||||||
func init() {
|
|
||||||
cmdRoot.AddCommand(cmdCat)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCatArgs(args []string) error {
|
func validateCatArgs(args []string) error {
|
||||||
@@ -64,12 +66,14 @@ func validateCatArgs(args []string) error {
|
|||||||
return nil
|
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 {
|
if err := validateCatArgs(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -81,7 +85,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
if tpe != "masterkey" && tpe != "config" && tpe != "snapshot" && tpe != "tree" {
|
if tpe != "masterkey" && tpe != "config" && tpe != "snapshot" && tpe != "tree" {
|
||||||
id, err = restic.ParseID(args[1])
|
id, err = restic.ParseID(args[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to parse ID: %v\n", err)
|
return errors.Fatalf("unable to parse ID: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +96,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
printer.S(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "index":
|
case "index":
|
||||||
buf, err := repo.LoadUnpacked(ctx, restic.IndexFile, id)
|
buf, err := repo.LoadUnpacked(ctx, restic.IndexFile, id)
|
||||||
@@ -100,12 +104,12 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
printer.S(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "snapshot":
|
case "snapshot":
|
||||||
sn, _, err := restic.FindSnapshot(ctx, repo, repo, args[1])
|
sn, _, err := data.FindSnapshot(ctx, repo, repo, args[1])
|
||||||
if err != nil {
|
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, "", " ")
|
buf, err := json.MarshalIndent(sn, "", " ")
|
||||||
@@ -113,7 +117,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
printer.S(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "key":
|
case "key":
|
||||||
key, err := repository.LoadKey(ctx, repo, id)
|
key, err := repository.LoadKey(ctx, repo, id)
|
||||||
@@ -126,7 +130,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
printer.S(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "masterkey":
|
case "masterkey":
|
||||||
buf, err := json.MarshalIndent(repo.Key(), "", " ")
|
buf, err := json.MarshalIndent(repo.Key(), "", " ")
|
||||||
@@ -134,7 +138,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
printer.S(string(buf))
|
||||||
return nil
|
return nil
|
||||||
case "lock":
|
case "lock":
|
||||||
lock, err := restic.LoadLock(ctx, repo, id)
|
lock, err := restic.LoadLock(ctx, repo, id)
|
||||||
@@ -147,7 +151,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Println(string(buf))
|
printer.S(string(buf))
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
case "pack":
|
case "pack":
|
||||||
@@ -159,15 +163,14 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
|
|
||||||
hash := restic.Hash(buf)
|
hash := restic.Hash(buf)
|
||||||
if !hash.Equal(id) {
|
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
|
return err
|
||||||
|
|
||||||
case "blob":
|
case "blob":
|
||||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
err = repo.LoadIndex(ctx, printer)
|
||||||
err = repo.LoadIndex(ctx, bar)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -182,25 +185,24 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = globalOptions.stdout.Write(buf)
|
_, err = term.OutputRaw().Write(buf)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Fatal("blob not found")
|
return errors.Fatal("blob not found")
|
||||||
|
|
||||||
case "tree":
|
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 {
|
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, printer)
|
||||||
err = repo.LoadIndex(ctx, bar)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -209,7 +211,7 @@ func runCat(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = globalOptions.stdout.Write(buf)
|
_, err = term.OutputRaw().Write(buf)
|
||||||
return err
|
return err
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -10,22 +11,25 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend/cache"
|
"github.com/restic/restic/internal/backend/cache"
|
||||||
"github.com/restic/restic/internal/checker"
|
"github.com/restic/restic/internal/checker"
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/fs"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdCheck = &cobra.Command{
|
func newCheckCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "check [flags]",
|
var opts CheckOptions
|
||||||
Short: "Check the repository for errors",
|
cmd := &cobra.Command{
|
||||||
Long: `
|
Use: "check [flags]",
|
||||||
|
Short: "Check the repository for errors",
|
||||||
|
Long: `
|
||||||
The "check" command tests the repository for errors and reports any errors it
|
The "check" command tests the repository for errors and reports any errors it
|
||||||
finds. It can also be used to read all data and therefore simulate a restore.
|
finds. It can also be used to read all data and therefore simulate a restore.
|
||||||
|
|
||||||
@@ -41,16 +45,26 @@ Exit status is 10 if the repository does not exist.
|
|||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
term, cancel := setupTermstatus()
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
defer cancel()
|
summary, err := runCheck(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
return runCheck(cmd.Context(), checkOptions, globalOptions, args, term)
|
if globalOptions.JSON {
|
||||||
},
|
if err != nil && summary.NumErrors == 0 {
|
||||||
PreRunE: func(_ *cobra.Command, _ []string) error {
|
summary.NumErrors = 1
|
||||||
return checkFlags(checkOptions)
|
}
|
||||||
},
|
globalOptions.Term.Print(ui.ToJSONString(summary))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
PreRunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
return checkFlags(opts)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckOptions bundles all options for the 'check' command.
|
// CheckOptions bundles all options for the 'check' command.
|
||||||
@@ -59,16 +73,12 @@ type CheckOptions struct {
|
|||||||
ReadDataSubset string
|
ReadDataSubset string
|
||||||
CheckUnused bool
|
CheckUnused bool
|
||||||
WithCache bool
|
WithCache bool
|
||||||
|
data.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkOptions CheckOptions
|
func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&opts.ReadData, "read-data", false, "read all data blobs")
|
||||||
func init() {
|
f.StringVar(&opts.ReadDataSubset, "read-data-subset", "", "read a `subset` of data packs, specified as 'n/t' for specific part, or either 'x%' or 'x.y%' or a size in bytes with suffixes k/K, m/M, g/G, t/T for a random subset")
|
||||||
cmdRoot.AddCommand(cmdCheck)
|
|
||||||
|
|
||||||
f := cmdCheck.Flags()
|
|
||||||
f.BoolVar(&checkOptions.ReadData, "read-data", false, "read all data blobs")
|
|
||||||
f.StringVar(&checkOptions.ReadDataSubset, "read-data-subset", "", "read a `subset` of data packs, specified as 'n/t' for specific part, or either 'x%' or 'x.y%' or a size in bytes with suffixes k/K, m/M, g/G, t/T for a random subset")
|
|
||||||
var ignored bool
|
var ignored bool
|
||||||
f.BoolVar(&ignored, "check-unused", false, "find unused blobs")
|
f.BoolVar(&ignored, "check-unused", false, "find unused blobs")
|
||||||
err := f.MarkDeprecated("check-unused", "`--check-unused` is deprecated and will be ignored")
|
err := f.MarkDeprecated("check-unused", "`--check-unused` is deprecated and will be ignored")
|
||||||
@@ -76,7 +86,8 @@ func init() {
|
|||||||
// MarkDeprecated only returns an error when the flag is not found
|
// MarkDeprecated only returns an error when the flag is not found
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
f.BoolVar(&checkOptions.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository")
|
f.BoolVar(&opts.WithCache, "with-cache", false, "use existing cache, only read uncached data from repository")
|
||||||
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkFlags(opts CheckOptions) error {
|
func checkFlags(opts CheckOptions) error {
|
||||||
@@ -164,7 +175,7 @@ func parsePercentage(s string) (float64, error) {
|
|||||||
// - if the user explicitly requested --no-cache, we don't use any cache
|
// - 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
|
// - 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
|
// - 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() {}
|
cleanup = func() {}
|
||||||
if opts.WithCache {
|
if opts.WithCache {
|
||||||
// use the default cache, no setup needed
|
// use the default cache, no setup needed
|
||||||
@@ -185,7 +196,7 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
|
|||||||
// use a cache in a temporary directory
|
// use a cache in a temporary directory
|
||||||
err := os.MkdirAll(cachedir, 0755)
|
err := os.MkdirAll(cachedir, 0755)
|
||||||
if err != nil {
|
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
|
gopts.NoCache = true
|
||||||
return cleanup
|
return cleanup
|
||||||
}
|
}
|
||||||
@@ -202,7 +213,7 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
|
|||||||
printer.P("using temporary cache in %v\n", tempdir)
|
printer.P("using temporary cache in %v\n", tempdir)
|
||||||
|
|
||||||
cleanup = func() {
|
cleanup = func() {
|
||||||
err := fs.RemoveAll(tempdir)
|
err := os.RemoveAll(tempdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printer.E("error removing temporary cache directory: %v\n", err)
|
printer.E("error removing temporary cache directory: %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -211,12 +222,15 @@ func prepareCheckCache(opts CheckOptions, gopts *GlobalOptions, printer progress
|
|||||||
return cleanup
|
return cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args []string, term *termstatus.Terminal) error {
|
func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args []string, term ui.Terminal) (checkSummary, error) {
|
||||||
if len(args) != 0 {
|
summary := checkSummary{MessageType: "summary"}
|
||||||
return errors.Fatal("the check command expects no arguments, only options - please see `restic help check` for usage and flags")
|
|
||||||
}
|
|
||||||
|
|
||||||
printer := newTerminalProgressPrinter(gopts.verbosity, term)
|
var printer progress.Printer
|
||||||
|
if !gopts.JSON {
|
||||||
|
printer = ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
} else {
|
||||||
|
printer = newJSONErrorPrinter(term)
|
||||||
|
}
|
||||||
|
|
||||||
cleanup := prepareCheckCache(opts, &gopts, printer)
|
cleanup := prepareCheckCache(opts, &gopts, printer)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
@@ -224,55 +238,44 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
if !gopts.NoLock {
|
if !gopts.NoLock {
|
||||||
printer.P("create exclusive lock for repository\n")
|
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 {
|
if err != nil {
|
||||||
return err
|
return summary, err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
chkr := checker.New(repo, opts.CheckUnused)
|
chkr := checker.New(repo, opts.CheckUnused)
|
||||||
err = chkr.LoadSnapshots(ctx)
|
err = chkr.LoadSnapshots(ctx, &opts.SnapshotFilter, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return summary, err
|
||||||
}
|
}
|
||||||
|
|
||||||
printer.P("load indexes\n")
|
printer.P("load indexes\n")
|
||||||
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
hints, errs := chkr.LoadIndex(ctx, printer)
|
||||||
hints, errs := chkr.LoadIndex(ctx, bar)
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
errorsFound := false
|
errorsFound := false
|
||||||
suggestIndexRebuild := false
|
|
||||||
suggestLegacyIndexRebuild := false
|
|
||||||
mixedFound := false
|
|
||||||
for _, hint := range hints {
|
for _, hint := range hints {
|
||||||
switch hint.(type) {
|
switch hint.(type) {
|
||||||
case *checker.ErrDuplicatePacks:
|
case *repository.ErrDuplicatePacks:
|
||||||
term.Print(hint.Error())
|
printer.S("%s", hint.Error())
|
||||||
suggestIndexRebuild = true
|
summary.HintRepairIndex = true
|
||||||
case *checker.ErrOldIndexFormat:
|
case *repository.ErrMixedPack:
|
||||||
printer.E("error: %v\n", hint)
|
printer.S("%s", hint.Error())
|
||||||
suggestLegacyIndexRebuild = true
|
summary.HintPrune = true
|
||||||
errorsFound = true
|
|
||||||
case *checker.ErrMixedPack:
|
|
||||||
term.Print(hint.Error())
|
|
||||||
mixedFound = true
|
|
||||||
default:
|
default:
|
||||||
printer.E("error: %v\n", hint)
|
printer.E("error: %v\n", hint)
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if suggestIndexRebuild {
|
if summary.HintRepairIndex {
|
||||||
term.Print("Duplicate packs are non-critical, you can run `restic repair index' to correct this.\n")
|
printer.S("Duplicate packs are non-critical, you can run `restic repair index' to correct this.\n")
|
||||||
}
|
}
|
||||||
if suggestLegacyIndexRebuild {
|
if summary.HintPrune {
|
||||||
printer.E("error: Found indexes using the legacy format, you must run `restic repair index' to correct this.\n")
|
printer.S("Mixed packs with tree and data blobs are non-critical, you can run `restic prune` to correct this.\n")
|
||||||
}
|
|
||||||
if mixedFound {
|
|
||||||
term.Print("Mixed packs with tree and data blobs are non-critical, you can run `restic prune` to correct this.\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
@@ -280,8 +283,10 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
printer.E("error: %v\n", err)
|
printer.E("error: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summary.NumErrors += len(errs)
|
||||||
|
summary.HintRepairIndex = true
|
||||||
printer.E("\nThe repository index is damaged and must be repaired. You must run `restic repair index' to correct this.\n\n")
|
printer.E("\nThe repository index is damaged and must be repaired. You must run `restic repair index' to correct this.\n\n")
|
||||||
return errors.Fatal("repository contains errors")
|
return summary, errors.Fatal("repository contains errors")
|
||||||
}
|
}
|
||||||
|
|
||||||
orphanedPacks := 0
|
orphanedPacks := 0
|
||||||
@@ -292,7 +297,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
go chkr.Packs(ctx, errChan)
|
go chkr.Packs(ctx, errChan)
|
||||||
|
|
||||||
for err := range errChan {
|
for err := range errChan {
|
||||||
var packErr *checker.PackError
|
var packErr *repository.PackError
|
||||||
if errors.As(err, &packErr) {
|
if errors.As(err, &packErr) {
|
||||||
if packErr.Orphaned {
|
if packErr.Orphaned {
|
||||||
orphanedPacks++
|
orphanedPacks++
|
||||||
@@ -302,23 +307,24 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
salvagePacks.Insert(packErr.ID)
|
salvagePacks.Insert(packErr.ID)
|
||||||
}
|
}
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
|
summary.NumErrors++
|
||||||
printer.E("%v\n", err)
|
printer.E("%v\n", err)
|
||||||
}
|
}
|
||||||
} else if err == checker.ErrLegacyLayout {
|
|
||||||
errorsFound = true
|
|
||||||
printer.E("error: repository still uses the S3 legacy layout\nYou must run `restic migrate s3legacy` to correct this.\n")
|
|
||||||
} else {
|
} else {
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
printer.E("%v\n", err)
|
printer.E("%v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if orphanedPacks > 0 && !errorsFound {
|
if orphanedPacks > 0 {
|
||||||
// hide notice if repository is damaged
|
summary.HintPrune = true
|
||||||
printer.P("%d additional files were found in the repo, which likely contain duplicate data.\nThis is non-critical, you can run `restic prune` to correct this.\n", orphanedPacks)
|
if !errorsFound {
|
||||||
|
// hide notice if repository is damaged
|
||||||
|
printer.P("%d additional files were found in the repo, which likely contain duplicate data.\nThis is non-critical, you can run `restic prune` to correct this.\n", orphanedPacks)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
printer.P("check snapshots, trees and blobs\n")
|
printer.P("check snapshots, trees and blobs\n")
|
||||||
@@ -328,7 +334,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
bar := newTerminalProgressMax(!gopts.Quiet, 0, "snapshots", term)
|
bar := printer.NewCounter("snapshots")
|
||||||
defer bar.Done()
|
defer bar.Done()
|
||||||
chkr.Structure(ctx, bar, errChan)
|
chkr.Structure(ctx, bar, errChan)
|
||||||
}()
|
}()
|
||||||
@@ -338,9 +344,11 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
if e, ok := err.(*checker.TreeError); ok {
|
if e, ok := err.(*checker.TreeError); ok {
|
||||||
printer.E("error for tree %v:\n", e.ID.Str())
|
printer.E("error for tree %v:\n", e.ID.Str())
|
||||||
for _, treeErr := range e.Errors {
|
for _, treeErr := range e.Errors {
|
||||||
|
summary.NumErrors++
|
||||||
printer.E(" %v\n", treeErr)
|
printer.E(" %v\n", treeErr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
summary.NumErrors++
|
||||||
printer.E("error: %v\n", err)
|
printer.E("error: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,13 +358,14 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
// deadlocking in the case of errors.
|
// deadlocking in the case of errors.
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the following block only used for tests
|
||||||
if opts.CheckUnused {
|
if opts.CheckUnused {
|
||||||
unused, err := chkr.UnusedBlobs(ctx)
|
unused, err := chkr.UnusedBlobs(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return summary, err
|
||||||
}
|
}
|
||||||
for _, id := range unused {
|
for _, id := range unused {
|
||||||
printer.P("unused blob %v\n", id)
|
printer.P("unused blob %v\n", id)
|
||||||
@@ -364,16 +373,20 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doReadData := func(packs map[restic.ID]int64) {
|
readDataFilter, err := buildPacksFilter(opts, printer, chkr.IsFiltered())
|
||||||
packCount := uint64(len(packs))
|
if err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
|
||||||
p := newTerminalProgressMax(!gopts.Quiet, packCount, "packs", term)
|
if readDataFilter != nil {
|
||||||
|
p := printer.NewCounter("packs")
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
|
|
||||||
go chkr.ReadPacks(ctx, packs, p, errChan)
|
go chkr.ReadPacks(ctx, readDataFilter, p, errChan)
|
||||||
|
|
||||||
for err := range errChan {
|
for err := range errChan {
|
||||||
errorsFound = true
|
errorsFound = true
|
||||||
|
summary.NumErrors++
|
||||||
printer.E("%v\n", err)
|
printer.E("%v\n", err)
|
||||||
if err, ok := err.(*repository.ErrPackData); ok {
|
if err, ok := err.(*repository.ErrPackData); ok {
|
||||||
salvagePacks.Insert(err.PackID)
|
salvagePacks.Insert(err.PackID)
|
||||||
@@ -382,70 +395,85 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
p.Done()
|
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 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 errors.Fatal("internal error: failed to select packs to check")
|
|
||||||
}
|
|
||||||
doReadData(packs)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(salvagePacks) > 0 {
|
if len(salvagePacks) > 0 {
|
||||||
printer.E("\nThe repository contains damaged pack files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.\n\n")
|
printer.E("\nThe repository contains damaged pack files. These damaged files must be removed to repair the repository. This can be done using the following commands. Please read the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html first.\n\n")
|
||||||
var strIDs []string
|
|
||||||
for id := range salvagePacks {
|
for id := range salvagePacks {
|
||||||
strIDs = append(strIDs, id.String())
|
summary.BrokenPacks = append(summary.BrokenPacks, id.String())
|
||||||
}
|
}
|
||||||
printer.E("restic repair packs %v\nrestic repair snapshots --forget\n\n", strings.Join(strIDs, " "))
|
printer.E("restic repair packs %v\nrestic repair snapshots --forget\n\n", strings.Join(summary.BrokenPacks, " "))
|
||||||
printer.E("Damaged pack files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!\n")
|
printer.E("Damaged pack files can be caused by backend problems, hardware problems or bugs in restic. Please open an issue at https://github.com/restic/restic/issues/new/choose for further troubleshooting!\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
if errorsFound {
|
if errorsFound {
|
||||||
if len(salvagePacks) == 0 {
|
if len(salvagePacks) == 0 {
|
||||||
printer.E("\nThe repository is damaged and must be repaired. Please follow the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html .\n\n")
|
printer.E("\nThe repository is damaged and must be repaired. Please follow the troubleshooting guide at https://restic.readthedocs.io/en/stable/077_troubleshooting.html .\n\n")
|
||||||
}
|
}
|
||||||
return errors.Fatal("repository contains errors")
|
return summary, errors.Fatal("repository contains errors")
|
||||||
}
|
}
|
||||||
printer.P("no errors were found\n")
|
printer.P("no errors were found\n")
|
||||||
|
return summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
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.
|
// selectPacksByBucket selects subsets of packs by ranges of buckets.
|
||||||
@@ -491,3 +519,47 @@ func selectRandomPacksByFileSize(allPacks map[restic.ID]int64, subsetSize int64,
|
|||||||
packs := selectRandomPacksByPercentage(allPacks, subsetPercentage)
|
packs := selectRandomPacksByPercentage(allPacks, subsetPercentage)
|
||||||
return packs
|
return packs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type checkSummary struct {
|
||||||
|
MessageType string `json:"message_type"` // "summary"
|
||||||
|
NumErrors int `json:"num_errors"`
|
||||||
|
BrokenPacks []string `json:"broken_packs"` // run "restic repair packs ID..." and "restic repair snapshots --forget" to remove damaged files
|
||||||
|
HintRepairIndex bool `json:"suggest_repair_index"` // run "restic repair index"
|
||||||
|
HintPrune bool `json:"suggest_prune"` // run "restic prune"
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkError struct {
|
||||||
|
MessageType string `json:"message_type"` // "error"
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonErrorPrinter struct {
|
||||||
|
term ui.Terminal
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJSONErrorPrinter(term ui.Terminal) *jsonErrorPrinter {
|
||||||
|
return &jsonErrorPrinter{
|
||||||
|
term: term,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) *progress.Counter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
||||||
|
status := checkError{
|
||||||
|
MessageType: "error",
|
||||||
|
Message: fmt.Sprintf(msg, args...),
|
||||||
|
}
|
||||||
|
p.term.Error(ui.ToJSONString(status))
|
||||||
|
}
|
||||||
|
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
|
||||||
|
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
|
||||||
|
func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {}
|
||||||
|
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
|
||||||
|
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
|
||||||
|
|||||||
@@ -1,38 +1,101 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
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()
|
t.Helper()
|
||||||
output, err := testRunCheckOutput(gopts, true)
|
output, err := testRunCheckOutput(t, gopts, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(output)
|
t.Error(output)
|
||||||
t.Fatalf("unexpected error: %+v", err)
|
t.Fatalf("unexpected error: %+v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunCheckMustFail(t testing.TB, gopts GlobalOptions) {
|
func testRunCheckMustFail(t testing.TB, gopts global.Options) {
|
||||||
t.Helper()
|
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")
|
rtest.Assert(t, err != nil, "expected non nil error after check of damaged repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunCheckOutput(gopts GlobalOptions, checkUnused bool) (string, error) {
|
func testRunCheckOutput(t testing.TB, gopts global.Options, checkUnused bool) (string, error) {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
gopts.stdout = buf
|
|
||||||
err := withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
|
||||||
opts := CheckOptions{
|
opts := CheckOptions{
|
||||||
ReadData: true,
|
ReadData: true,
|
||||||
CheckUnused: checkUnused,
|
CheckUnused: checkUnused,
|
||||||
}
|
}
|
||||||
return runCheck(context.TODO(), opts, gopts, nil, term)
|
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.Term)
|
||||||
|
return err
|
||||||
})
|
})
|
||||||
return buf.String(), 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"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
@@ -202,7 +203,7 @@ func TestPrepareCheckCache(t *testing.T) {
|
|||||||
err := os.Remove(tmpDirBase)
|
err := os.Remove(tmpDirBase)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
gopts := GlobalOptions{CacheDir: tmpDirBase}
|
gopts := global.Options{CacheDir: tmpDirBase}
|
||||||
cleanup := prepareCheckCache(testCase.opts, &gopts, &progress.NoopPrinter{})
|
cleanup := prepareCheckCache(testCase.opts, &gopts, &progress.NoopPrinter{})
|
||||||
files, err := os.ReadDir(tmpDirBase)
|
files, err := os.ReadDir(tmpDirBase)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
@@ -232,7 +233,7 @@ func TestPrepareCheckCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareDefaultCheckCache(t *testing.T) {
|
func TestPrepareDefaultCheckCache(t *testing.T) {
|
||||||
gopts := GlobalOptions{CacheDir: ""}
|
gopts := global.Options{CacheDir: ""}
|
||||||
cleanup := prepareCheckCache(CheckOptions{}, &gopts, &progress.NoopPrinter{})
|
cleanup := prepareCheckCache(CheckOptions{}, &gopts, &progress.NoopPrinter{})
|
||||||
_, err := os.ReadDir(gopts.CacheDir)
|
_, err := os.ReadDir(gopts.CacheDir)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|||||||
@@ -3,20 +3,29 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"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/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdCopy = &cobra.Command{
|
func newCopyCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "copy [flags] [snapshotID ...]",
|
var opts CopyOptions
|
||||||
Short: "Copy snapshots from one repository to another",
|
cmd := &cobra.Command{
|
||||||
Long: `
|
Use: "copy [flags] [snapshotID ...]",
|
||||||
|
Short: "Copy snapshots from one repository to another",
|
||||||
|
Long: `
|
||||||
The "copy" command copies one or more snapshots from one repository to another.
|
The "copy" command copies one or more snapshots from one repository to another.
|
||||||
|
|
||||||
NOTE: This process will have to both download (read) and upload (write) the
|
NOTE: This process will have to both download (read) and upload (write) the
|
||||||
@@ -40,31 +49,65 @@ Exit status is 10 if the repository does not exist.
|
|||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runCopy(cmd.Context(), copyOptions, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
},
|
return runCopy(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyOptions bundles all options for the copy command.
|
// CopyOptions bundles all options for the copy command.
|
||||||
type CopyOptions struct {
|
type CopyOptions struct {
|
||||||
secondaryRepoOptions
|
global.SecondaryRepoOptions
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
var copyOptions CopyOptions
|
func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
opts.SecondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from")
|
||||||
func init() {
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||||
cmdRoot.AddCommand(cmdCopy)
|
|
||||||
|
|
||||||
f := cmdCopy.Flags()
|
|
||||||
initSecondaryRepoOptions(f, ©Options.secondaryRepoOptions, "destination", "to copy snapshots from")
|
|
||||||
initMultiSnapshotFilter(f, ©Options.SnapshotFilter, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []string) error {
|
// collectAllSnapshots: select all snapshot trees to be copied
|
||||||
secondaryGopts, isFromRepo, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "destination")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -73,13 +116,13 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
|
|||||||
gopts, secondaryGopts = secondaryGopts, gopts
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
ctx, dstRepo, unlock, err := openWithAppendLock(ctx, secondaryGopts, false)
|
ctx, dstRepo, unlock, err := openWithAppendLock(ctx, secondaryGopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -96,18 +139,16 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
|
|||||||
}
|
}
|
||||||
|
|
||||||
debug.Log("Loading source index")
|
debug.Log("Loading source index")
|
||||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
if err := srcRepo.LoadIndex(ctx, printer); err != nil {
|
||||||
if err := srcRepo.LoadIndex(ctx, bar); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bar = newIndexProgress(gopts.Quiet, gopts.JSON)
|
|
||||||
debug.Log("Loading destination index")
|
debug.Log("Loading destination index")
|
||||||
if err := dstRepo.LoadIndex(ctx, bar); err != nil {
|
if err := dstRepo.LoadIndex(ctx, printer); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot)
|
dstSnapshotByOriginal := make(map[restic.ID][]*data.Snapshot)
|
||||||
for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil) {
|
for sn := range FindFilteredSnapshots(ctx, dstSnapshotLister, dstRepo, &opts.SnapshotFilter, nil, printer) {
|
||||||
if sn.Original != nil && !sn.Original.IsNull() {
|
if sn.Original != nil && !sn.Original.IsNull() {
|
||||||
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
||||||
}
|
}
|
||||||
@@ -118,53 +159,16 @@ func runCopy(ctx context.Context, opts CopyOptions, gopts GlobalOptions, args []
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// remember already processed trees across all snapshots
|
selectedSnapshots := collectAllSnapshots(ctx, opts, srcSnapshotLister, srcRepo, dstSnapshotByOriginal, args, printer)
|
||||||
visitedTrees := restic.NewIDSet()
|
|
||||||
|
|
||||||
for sn := range FindFilteredSnapshots(ctx, srcSnapshotLister, srcRepo, &opts.SnapshotFilter, args) {
|
if err := copyTreeBatched(ctx, srcRepo, dstRepo, selectedSnapshots, printer); err != nil {
|
||||||
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
|
return err
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.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
|
// everything except Parent and Original must match
|
||||||
if !sna.Time.Equal(snb.Time) || !sna.Tree.Equal(*snb.Tree) || sna.Hostname != snb.Hostname ||
|
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 ||
|
sna.Username != snb.Username || sna.UID != snb.UID || sna.GID != snb.GID ||
|
||||||
@@ -183,64 +187,158 @@ func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
// copyTreeBatched copies multiple snapshots in one go. Snapshots are written after
|
||||||
visitedTrees restic.IDSet, rootTreeID restic.ID, quiet bool) error {
|
// 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 {
|
targetSize := uint64(dstRepo.PackSize()) * 100
|
||||||
visited := visitedTrees.Has(treeID)
|
minDuration := 1 * time.Minute
|
||||||
visitedTrees.Insert(treeID)
|
|
||||||
return visited
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
copyBlobs := restic.NewBlobSet()
|
// use pull-based iterator to allow iteration in multiple steps
|
||||||
packList := restic.NewIDSet()
|
next, stop := iter.Pull(selectedSnapshots)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
enqueue := func(h restic.BlobHandle) {
|
for {
|
||||||
pb := srcRepo.LookupBlob(h.Type, h.ID)
|
var batch []*data.Snapshot
|
||||||
copyBlobs.Insert(h)
|
batchSize := uint64(0)
|
||||||
for _, p := range pb {
|
startTime := time.Now()
|
||||||
packList.Insert(p.PackID)
|
|
||||||
|
// 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 {
|
return nil
|
||||||
for tree := range treeStream {
|
}
|
||||||
if tree.Error != nil {
|
|
||||||
return fmt.Errorf("LoadTree(%v) returned error %v", tree.ID.Str(), tree.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do we already have this tree blob?
|
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
||||||
treeHandle := restic.BlobHandle{ID: tree.ID, Type: restic.TreeBlob}
|
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer progress.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
|
||||||
if _, ok := dstRepo.LookupBlobSize(treeHandle.Type, treeHandle.ID); !ok {
|
|
||||||
// copy raw tree bytes to avoid problems if the serialization changes
|
|
||||||
enqueue(treeHandle)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range tree.Nodes {
|
copyBlobs := srcRepo.NewAssociatedBlobSet()
|
||||||
// Recursion into directories is handled by StreamTrees
|
packList := restic.NewIDSet()
|
||||||
// Copy the blobs for this file.
|
var lock sync.Mutex
|
||||||
for _, blobID := range entry.Content {
|
|
||||||
h := restic.BlobHandle{Type: restic.DataBlob, ID: blobID}
|
enqueue := func(h restic.BlobHandle) {
|
||||||
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
|
lock.Lock()
|
||||||
enqueue(h)
|
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
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
printer.P("snapshot %s saved, copied from source snapshot %s", newID.Str(), sn.ID().Str())
|
||||||
bar := newProgressMax(!quiet, uint64(len(packList)), "packs copied")
|
|
||||||
_, err = repository.Repack(ctx, srcRepo, dstRepo, packList, copyBlobs, bar)
|
|
||||||
bar.Done()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,28 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
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 := srcGopts
|
||||||
gopts.Repo = dstGopts.Repo
|
gopts.Repo = dstGopts.Repo
|
||||||
gopts.password = dstGopts.password
|
gopts.Password = dstGopts.Password
|
||||||
gopts.InsecureNoPassword = dstGopts.InsecureNoPassword
|
gopts.InsecureNoPassword = dstGopts.InsecureNoPassword
|
||||||
copyOpts := CopyOptions{
|
copyOpts := CopyOptions{
|
||||||
secondaryRepoOptions: secondaryRepoOptions{
|
SecondaryRepoOptions: global.SecondaryRepoOptions{
|
||||||
Repo: srcGopts.Repo,
|
Repo: srcGopts.Repo,
|
||||||
password: srcGopts.password,
|
Password: srcGopts.Password,
|
||||||
InsecureNoPassword: srcGopts.InsecureNoPassword,
|
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) {
|
func TestCopy(t *testing.T) {
|
||||||
@@ -45,8 +50,8 @@ func TestCopy(t *testing.T) {
|
|||||||
copiedSnapshotIDs := testListSnapshots(t, env2.gopts, 3)
|
copiedSnapshotIDs := testListSnapshots(t, env2.gopts, 3)
|
||||||
|
|
||||||
// Check that the copies size seems reasonable
|
// Check that the copies size seems reasonable
|
||||||
stat := dirStats(env.repo)
|
stat := dirStats(t, env.repo)
|
||||||
stat2 := dirStats(env2.repo)
|
stat2 := dirStats(t, env2.repo)
|
||||||
sizeDiff := int64(stat.size) - int64(stat2.size)
|
sizeDiff := int64(stat.size) - int64(stat2.size)
|
||||||
if sizeDiff < 0 {
|
if sizeDiff < 0 {
|
||||||
sizeDiff = -sizeDiff
|
sizeDiff = -sizeDiff
|
||||||
@@ -69,7 +74,7 @@ func TestCopy(t *testing.T) {
|
|||||||
testRunRestore(t, env2.gopts, restoredir, snapshotID.String())
|
testRunRestore(t, env2.gopts, restoredir, snapshotID.String())
|
||||||
foundMatch := false
|
foundMatch := false
|
||||||
for cmpdir := range origRestores {
|
for cmpdir := range origRestores {
|
||||||
diff := directoriesContentsDiff(restoredir, cmpdir)
|
diff := directoriesContentsDiff(t, restoredir, cmpdir)
|
||||||
if diff == "" {
|
if diff == "" {
|
||||||
delete(origRestores, cmpdir)
|
delete(origRestores, cmpdir)
|
||||||
foundMatch = true
|
foundMatch = true
|
||||||
@@ -80,6 +85,41 @@ func TestCopy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rtest.Assert(t, len(origRestores) == 0, "found not copied snapshots")
|
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 boths 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) {
|
func TestCopyIncremental(t *testing.T) {
|
||||||
@@ -142,7 +182,7 @@ func TestCopyToEmptyPassword(t *testing.T) {
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
env2, cleanup2 := withTestEnvironment(t)
|
env2, cleanup2 := withTestEnvironment(t)
|
||||||
defer cleanup2()
|
defer cleanup2()
|
||||||
env2.gopts.password = ""
|
env2.gopts.Password = ""
|
||||||
env2.gopts.InsecureNoPassword = true
|
env2.gopts.InsecureNoPassword = true
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build debug
|
//go:build debug
|
||||||
// +build debug
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -18,27 +17,44 @@ import (
|
|||||||
|
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/crypto"
|
"github.com/restic/restic/internal/crypto"
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/repository/index"
|
"github.com/restic/restic/internal/repository/index"
|
||||||
"github.com/restic/restic/internal/repository/pack"
|
"github.com/restic/restic/internal/repository/pack"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdDebug = &cobra.Command{
|
func registerDebugCommand(cmd *cobra.Command, globalOptions *global.Options) {
|
||||||
Use: "debug",
|
cmd.AddCommand(
|
||||||
Short: "Debug commands",
|
newDebugCommand(globalOptions),
|
||||||
GroupID: cmdGroupDefault,
|
)
|
||||||
DisableAutoGenTag: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdDebugDump = &cobra.Command{
|
func newDebugCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "dump [indexes|snapshots|all|packs]",
|
cmd := &cobra.Command{
|
||||||
Short: "Dump data structures",
|
Use: "debug",
|
||||||
Long: `
|
Short: "Debug commands",
|
||||||
|
GroupID: cmdGroupDefault,
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
}
|
||||||
|
cmd.AddCommand(newDebugDumpCommand(globalOptions))
|
||||||
|
cmd.AddCommand(newDebugExamineCommand(globalOptions))
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDebugDumpCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "dump [indexes|snapshots|all|packs]",
|
||||||
|
Short: "Dump data structures",
|
||||||
|
Long: `
|
||||||
The "dump" command dumps data structures from the repository as JSON objects. It
|
The "dump" command dumps data structures from the repository as JSON objects. It
|
||||||
is used for debugging purposes only.
|
is used for debugging purposes only.
|
||||||
|
|
||||||
@@ -51,10 +67,28 @@ Exit status is 10 if the repository does not exist.
|
|||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDebugDump(cmd.Context(), globalOptions, args)
|
return runDebugDump(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDebugExamineCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
|
var opts DebugExamineOptions
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "examine pack-ID...",
|
||||||
|
Short: "Examine a pack file",
|
||||||
|
DisableAutoGenTag: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runDebugExamine(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type DebugExamineOptions struct {
|
type DebugExamineOptions struct {
|
||||||
@@ -64,16 +98,11 @@ type DebugExamineOptions struct {
|
|||||||
ReuploadBlobs bool
|
ReuploadBlobs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugExamineOpts DebugExamineOptions
|
func (opts *DebugExamineOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&opts.ExtractPack, "extract-pack", false, "write blobs to the current directory")
|
||||||
func init() {
|
f.BoolVar(&opts.ReuploadBlobs, "reupload-blobs", false, "reupload blobs to the repository")
|
||||||
cmdRoot.AddCommand(cmdDebug)
|
f.BoolVar(&opts.TryRepair, "try-repair", false, "try to repair broken blobs with single bit flips")
|
||||||
cmdDebug.AddCommand(cmdDebugDump)
|
f.BoolVar(&opts.RepairByte, "repair-byte", false, "try to repair broken blobs by trying bytes")
|
||||||
cmdDebug.AddCommand(cmdDebugExamine)
|
|
||||||
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.ExtractPack, "extract-pack", false, "write blobs to the current directory")
|
|
||||||
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.ReuploadBlobs, "reupload-blobs", false, "reupload blobs to the repository")
|
|
||||||
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.TryRepair, "try-repair", false, "try to repair broken blobs with single bit flips")
|
|
||||||
cmdDebugExamine.Flags().BoolVar(&debugExamineOpts.RepairByte, "repair-byte", false, "try to repair broken blobs by trying bytes")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func prettyPrintJSON(wr io.Writer, item interface{}) error {
|
func prettyPrintJSON(wr io.Writer, item interface{}) error {
|
||||||
@@ -87,12 +116,14 @@ func prettyPrintJSON(wr io.Writer, item interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io.Writer) 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(wr, "snapshot_id: %v\n", id)
|
if _, err := fmt.Fprintf(wr, "snapshot_id: %v\n", id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return prettyPrintJSON(wr, snapshot)
|
return prettyPrintJSON(wr, snapshot)
|
||||||
})
|
})
|
||||||
@@ -113,13 +144,13 @@ type Blob struct {
|
|||||||
Offset uint `json:"offset"`
|
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
|
var m sync.Mutex
|
||||||
return restic.ParallelList(ctx, repo, restic.PackFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error {
|
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)
|
blobs, _, err := repo.ListPack(ctx, id, size)
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +173,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, oldFormat bool, err error) error {
|
return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error {
|
||||||
Printf("index_id: %v\n", id)
|
printer.S("index_id: %v", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -153,12 +184,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 {
|
if len(args) != 1 {
|
||||||
return errors.Fatal("type not specified")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -168,20 +201,20 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
|
|||||||
|
|
||||||
switch tpe {
|
switch tpe {
|
||||||
case "indexes":
|
case "indexes":
|
||||||
return dumpIndexes(ctx, repo, globalOptions.stdout)
|
return dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||||
case "snapshots":
|
case "snapshots":
|
||||||
return debugPrintSnapshots(ctx, repo, globalOptions.stdout)
|
return debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
||||||
case "packs":
|
case "packs":
|
||||||
return printPacks(ctx, repo, globalOptions.stdout)
|
return printPacks(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||||
case "all":
|
case "all":
|
||||||
Printf("snapshots:\n")
|
printer.S("snapshots:")
|
||||||
err := debugPrintSnapshots(ctx, repo, globalOptions.stdout)
|
err := debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Printf("\nindexes:\n")
|
printer.S("indexes:")
|
||||||
err = dumpIndexes(ctx, repo, globalOptions.stdout)
|
err = dumpIndexes(ctx, repo, gopts.Term.OutputWriter(), printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -192,20 +225,11 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdDebugExamine = &cobra.Command{
|
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer progress.Printer) []byte {
|
||||||
Use: "examine pack-ID...",
|
|
||||||
Short: "Examine a pack file",
|
|
||||||
DisableAutoGenTag: true,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
return runDebugExamine(cmd.Context(), globalOptions, debugExamineOpts, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, bytewise bool) []byte {
|
|
||||||
if bytewise {
|
if bytewise {
|
||||||
Printf(" trying to repair blob by finding a broken byte\n")
|
printer.S(" trying to repair blob by finding a broken byte")
|
||||||
} else {
|
} 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)
|
ch := make(chan int)
|
||||||
@@ -215,7 +239,7 @@ func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, by
|
|||||||
var found bool
|
var found bool
|
||||||
|
|
||||||
workers := runtime.GOMAXPROCS(0)
|
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++ {
|
for i := 0; i < workers; i++ {
|
||||||
wg.Go(func() error {
|
wg.Go(func() error {
|
||||||
// make a local copy of the buffer
|
// make a local copy of the buffer
|
||||||
@@ -229,9 +253,9 @@ func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, by
|
|||||||
nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
|
nonce, plaintext := buf[:key.NonceSize()], buf[key.NonceSize():]
|
||||||
plaintext, err := key.Open(plaintext[:0], nonce, plaintext, nil)
|
plaintext, err := key.Open(plaintext[:0], nonce, plaintext, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
Printf("\n")
|
printer.S("")
|
||||||
Printf(" blob could be repaired by XORing byte %v with 0x%02x\n", idx, pattern)
|
printer.S(" blob could be repaired by XORing byte %v with 0x%02x", idx, pattern)
|
||||||
Printf(" hash is %v\n", restic.Hash(plaintext))
|
printer.S(" hash is %v", restic.Hash(plaintext))
|
||||||
close(done)
|
close(done)
|
||||||
found = true
|
found = true
|
||||||
fixed = plaintext
|
fixed = plaintext
|
||||||
@@ -272,7 +296,7 @@ func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, by
|
|||||||
select {
|
select {
|
||||||
case ch <- i:
|
case ch <- i:
|
||||||
case <-done:
|
case <-done:
|
||||||
Printf(" done after %v\n", time.Since(start))
|
printer.S(" done after %v", time.Since(start))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +306,7 @@ func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, by
|
|||||||
remaining := len(input) - i
|
remaining := len(input) - i
|
||||||
eta := time.Duration(float64(remaining)/gps) * time.Second
|
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)
|
i, len(input), float32(i)/float32(len(input))*100, gps, eta)
|
||||||
info = time.Now()
|
info = time.Now()
|
||||||
}
|
}
|
||||||
@@ -295,12 +319,12 @@ func tryRepairWithBitflip(ctx context.Context, key *crypto.Key, input []byte, by
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
Printf("\n blob could not be repaired\n")
|
printer.S("\n blob could not be repaired")
|
||||||
}
|
}
|
||||||
return fixed
|
return fixed
|
||||||
}
|
}
|
||||||
|
|
||||||
func decryptUnsigned(ctx context.Context, k *crypto.Key, buf []byte) []byte {
|
func decryptUnsigned(k *crypto.Key, buf []byte) []byte {
|
||||||
// strip signature at the end
|
// strip signature at the end
|
||||||
l := len(buf)
|
l := len(buf)
|
||||||
nonce, ct := buf[:16], buf[16:l-16]
|
nonce, ct := buf[:16], buf[16:l-16]
|
||||||
@@ -316,7 +340,7 @@ func decryptUnsigned(ctx context.Context, k *crypto.Key, buf []byte) []byte {
|
|||||||
return out
|
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.Blob, printer progress.Printer) error {
|
||||||
dec, err := zstd.NewReader(nil)
|
dec, err := zstd.NewReader(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -328,17 +352,11 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
wg, ctx := errgroup.WithContext(ctx)
|
err = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||||
|
|
||||||
if opts.ReuploadBlobs {
|
|
||||||
repo.StartPackUploader(ctx, wg)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Go(func() error {
|
|
||||||
for _, blob := range list {
|
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) {
|
if int(blob.Offset+blob.Length) > len(pack) {
|
||||||
Warnf("skipping truncated blob\n")
|
printer.E("skipping truncated blob")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
buf := pack[blob.Offset : blob.Offset+blob.Length]
|
buf := pack[blob.Offset : blob.Offset+blob.Length]
|
||||||
@@ -349,16 +367,16 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
|||||||
outputPrefix := ""
|
outputPrefix := ""
|
||||||
filePrefix := ""
|
filePrefix := ""
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error decrypting blob: %v\n", err)
|
printer.E("error decrypting blob: %v", err)
|
||||||
if opts.TryRepair || opts.RepairByte {
|
if opts.TryRepair || opts.RepairByte {
|
||||||
plaintext = tryRepairWithBitflip(ctx, key, buf, opts.RepairByte)
|
plaintext = tryRepairWithBitflip(key, buf, opts.RepairByte, printer)
|
||||||
}
|
}
|
||||||
if plaintext != nil {
|
if plaintext != nil {
|
||||||
outputPrefix = "repaired "
|
outputPrefix = "repaired "
|
||||||
filePrefix = "repaired-"
|
filePrefix = "repaired-"
|
||||||
} else {
|
} else {
|
||||||
plaintext = decryptUnsigned(ctx, key, buf)
|
plaintext = decryptUnsigned(key, buf)
|
||||||
err = storePlainBlob(blob.ID, "damaged-", plaintext)
|
err = storePlainBlob(blob.ID, "damaged-", plaintext, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -369,7 +387,7 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
|||||||
if blob.IsCompressed() {
|
if blob.IsCompressed() {
|
||||||
decompressed, err := dec.DecodeAll(plaintext, nil)
|
decompressed, err := dec.DecodeAll(plaintext, nil)
|
||||||
if err != 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 {
|
if decompressed != nil {
|
||||||
plaintext = decompressed
|
plaintext = decompressed
|
||||||
@@ -379,37 +397,32 @@ func loadBlobs(ctx context.Context, opts DebugExamineOptions, repo restic.Reposi
|
|||||||
id := restic.Hash(plaintext)
|
id := restic.Hash(plaintext)
|
||||||
var prefix string
|
var prefix string
|
||||||
if !id.Equal(blob.ID) {
|
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-"
|
prefix = "wrong-hash-"
|
||||||
} else {
|
} 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-"
|
prefix = "correct-"
|
||||||
}
|
}
|
||||||
if opts.ExtractPack {
|
if opts.ExtractPack {
|
||||||
err = storePlainBlob(id, filePrefix+prefix, plaintext)
|
err = storePlainBlob(id, filePrefix+prefix, plaintext, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opts.ReuploadBlobs {
|
if opts.ReuploadBlobs {
|
||||||
_, _, _, err := repo.SaveBlob(ctx, blob.Type, plaintext, id, true)
|
_, _, _, err := uploader.SaveBlob(ctx, blob.Type, plaintext, id, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 nil
|
||||||
})
|
})
|
||||||
|
return err
|
||||||
return wg.Wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
filename := fmt.Sprintf("%s%s.bin", prefix, id)
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -427,16 +440,18 @@ func storePlainBlob(id restic.ID, prefix string, plain []byte) error {
|
|||||||
return err
|
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
|
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 {
|
if opts.ExtractPack && gopts.NoLock {
|
||||||
return fmt.Errorf("--extract-pack and --no-lock are mutually exclusive")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -448,7 +463,7 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
id, err = restic.Find(ctx, repo, restic.PackFile, name)
|
id, err = restic.Find(ctx, repo, restic.PackFile, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error: %v\n", err)
|
printer.E("error: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,16 +474,15 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
|
|||||||
return errors.Fatal("no pack files to examine")
|
return errors.Fatal("no pack files to examine")
|
||||||
}
|
}
|
||||||
|
|
||||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
err = repo.LoadIndex(ctx, printer)
|
||||||
err = repo.LoadIndex(ctx, bar)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
err := examinePack(ctx, opts, repo, id)
|
err := examinePack(ctx, opts, repo, id, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error: %v\n", err)
|
printer.E("error: %v", err)
|
||||||
}
|
}
|
||||||
if err == context.Canceled {
|
if err == context.Canceled {
|
||||||
break
|
break
|
||||||
@@ -477,24 +491,24 @@ func runDebugExamine(ctx context.Context, gopts GlobalOptions, opts DebugExamine
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID) error {
|
func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repository, id restic.ID, printer progress.Printer) error {
|
||||||
Printf("examine %v\n", id)
|
printer.S("examine %v", id)
|
||||||
|
|
||||||
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
|
buf, err := repo.LoadRaw(ctx, restic.PackFile, id)
|
||||||
// also process damaged pack files
|
// also process damaged pack files
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
Printf(" file size is %v\n", len(buf))
|
printer.S(" file size is %v", len(buf))
|
||||||
gotID := restic.Hash(buf)
|
gotID := restic.Hash(buf)
|
||||||
if !id.Equal(gotID) {
|
if !id.Equal(gotID) {
|
||||||
Printf(" wanted hash %v, got %v\n", id, gotID)
|
printer.S(" wanted hash %v, got %v", id, gotID)
|
||||||
} else {
|
} else {
|
||||||
Printf(" hash for file content matches\n")
|
printer.S(" hash for file content matches")
|
||||||
}
|
}
|
||||||
|
|
||||||
Printf(" ========================================\n")
|
printer.S(" ========================================")
|
||||||
Printf(" looking for info in the indexes\n")
|
printer.S(" looking for info in the indexes")
|
||||||
|
|
||||||
blobsLoaded := false
|
blobsLoaded := false
|
||||||
// examine all data the indexes have for the pack file
|
// examine all data the indexes have for the pack file
|
||||||
@@ -504,32 +518,32 @@ func examinePack(ctx context.Context, opts DebugExamineOptions, repo restic.Repo
|
|||||||
continue
|
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 {
|
if err != nil {
|
||||||
Warnf("error: %v\n", err)
|
printer.E("error: %v", err)
|
||||||
} else {
|
} else {
|
||||||
blobsLoaded = true
|
blobsLoaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Printf(" ========================================\n")
|
printer.S(" ========================================")
|
||||||
Printf(" inspect the pack itself\n")
|
printer.S(" inspect the pack itself")
|
||||||
|
|
||||||
blobs, _, err := repo.ListPack(ctx, id, int64(len(buf)))
|
blobs, _, err := repo.ListPack(ctx, id, int64(len(buf)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("pack %v: %v", id.Str(), err)
|
return fmt.Errorf("pack %v: %v", id.Str(), err)
|
||||||
}
|
}
|
||||||
checkPackSize(blobs, len(buf))
|
checkPackSize(blobs, len(buf), printer)
|
||||||
|
|
||||||
if !blobsLoaded {
|
if !blobsLoaded {
|
||||||
return loadBlobs(ctx, opts, repo, id, blobs)
|
return loadBlobs(ctx, opts, repo, id, blobs, printer)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPackSize(blobs []restic.Blob, fileSize int) {
|
func checkPackSize(blobs []restic.Blob, fileSize int, printer progress.Printer) {
|
||||||
// track current size and offset
|
// track current size and offset
|
||||||
var size, offset uint64
|
var size, offset uint64
|
||||||
|
|
||||||
@@ -538,9 +552,9 @@ func checkPackSize(blobs []restic.Blob, fileSize int) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, pb := range blobs {
|
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) {
|
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)
|
offset = uint64(pb.Offset + pb.Length)
|
||||||
size += uint64(pb.Length)
|
size += uint64(pb.Length)
|
||||||
@@ -548,8 +562,8 @@ func checkPackSize(blobs []restic.Blob, fileSize int) {
|
|||||||
size += uint64(pack.CalculateHeaderSize(blobs))
|
size += uint64(pack.CalculateHeaderSize(blobs))
|
||||||
|
|
||||||
if uint64(fileSize) != size {
|
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 {
|
} else {
|
||||||
Printf(" file sizes match\n")
|
printer.S(" file sizes match")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
cmd/restic/cmd_debug_disabled.go
Normal file
12
cmd/restic/cmd_debug_disabled.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !debug
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerDebugCommand(_ *cobra.Command, _ *global.Options) {
|
||||||
|
// No commands to register in non-debug mode
|
||||||
|
}
|
||||||
@@ -5,19 +5,24 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdDiff = &cobra.Command{
|
func newDiffCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "diff [flags] snapshotID snapshotID",
|
var opts DiffOptions
|
||||||
Short: "Show differences between two snapshots",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "diff [flags] snapshotID snapshotID",
|
||||||
|
Short: "Show differences between two snapshots",
|
||||||
|
Long: `
|
||||||
The "diff" command shows differences from the first to the second snapshot. The
|
The "diff" command shows differences from the first to the second snapshot. The
|
||||||
first characters in each line display what has happened to a particular file or
|
first characters in each line display what has happened to a particular file or
|
||||||
directory:
|
directory:
|
||||||
@@ -45,11 +50,15 @@ Exit status is 10 if the repository does not exist.
|
|||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDiff(cmd.Context(), diffOptions, globalOptions, args)
|
return runDiff(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiffOptions collects all options for the diff command.
|
// DiffOptions collects all options for the diff command.
|
||||||
@@ -57,19 +66,14 @@ type DiffOptions struct {
|
|||||||
ShowMetadata bool
|
ShowMetadata bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var diffOptions DiffOptions
|
func (opts *DiffOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.BoolVar(&opts.ShowMetadata, "metadata", false, "print changes in metadata")
|
||||||
func init() {
|
|
||||||
cmdRoot.AddCommand(cmdDiff)
|
|
||||||
|
|
||||||
f := cmdDiff.Flags()
|
|
||||||
f.BoolVar(&diffOptions.ShowMetadata, "metadata", false, "print changes in metadata")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*restic.Snapshot, string, error) {
|
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*data.Snapshot, string, error) {
|
||||||
sn, subfolder, err := restic.FindSnapshot(ctx, be, repo, desc)
|
sn, subfolder, err := data.FindSnapshot(ctx, be, repo, desc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", errors.Fatal(err.Error())
|
return nil, "", errors.Fatalf("%s", err)
|
||||||
}
|
}
|
||||||
return sn, subfolder, err
|
return sn, subfolder, err
|
||||||
}
|
}
|
||||||
@@ -79,6 +83,7 @@ type Comparer struct {
|
|||||||
repo restic.BlobLoader
|
repo restic.BlobLoader
|
||||||
opts DiffOptions
|
opts DiffOptions
|
||||||
printChange func(change *Change)
|
printChange func(change *Change)
|
||||||
|
printError func(string, ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type Change struct {
|
type Change struct {
|
||||||
@@ -102,15 +107,15 @@ type DiffStat struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add adds stats information for node to s.
|
// 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 {
|
if node == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch node.Type {
|
switch node.Type {
|
||||||
case "file":
|
case data.NodeTypeFile:
|
||||||
s.Files++
|
s.Files++
|
||||||
case "dir":
|
case data.NodeTypeDir:
|
||||||
s.Dirs++
|
s.Dirs++
|
||||||
default:
|
default:
|
||||||
s.Others++
|
s.Others++
|
||||||
@@ -118,13 +123,13 @@ func (s *DiffStat) Add(node *restic.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addBlobs adds the blobs of node to s.
|
// 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 {
|
if node == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch node.Type {
|
switch node.Type {
|
||||||
case "file":
|
case data.NodeTypeFile:
|
||||||
for _, blob := range node.Content {
|
for _, blob := range node.Content {
|
||||||
h := restic.BlobHandle{
|
h := restic.BlobHandle{
|
||||||
ID: blob,
|
ID: blob,
|
||||||
@@ -132,7 +137,7 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
|||||||
}
|
}
|
||||||
bs.Insert(h)
|
bs.Insert(h)
|
||||||
}
|
}
|
||||||
case "dir":
|
case data.NodeTypeDir:
|
||||||
h := restic.BlobHandle{
|
h := restic.BlobHandle{
|
||||||
ID: *node.Subtree,
|
ID: *node.Subtree,
|
||||||
Type: restic.TreeBlob,
|
Type: restic.TreeBlob,
|
||||||
@@ -142,18 +147,18 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DiffStatsContainer struct {
|
type DiffStatsContainer struct {
|
||||||
MessageType string `json:"message_type"` // "statistics"
|
MessageType string `json:"message_type"` // "statistics"
|
||||||
SourceSnapshot string `json:"source_snapshot"`
|
SourceSnapshot string `json:"source_snapshot"`
|
||||||
TargetSnapshot string `json:"target_snapshot"`
|
TargetSnapshot string `json:"target_snapshot"`
|
||||||
ChangedFiles int `json:"changed_files"`
|
ChangedFiles int `json:"changed_files"`
|
||||||
Added DiffStat `json:"added"`
|
Added DiffStat `json:"added"`
|
||||||
Removed DiffStat `json:"removed"`
|
Removed DiffStat `json:"removed"`
|
||||||
BlobsBefore, BlobsAfter, BlobsCommon restic.BlobSet `json:"-"`
|
BlobsBefore, BlobsAfter, BlobsCommon restic.AssociatedBlobSet `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateBlobs updates the blob counters in the stats struct.
|
// updateBlobs updates the blob counters in the stats struct.
|
||||||
func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) {
|
func updateBlobs(repo restic.Loader, blobs restic.AssociatedBlobSet, stats *DiffStat, printError func(string, ...interface{})) {
|
||||||
for h := range blobs {
|
for h := range blobs.Keys() {
|
||||||
switch h.Type {
|
switch h.Type {
|
||||||
case restic.DataBlob:
|
case restic.DataBlob:
|
||||||
stats.DataBlobs++
|
stats.DataBlobs++
|
||||||
@@ -163,7 +168,7 @@ func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) {
|
|||||||
|
|
||||||
size, found := repo.LookupBlobSize(h.Type, h.ID)
|
size, found := repo.LookupBlobSize(h.Type, h.ID)
|
||||||
if !found {
|
if !found {
|
||||||
Warnf("unable to find blob size for %v\n", h)
|
printError("unable to find blob size for %v", h)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, node := range tree.Nodes {
|
for item := range tree {
|
||||||
|
if item.Error != nil {
|
||||||
|
return item.Error
|
||||||
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
node := item.Node
|
||||||
name := path.Join(prefix, node.Name)
|
name := path.Join(prefix, node.Name)
|
||||||
if node.Type == "dir" {
|
if node.Type == data.NodeTypeDir {
|
||||||
name += "/"
|
name += "/"
|
||||||
}
|
}
|
||||||
c.printChange(NewChange(name, mode))
|
c.printChange(NewChange(name, mode))
|
||||||
stats.Add(node)
|
stats.Add(node)
|
||||||
addBlobs(blobs, node)
|
addBlobs(blobs, node)
|
||||||
|
|
||||||
if node.Type == "dir" {
|
if node.Type == data.NodeTypeDir {
|
||||||
err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree)
|
err := c.printDir(ctx, mode, stats, blobs, name, *node.Subtree)
|
||||||
if err != nil && err != context.Canceled {
|
if err != nil && err != context.Canceled {
|
||||||
Warnf("error: %v\n", err)
|
c.printError("error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,24 +210,28 @@ func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, b
|
|||||||
return ctx.Err()
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, node := range tree.Nodes {
|
for item := range tree {
|
||||||
|
if item.Error != nil {
|
||||||
|
return item.Error
|
||||||
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node := item.Node
|
||||||
addBlobs(blobs, node)
|
addBlobs(blobs, node)
|
||||||
|
|
||||||
if node.Type == "dir" {
|
if node.Type == data.NodeTypeDir {
|
||||||
err := c.collectDir(ctx, blobs, *node.Subtree)
|
err := c.collectDir(ctx, blobs, *node.Subtree)
|
||||||
if err != nil && err != context.Canceled {
|
if err != nil && err != context.Canceled {
|
||||||
Warnf("error: %v\n", err)
|
c.printError("error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,56 +239,41 @@ func (c *Comparer) collectDir(ctx context.Context, blobs restic.BlobSet, id rest
|
|||||||
return ctx.Err()
|
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 {
|
func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, prefix string, id1, id2 restic.ID) error {
|
||||||
debug.Log("diffing %v to %v", id1, id2)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tree2, err := restic.LoadTree(ctx, c.repo, id2)
|
tree2, err := data.LoadTree(ctx, c.repo, id2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tree1Nodes, tree2Nodes, names := uniqueNodeNames(tree1, tree2)
|
for dt := range data.DualTreeIterator(tree1, tree2) {
|
||||||
|
if dt.Error != nil {
|
||||||
for _, name := range names {
|
return dt.Error
|
||||||
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
node1, t1 := tree1Nodes[name]
|
node1 := dt.Tree1
|
||||||
node2, t2 := tree2Nodes[name]
|
node2 := dt.Tree2
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if node1 != nil {
|
||||||
|
name = node1.Name
|
||||||
|
} else {
|
||||||
|
name = node2.Name
|
||||||
|
}
|
||||||
|
|
||||||
addBlobs(stats.BlobsBefore, node1)
|
addBlobs(stats.BlobsBefore, node1)
|
||||||
addBlobs(stats.BlobsAfter, node2)
|
addBlobs(stats.BlobsAfter, node2)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case t1 && t2:
|
case node1 != nil && node2 != nil:
|
||||||
name := path.Join(prefix, name)
|
name := path.Join(prefix, name)
|
||||||
mod := ""
|
mod := ""
|
||||||
|
|
||||||
@@ -284,12 +281,12 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
|||||||
mod += "T"
|
mod += "T"
|
||||||
}
|
}
|
||||||
|
|
||||||
if node2.Type == "dir" {
|
if node2.Type == data.NodeTypeDir {
|
||||||
name += "/"
|
name += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
if node1.Type == "file" &&
|
if node1.Type == data.NodeTypeFile &&
|
||||||
node2.Type == "file" &&
|
node2.Type == data.NodeTypeFile &&
|
||||||
!reflect.DeepEqual(node1.Content, node2.Content) {
|
!reflect.DeepEqual(node1.Content, node2.Content) {
|
||||||
mod += "M"
|
mod += "M"
|
||||||
stats.ChangedFiles++
|
stats.ChangedFiles++
|
||||||
@@ -311,7 +308,7 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
|||||||
c.printChange(NewChange(name, mod))
|
c.printChange(NewChange(name, mod))
|
||||||
}
|
}
|
||||||
|
|
||||||
if node1.Type == "dir" && node2.Type == "dir" {
|
if node1.Type == data.NodeTypeDir && node2.Type == data.NodeTypeDir {
|
||||||
var err error
|
var err error
|
||||||
if (*node1.Subtree).Equal(*node2.Subtree) {
|
if (*node1.Subtree).Equal(*node2.Subtree) {
|
||||||
err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree)
|
err = c.collectDir(ctx, stats.BlobsCommon, *node1.Subtree)
|
||||||
@@ -319,35 +316,35 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
|||||||
err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
err = c.diffTree(ctx, stats, name, *node1.Subtree, *node2.Subtree)
|
||||||
}
|
}
|
||||||
if err != nil && err != context.Canceled {
|
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)
|
prefix := path.Join(prefix, name)
|
||||||
if node1.Type == "dir" {
|
if node1.Type == data.NodeTypeDir {
|
||||||
prefix += "/"
|
prefix += "/"
|
||||||
}
|
}
|
||||||
c.printChange(NewChange(prefix, "-"))
|
c.printChange(NewChange(prefix, "-"))
|
||||||
stats.Removed.Add(node1)
|
stats.Removed.Add(node1)
|
||||||
|
|
||||||
if node1.Type == "dir" {
|
if node1.Type == data.NodeTypeDir {
|
||||||
err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree)
|
err := c.printDir(ctx, "-", &stats.Removed, stats.BlobsBefore, prefix, *node1.Subtree)
|
||||||
if err != nil && err != context.Canceled {
|
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)
|
prefix := path.Join(prefix, name)
|
||||||
if node2.Type == "dir" {
|
if node2.Type == data.NodeTypeDir {
|
||||||
prefix += "/"
|
prefix += "/"
|
||||||
}
|
}
|
||||||
c.printChange(NewChange(prefix, "+"))
|
c.printChange(NewChange(prefix, "+"))
|
||||||
stats.Added.Add(node2)
|
stats.Added.Add(node2)
|
||||||
|
|
||||||
if node2.Type == "dir" {
|
if node2.Type == data.NodeTypeDir {
|
||||||
err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree)
|
err := c.printDir(ctx, "+", &stats.Added, stats.BlobsAfter, prefix, *node2.Subtree)
|
||||||
if err != nil && err != context.Canceled {
|
if err != nil && err != context.Canceled {
|
||||||
Warnf("error: %v\n", err)
|
c.printError("error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,12 +353,14 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
|||||||
return ctx.Err()
|
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 {
|
if len(args) != 2 {
|
||||||
return errors.Fatalf("specify two snapshot IDs")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -383,10 +382,9 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !gopts.JSON {
|
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, printer); err != nil {
|
||||||
if err = repo.LoadIndex(ctx, bar); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,30 +396,31 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
|||||||
return errors.Errorf("snapshot %v has nil tree", sn2.ID().Str())
|
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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &Comparer{
|
c := &Comparer{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
opts: opts,
|
opts: opts,
|
||||||
|
printError: printer.E,
|
||||||
printChange: func(change *Change) {
|
printChange: func(change *Change) {
|
||||||
Printf("%-5s%v\n", change.Modifier, change.Path)
|
printer.S("%-5s%v", change.Modifier, change.Path)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
enc := json.NewEncoder(globalOptions.stdout)
|
enc := json.NewEncoder(gopts.Term.OutputWriter())
|
||||||
c.printChange = func(change *Change) {
|
c.printChange = func(change *Change) {
|
||||||
err := enc.Encode(change)
|
err := enc.Encode(change)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("JSON encode failed: %v\n", err)
|
printer.E("JSON encode failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,9 +433,9 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
|||||||
MessageType: "statistics",
|
MessageType: "statistics",
|
||||||
SourceSnapshot: args[0],
|
SourceSnapshot: args[0],
|
||||||
TargetSnapshot: args[1],
|
TargetSnapshot: args[1],
|
||||||
BlobsBefore: restic.NewBlobSet(),
|
BlobsBefore: repo.NewAssociatedBlobSet(),
|
||||||
BlobsAfter: restic.NewBlobSet(),
|
BlobsAfter: repo.NewAssociatedBlobSet(),
|
||||||
BlobsCommon: restic.NewBlobSet(),
|
BlobsCommon: repo.NewAssociatedBlobSet(),
|
||||||
}
|
}
|
||||||
stats.BlobsBefore.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn1.Tree})
|
stats.BlobsBefore.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn1.Tree})
|
||||||
stats.BlobsAfter.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn2.Tree})
|
stats.BlobsAfter.Insert(restic.BlobHandle{Type: restic.TreeBlob, ID: *sn2.Tree})
|
||||||
@@ -447,23 +446,23 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
|||||||
}
|
}
|
||||||
|
|
||||||
both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
|
both := stats.BlobsBefore.Intersect(stats.BlobsAfter)
|
||||||
updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed)
|
updateBlobs(repo, stats.BlobsBefore.Sub(both).Sub(stats.BlobsCommon), &stats.Removed, printer.E)
|
||||||
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added)
|
updateBlobs(repo, stats.BlobsAfter.Sub(both).Sub(stats.BlobsCommon), &stats.Added, printer.E)
|
||||||
|
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
err := json.NewEncoder(globalOptions.stdout).Encode(stats)
|
err := json.NewEncoder(gopts.Term.OutputWriter()).Encode(stats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("JSON encode failed: %v\n", err)
|
printer.E("JSON encode failed: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Printf("\n")
|
printer.S("")
|
||||||
Printf("Files: %5d new, %5d removed, %5d changed\n", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
|
printer.S("Files: %5d new, %5d removed, %5d changed", stats.Added.Files, stats.Removed.Files, stats.ChangedFiles)
|
||||||
Printf("Dirs: %5d new, %5d removed\n", stats.Added.Dirs, stats.Removed.Dirs)
|
printer.S("Dirs: %5d new, %5d removed", stats.Added.Dirs, stats.Removed.Dirs)
|
||||||
Printf("Others: %5d new, %5d removed\n", stats.Added.Others, stats.Removed.Others)
|
printer.S("Others: %5d new, %5d removed", stats.Added.Others, stats.Removed.Others)
|
||||||
Printf("Data Blobs: %5d new, %5d removed\n", stats.Added.DataBlobs, stats.Removed.DataBlobs)
|
printer.S("Data Blobs: %5d new, %5d removed", stats.Added.DataBlobs, stats.Removed.DataBlobs)
|
||||||
Printf("Tree Blobs: %5d new, %5d removed\n", stats.Added.TreeBlobs, stats.Removed.TreeBlobs)
|
printer.S("Tree Blobs: %5d new, %5d removed", stats.Added.TreeBlobs, stats.Removed.TreeBlobs)
|
||||||
Printf(" Added: %-5s\n", ui.FormatBytes(uint64(stats.Added.Bytes)))
|
printer.S(" Added: %-5s", ui.FormatBytes(stats.Added.Bytes))
|
||||||
Printf(" Removed: %-5s\n", ui.FormatBytes(uint64(stats.Removed.Bytes)))
|
printer.S(" Removed: %-5s", ui.FormatBytes(stats.Removed.Bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunDiffOutput(gopts GlobalOptions, firstSnapshotID string, secondSnapshotID string) (string, error) {
|
func testRunDiffOutput(t testing.TB, gopts global.Options, firstSnapshotID string, secondSnapshotID string) (string, error) {
|
||||||
buf, err := withCaptureStdout(func() error {
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
opts := DiffOptions{
|
opts := DiffOptions{
|
||||||
ShowMetadata: false,
|
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
|
return buf.String(), err
|
||||||
}
|
}
|
||||||
@@ -123,10 +124,10 @@ func TestDiff(t *testing.T) {
|
|||||||
|
|
||||||
// quiet suppresses the diff output except for the summary
|
// quiet suppresses the diff output except for the summary
|
||||||
env.gopts.Quiet = false
|
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")
|
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)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
for _, pattern := range diffOutputRegexPatterns {
|
for _, pattern := range diffOutputRegexPatterns {
|
||||||
@@ -137,7 +138,7 @@ func TestDiff(t *testing.T) {
|
|||||||
|
|
||||||
// check quiet output
|
// check quiet output
|
||||||
env.gopts.Quiet = true
|
env.gopts.Quiet = true
|
||||||
outQuiet, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
|
outQuiet, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Assert(t, len(outQuiet) < len(out), "expected shorter output on quiet mode %v vs. %v", len(outQuiet), len(out))
|
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
|
// quiet suppresses the diff output except for the summary
|
||||||
env.gopts.Quiet = false
|
env.gopts.Quiet = false
|
||||||
env.gopts.JSON = true
|
env.gopts.JSON = true
|
||||||
out, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
|
out, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
var stat DiffStatsContainer
|
var stat DiffStatsContainer
|
||||||
@@ -181,7 +182,7 @@ func TestDiffJSON(t *testing.T) {
|
|||||||
|
|
||||||
// check quiet output
|
// check quiet output
|
||||||
env.gopts.Quiet = true
|
env.gopts.Quiet = true
|
||||||
outQuiet, err := testRunDiffOutput(env.gopts, firstSnapshotID, secondSnapshotID)
|
outQuiet, err := testRunDiffOutput(t, env.gopts, firstSnapshotID, secondSnapshotID)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
stat = DiffStatsContainer{}
|
stat = DiffStatsContainer{}
|
||||||
|
|||||||
@@ -7,18 +7,24 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/dump"
|
"github.com/restic/restic/internal/dump"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdDump = &cobra.Command{
|
func newDumpCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "dump [flags] snapshotID file",
|
var opts DumpOptions
|
||||||
Short: "Print a backed-up file to stdout",
|
cmd := &cobra.Command{
|
||||||
Long: `
|
Use: "dump [flags] snapshotID file",
|
||||||
|
Short: "Print a backed-up file to stdout",
|
||||||
|
Long: `
|
||||||
The "dump" command extracts files from a snapshot from the repository. If a
|
The "dump" command extracts files from a snapshot from the repository. If a
|
||||||
single file is selected, it prints its contents to stdout. Folders are output
|
single file is selected, it prints its contents to stdout. Folders are output
|
||||||
as a tar (default) or zip file containing the contents of the specified folder.
|
as a tar (default) or zip file containing the contents of the specified folder.
|
||||||
@@ -40,29 +46,29 @@ Exit status is 10 if the repository does not exist.
|
|||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDump(cmd.Context(), dumpOptions, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
},
|
return runDump(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// DumpOptions collects all options for the dump command.
|
// DumpOptions collects all options for the dump command.
|
||||||
type DumpOptions struct {
|
type DumpOptions struct {
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
Archive string
|
Archive string
|
||||||
Target string
|
Target string
|
||||||
}
|
}
|
||||||
|
|
||||||
var dumpOptions DumpOptions
|
func (opts *DumpOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
initSingleSnapshotFilter(f, &opts.SnapshotFilter)
|
||||||
func init() {
|
f.StringVarP(&opts.Archive, "archive", "a", "tar", "set archive `format` as \"tar\" or \"zip\"")
|
||||||
cmdRoot.AddCommand(cmdDump)
|
f.StringVarP(&opts.Target, "target", "t", "", "write the output to target `path`")
|
||||||
|
|
||||||
flags := cmdDump.Flags()
|
|
||||||
initSingleSnapshotFilter(flags, &dumpOptions.SnapshotFilter)
|
|
||||||
flags.StringVarP(&dumpOptions.Archive, "archive", "a", "tar", "set archive `format` as \"tar\" or \"zip\"")
|
|
||||||
flags.StringVarP(&dumpOptions.Target, "target", "t", "", "write the output to target `path`")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitPath(p string) []string {
|
func splitPath(p string) []string {
|
||||||
@@ -74,7 +80,7 @@ func splitPath(p string) []string {
|
|||||||
return append(s, f)
|
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
|
// If we print / we need to assume that there are multiple nodes at that
|
||||||
// level in the tree.
|
// level in the tree.
|
||||||
if pathComponents[0] == "" {
|
if pathComponents[0] == "" {
|
||||||
@@ -86,35 +92,38 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
|
|||||||
|
|
||||||
item := filepath.Join(prefix, pathComponents[0])
|
item := filepath.Join(prefix, pathComponents[0])
|
||||||
l := len(pathComponents)
|
l := len(pathComponents)
|
||||||
for _, node := range tree.Nodes {
|
for it := range tree {
|
||||||
|
if it.Error != nil {
|
||||||
|
return it.Error
|
||||||
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
node := it.Node
|
||||||
// If dumping something in the highest level it will just take the
|
// If dumping something in the highest level it will just take the
|
||||||
// first item it finds and dump that according to the switch case below.
|
// first item it finds and dump that according to the switch case below.
|
||||||
if node.Name == pathComponents[0] {
|
if node.Name == pathComponents[0] {
|
||||||
switch {
|
switch {
|
||||||
case l == 1 && dump.IsFile(node):
|
case l == 1 && node.Type == data.NodeTypeFile:
|
||||||
return d.WriteNode(ctx, node)
|
return d.WriteNode(ctx, node)
|
||||||
case l > 1 && dump.IsDir(node):
|
case l > 1 && node.Type == data.NodeTypeDir:
|
||||||
subtree, err := restic.LoadTree(ctx, repo, *node.Subtree)
|
subtree, err := data.LoadTree(ctx, repo, *node.Subtree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
return errors.Wrapf(err, "cannot load subtree for %q", item)
|
||||||
}
|
}
|
||||||
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], d, canWriteArchiveFunc)
|
return printFromTree(ctx, subtree, repo, item, pathComponents[1:], d, canWriteArchiveFunc)
|
||||||
case dump.IsDir(node):
|
case node.Type == data.NodeTypeDir:
|
||||||
if err := canWriteArchiveFunc(); err != nil {
|
if err := canWriteArchiveFunc(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
subtree, err := restic.LoadTree(ctx, repo, *node.Subtree)
|
subtree, err := data.LoadTree(ctx, repo, *node.Subtree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return d.DumpTree(ctx, subtree, item)
|
return d.DumpTree(ctx, subtree, item)
|
||||||
case l > 1:
|
case l > 1:
|
||||||
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
return fmt.Errorf("%q should be a dir, but is a %q", item, node.Type)
|
||||||
case !dump.IsFile(node):
|
case node.Type != data.NodeTypeFile:
|
||||||
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
return fmt.Errorf("%q should be a file, but is a %q", item, node.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,11 +131,13 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
|
|||||||
return fmt.Errorf("path %q not found in snapshot", item)
|
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 {
|
if len(args) != 2 {
|
||||||
return errors.Fatal("no file and no snapshot ID specified")
|
return errors.Fatal("no file and no snapshot ID specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
|
||||||
switch opts.Archive {
|
switch opts.Archive {
|
||||||
case "tar", "zip":
|
case "tar", "zip":
|
||||||
default:
|
default:
|
||||||
@@ -140,39 +151,34 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
|
|||||||
|
|
||||||
splittedPath := splitPath(path.Clean(pathToPrint))
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
sn, subfolder, err := (&restic.SnapshotFilter{
|
sn, subfolder, err := opts.SnapshotFilter.FindLatest(ctx, repo, repo, snapshotIDString)
|
||||||
Hosts: opts.Hosts,
|
|
||||||
Paths: opts.Paths,
|
|
||||||
Tags: opts.Tags,
|
|
||||||
}).FindLatest(ctx, repo, repo, snapshotIDString)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("failed to find snapshot: %v", err)
|
return errors.Fatalf("failed to find snapshot: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
err = repo.LoadIndex(ctx, printer)
|
||||||
err = repo.LoadIndex(ctx, bar)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tree, err := restic.LoadTree(ctx, repo, *sn.Tree)
|
tree, err := data.LoadTree(ctx, repo, *sn.Tree)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err)
|
return errors.Fatalf("loading tree for snapshot %q failed: %v", snapshotIDString, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputFileWriter := os.Stdout
|
outputFileWriter := term.OutputRaw()
|
||||||
canWriteArchiveFunc := checkStdoutArchive
|
canWriteArchiveFunc := checkStdoutArchive(term)
|
||||||
|
|
||||||
if opts.Target != "" {
|
if opts.Target != "" {
|
||||||
file, err := os.Create(opts.Target)
|
file, err := os.Create(opts.Target)
|
||||||
@@ -196,9 +202,9 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args []
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkStdoutArchive() error {
|
func checkStdoutArchive(term ui.Terminal) func() error {
|
||||||
if stdoutIsTerminal() {
|
if term.OutputIsTerminal() {
|
||||||
return fmt.Errorf("stdout is the terminal, please redirect output")
|
return func() error { return fmt.Errorf("stdout is the terminal, please redirect output") }
|
||||||
}
|
}
|
||||||
return nil
|
return func() error { return nil }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/feature"
|
"github.com/restic/restic/internal/feature"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
"github.com/restic/restic/internal/ui/table"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var featuresCmd = &cobra.Command{
|
func newFeaturesCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "features",
|
cmd := &cobra.Command{
|
||||||
Short: "Print list of feature flags",
|
Use: "features",
|
||||||
Long: `
|
Short: "Print list of feature flags",
|
||||||
|
Long: `
|
||||||
The "features" command prints a list of supported feature flags.
|
The "features" command prints a list of supported feature flags.
|
||||||
|
|
||||||
To pass feature flags to restic, set the RESTIC_FEATURES environment variable
|
To pass feature flags to restic, set the RESTIC_FEATURES environment variable
|
||||||
@@ -31,29 +31,28 @@ EXIT STATUS
|
|||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful.
|
||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
`,
|
`,
|
||||||
GroupID: cmdGroupAdvanced,
|
GroupID: cmdGroupAdvanced,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(_ *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
if len(args) != 0 {
|
if len(args) != 0 {
|
||||||
return errors.Fatal("the feature command expects no arguments")
|
return errors.Fatal("the feature command expects no arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("All Feature Flags:\n")
|
globalOptions.Term.Print("All Feature Flags:\n")
|
||||||
flags := feature.Flag.List()
|
flags := feature.Flag.List()
|
||||||
|
|
||||||
tab := table.New()
|
tab := table.New()
|
||||||
tab.AddColumn("Name", "{{ .Name }}")
|
tab.AddColumn("Name", "{{ .Name }}")
|
||||||
tab.AddColumn("Type", "{{ .Type }}")
|
tab.AddColumn("Type", "{{ .Type }}")
|
||||||
tab.AddColumn("Default", "{{ .Default }}")
|
tab.AddColumn("Default", "{{ .Default }}")
|
||||||
tab.AddColumn("Description", "{{ .Description }}")
|
tab.AddColumn("Description", "{{ .Description }}")
|
||||||
|
|
||||||
for _, flag := range flags {
|
for _, flag := range flags {
|
||||||
tab.AddRow(flag)
|
tab.AddRow(flag)
|
||||||
}
|
}
|
||||||
return tab.Write(globalOptions.stdout)
|
return tab.Write(globalOptions.Term.OutputWriter())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
return cmd
|
||||||
cmdRoot.AddCommand(featuresCmd)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,27 +3,38 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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/debug"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/filter"
|
"github.com/restic/restic/internal/filter"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdFind = &cobra.Command{
|
func newFindCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
Use: "find [flags] PATTERN...",
|
var opts FindOptions
|
||||||
Short: "Find a file, a directory or restic IDs",
|
|
||||||
Long: `
|
cmd := &cobra.Command{
|
||||||
|
Use: "find [flags] PATTERN...",
|
||||||
|
Short: "Find a file, a directory or restic IDs",
|
||||||
|
Long: `
|
||||||
The "find" command searches for files or directories in snapshots stored in the
|
The "find" command searches for files or directories in snapshots stored in the
|
||||||
repo.
|
repo.
|
||||||
It can also be used to search for restic blobs or trees for troubleshooting.`,
|
It can also be used to search for restic blobs or trees for troubleshooting.
|
||||||
Example: `restic find config.json
|
The default sort option for the snapshots is youngest to oldest. To sort the
|
||||||
|
output from oldest to youngest specify --reverse.`,
|
||||||
|
Example: `restic find config.json
|
||||||
restic find --json "*.yml" "*.json"
|
restic find --json "*.yml" "*.json"
|
||||||
restic find --json --blob 420f620f b46ebe8a ddd38656
|
restic find --json --blob 420f620f b46ebe8a ddd38656
|
||||||
restic find --show-pack-id --blob 420f620f
|
restic find --show-pack-id --blob 420f620f
|
||||||
@@ -39,11 +50,16 @@ Exit status is 10 if the repository does not exist.
|
|||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
`,
|
`,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runFind(cmd.Context(), findOptions, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
},
|
return runFind(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AddFlags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindOptions bundles all options for the find command.
|
// FindOptions bundles all options for the find command.
|
||||||
@@ -56,27 +72,24 @@ type FindOptions struct {
|
|||||||
CaseInsensitive bool
|
CaseInsensitive bool
|
||||||
ListLong bool
|
ListLong bool
|
||||||
HumanReadable bool
|
HumanReadable bool
|
||||||
restic.SnapshotFilter
|
Reverse bool
|
||||||
|
data.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
var findOptions FindOptions
|
func (opts *FindOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
|
f.StringVarP(&opts.Oldest, "oldest", "O", "", "oldest modification date/time")
|
||||||
|
f.StringVarP(&opts.Newest, "newest", "N", "", "newest modification date/time")
|
||||||
|
f.StringArrayVarP(&opts.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
|
||||||
|
f.BoolVar(&opts.BlobID, "blob", false, "pattern is a blob-ID")
|
||||||
|
f.BoolVar(&opts.TreeID, "tree", false, "pattern is a tree-ID")
|
||||||
|
f.BoolVar(&opts.PackID, "pack", false, "pattern is a pack-ID")
|
||||||
|
f.BoolVar(&opts.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)")
|
||||||
|
f.BoolVarP(&opts.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
|
||||||
|
f.BoolVarP(&opts.Reverse, "reverse", "R", false, "reverse sort order oldest to newest")
|
||||||
|
f.BoolVarP(&opts.ListLong, "long", "l", false, "use a long listing format showing size and mode")
|
||||||
|
f.BoolVar(&opts.HumanReadable, "human-readable", false, "print sizes in human readable format")
|
||||||
|
|
||||||
func init() {
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||||
cmdRoot.AddCommand(cmdFind)
|
|
||||||
|
|
||||||
f := cmdFind.Flags()
|
|
||||||
f.StringVarP(&findOptions.Oldest, "oldest", "O", "", "oldest modification date/time")
|
|
||||||
f.StringVarP(&findOptions.Newest, "newest", "N", "", "newest modification date/time")
|
|
||||||
f.StringArrayVarP(&findOptions.Snapshots, "snapshot", "s", nil, "snapshot `id` to search in (can be given multiple times)")
|
|
||||||
f.BoolVar(&findOptions.BlobID, "blob", false, "pattern is a blob-ID")
|
|
||||||
f.BoolVar(&findOptions.TreeID, "tree", false, "pattern is a tree-ID")
|
|
||||||
f.BoolVar(&findOptions.PackID, "pack", false, "pattern is a pack-ID")
|
|
||||||
f.BoolVar(&findOptions.ShowPackID, "show-pack-id", false, "display the pack-ID the blobs belong to (with --blob or --tree)")
|
|
||||||
f.BoolVarP(&findOptions.CaseInsensitive, "ignore-case", "i", false, "ignore case for pattern")
|
|
||||||
f.BoolVarP(&findOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
|
|
||||||
f.BoolVar(&findOptions.HumanReadable, "human-readable", false, "print sizes in human readable format")
|
|
||||||
|
|
||||||
initMultiSnapshotFilter(f, &findOptions.SnapshotFilter, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type findPattern struct {
|
type findPattern struct {
|
||||||
@@ -114,13 +127,19 @@ type statefulOutput struct {
|
|||||||
HumanReadable bool
|
HumanReadable bool
|
||||||
JSON bool
|
JSON bool
|
||||||
inuse bool
|
inuse bool
|
||||||
newsn *restic.Snapshot
|
newsn *data.Snapshot
|
||||||
oldsn *restic.Snapshot
|
oldsn *data.Snapshot
|
||||||
hits int
|
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) {
|
func (s *statefulOutput) PrintPatternJSON(path string, node *data.Node) {
|
||||||
type findNode restic.Node
|
type findNode data.Node
|
||||||
b, err := json.Marshal(struct {
|
b, err := json.Marshal(struct {
|
||||||
// Add these attributes
|
// Add these attributes
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
@@ -141,40 +160,40 @@ func (s *statefulOutput) PrintPatternJSON(path string, node *restic.Node) {
|
|||||||
findNode: (*findNode)(node),
|
findNode: (*findNode)(node),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("Marshall failed: %v\n", err)
|
s.printer.E("Marshall failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.inuse {
|
if !s.inuse {
|
||||||
Printf("[")
|
_, _ = s.stdout.Write([]byte("["))
|
||||||
s.inuse = true
|
s.inuse = true
|
||||||
}
|
}
|
||||||
if s.newsn != s.oldsn {
|
if s.newsn != s.oldsn {
|
||||||
if s.oldsn != nil {
|
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.oldsn = s.newsn
|
||||||
s.hits = 0
|
s.hits = 0
|
||||||
}
|
}
|
||||||
if s.hits > 0 {
|
if s.hits > 0 {
|
||||||
Printf(",")
|
_, _ = s.stdout.Write([]byte(","))
|
||||||
}
|
}
|
||||||
Print(string(b))
|
_, _ = s.stdout.Write(b)
|
||||||
s.hits++
|
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.newsn != s.oldsn {
|
||||||
if s.oldsn != nil {
|
if s.oldsn != nil {
|
||||||
Verbosef("\n")
|
s.printer.P("")
|
||||||
}
|
}
|
||||||
s.oldsn = s.newsn
|
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 {
|
if s.JSON {
|
||||||
s.PrintPatternJSON(path, node)
|
s.PrintPatternJSON(path, node)
|
||||||
} else {
|
} else {
|
||||||
@@ -182,7 +201,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 {
|
b, err := json.Marshal(struct {
|
||||||
// Add these attributes
|
// Add these attributes
|
||||||
ObjectType string `json:"object_type"`
|
ObjectType string `json:"object_type"`
|
||||||
@@ -200,32 +219,32 @@ func (s *statefulOutput) PrintObjectJSON(kind, id, nodepath, treeID string, sn *
|
|||||||
Time: sn.Time,
|
Time: sn.Time,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("Marshall failed: %v\n", err)
|
s.printer.E("Marshall failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !s.inuse {
|
if !s.inuse {
|
||||||
Printf("[")
|
_, _ = s.stdout.Write([]byte("["))
|
||||||
s.inuse = true
|
s.inuse = true
|
||||||
}
|
}
|
||||||
if s.hits > 0 {
|
if s.hits > 0 {
|
||||||
Printf(",")
|
_, _ = s.stdout.Write([]byte(","))
|
||||||
}
|
}
|
||||||
Print(string(b))
|
_, _ = s.stdout.Write(b)
|
||||||
s.hits++
|
s.hits++
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *restic.Snapshot) {
|
func (s *statefulOutput) PrintObjectNormal(kind, id, nodepath, treeID string, sn *data.Snapshot) {
|
||||||
Printf("Found %s %s\n", kind, id)
|
s.printer.S("Found %s %s", kind, id)
|
||||||
if kind == "blob" {
|
if kind == "blob" {
|
||||||
Printf(" ... in file %s\n", nodepath)
|
s.printer.S(" ... in file %s", nodepath)
|
||||||
Printf(" (tree %s)\n", treeID)
|
s.printer.S(" (tree %s)", treeID)
|
||||||
} else {
|
} 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 {
|
if s.JSON {
|
||||||
s.PrintObjectJSON(kind, id, nodepath, treeID, sn)
|
s.PrintObjectJSON(kind, id, nodepath, treeID, sn)
|
||||||
} else {
|
} else {
|
||||||
@@ -237,12 +256,12 @@ func (s *statefulOutput) Finish() {
|
|||||||
if s.JSON {
|
if s.JSON {
|
||||||
// do some finishing up
|
// do some finishing up
|
||||||
if s.oldsn != nil {
|
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 {
|
if s.inuse {
|
||||||
Printf("]\n")
|
_, _ = s.stdout.Write([]byte("]\n"))
|
||||||
} else {
|
} else {
|
||||||
Printf("[]\n")
|
_, _ = s.stdout.Write([]byte("[]\n"))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -256,9 +275,14 @@ type Finder struct {
|
|||||||
blobIDs map[string]struct{}
|
blobIDs map[string]struct{}
|
||||||
treeIDs map[string]struct{}
|
treeIDs map[string]struct{}
|
||||||
itemsFound int
|
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)
|
debug.Log("searching in snapshot %s\n for entries within [%s %s]", sn.ID(), f.pat.oldest, f.pat.newest)
|
||||||
|
|
||||||
if sn.Tree == nil {
|
if sn.Tree == nil {
|
||||||
@@ -266,11 +290,12 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.out.newsn = sn
|
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 {
|
if err != nil {
|
||||||
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
||||||
|
|
||||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s\n", parentTreeID, sn.ID())
|
f.printer.S("Unable to load tree %s", parentTreeID)
|
||||||
|
f.printer.S(" ... which belongs to snapshot %s", sn.ID())
|
||||||
|
|
||||||
return walker.ErrSkipNode
|
return walker.ErrSkipNode
|
||||||
}
|
}
|
||||||
@@ -298,7 +323,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
var errIfNoMatch error
|
var errIfNoMatch error
|
||||||
if node.Type == "dir" {
|
if node.Type == data.NodeTypeDir {
|
||||||
var childMayMatch bool
|
var childMayMatch bool
|
||||||
for _, pat := range f.pat.pattern {
|
for _, pat := range f.pat.pattern {
|
||||||
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
|
mayMatch, err := filter.ChildMatch(pat, normalizedNodepath)
|
||||||
@@ -336,7 +361,27 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
func (f *Finder) findTree(treeID restic.ID, nodepath string) error {
|
||||||
|
found := false
|
||||||
|
if _, ok := f.treeIDs[treeID.String()]; ok {
|
||||||
|
found = true
|
||||||
|
} else if _, ok := f.treeIDs[treeID.Str()]; ok {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
f.out.PrintObject("tree", treeID.String(), nodepath, "", f.out.newsn)
|
||||||
|
f.itemsFound++
|
||||||
|
// Terminate if we have found all trees (and we are not
|
||||||
|
// looking for blobs)
|
||||||
|
if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil {
|
||||||
|
// Return an error to terminate the Walk
|
||||||
|
return errors.New("OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
|
||||||
debug.Log("searching IDs in snapshot %s", sn.ID())
|
debug.Log("searching IDs in snapshot %s", sn.ID())
|
||||||
|
|
||||||
if sn.Tree == nil {
|
if sn.Tree == nil {
|
||||||
@@ -344,40 +389,32 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.out.newsn = sn
|
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 {
|
if err != nil {
|
||||||
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
debug.Log("Error loading tree %v: %v", parentTreeID, err)
|
||||||
|
|
||||||
Printf("Unable to load tree %s\n ... which belongs to snapshot %s\n", parentTreeID, sn.ID())
|
f.printer.S("Unable to load tree %s", parentTreeID)
|
||||||
|
f.printer.S(" ... which belongs to snapshot %s", sn.ID())
|
||||||
|
|
||||||
return walker.ErrSkipNode
|
return walker.ErrSkipNode
|
||||||
}
|
}
|
||||||
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
|
if nodepath == "/" {
|
||||||
|
if err := f.findTree(parentTreeID, "/"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == "dir" && f.treeIDs != nil {
|
if node.Type == "dir" && f.treeIDs != nil {
|
||||||
treeID := node.Subtree
|
if err := f.findTree(*node.Subtree, nodepath); err != nil {
|
||||||
found := false
|
return err
|
||||||
if _, ok := f.treeIDs[treeID.Str()]; ok {
|
|
||||||
found = true
|
|
||||||
} else if _, ok := f.treeIDs[treeID.String()]; ok {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
f.out.PrintObject("tree", treeID.String(), nodepath, "", sn)
|
|
||||||
f.itemsFound++
|
|
||||||
// Terminate if we have found all trees (and we are not
|
|
||||||
// looking for blobs)
|
|
||||||
if f.itemsFound >= len(f.treeIDs) && f.blobIDs == nil {
|
|
||||||
// Return an error to terminate the Walk
|
|
||||||
return errors.New("OK")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == "file" && f.blobIDs != nil {
|
if node.Type == data.NodeTypeFile && f.blobIDs != nil {
|
||||||
for _, id := range node.Content {
|
for _, id := range node.Content {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
@@ -413,6 +450,9 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
|||||||
if f.blobIDs == nil {
|
if f.blobIDs == nil {
|
||||||
f.blobIDs = make(map[string]struct{})
|
f.blobIDs = make(map[string]struct{})
|
||||||
}
|
}
|
||||||
|
if f.treeIDs == nil {
|
||||||
|
f.treeIDs = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
|
||||||
debug.Log("Looking for packs...")
|
debug.Log("Looking for packs...")
|
||||||
err := f.repo.List(ctx, restic.PackFile, func(id restic.ID, size int64) error {
|
err := f.repo.List(ctx, restic.PackFile, func(id restic.ID, size int64) error {
|
||||||
@@ -433,7 +473,14 @@ func (f *Finder) packsToBlobs(ctx context.Context, packs []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, b := range blobs {
|
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
|
// Stop searching when all packs have been found
|
||||||
if len(packIDs) == 0 {
|
if len(packIDs) == 0 {
|
||||||
@@ -506,7 +553,7 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
|
|||||||
for h := range indexPackIDs {
|
for h := range indexPackIDs {
|
||||||
list = append(list, h)
|
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
|
return packIDs, nil
|
||||||
}
|
}
|
||||||
@@ -514,19 +561,20 @@ func (f *Finder) indexPacksToBlobs(ctx context.Context, packIDs map[string]struc
|
|||||||
func (f *Finder) findObjectPack(id string, t restic.BlobType) {
|
func (f *Finder) findObjectPack(id string, t restic.BlobType) {
|
||||||
rid, err := restic.ParseID(id)
|
rid, err := restic.ParseID(id)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
blobs := f.repo.LookupBlob(t, rid)
|
blobs := f.repo.LookupBlob(t, rid)
|
||||||
if len(blobs) == 0 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range blobs {
|
for _, b := range blobs {
|
||||||
if b.ID.Equal(rid) {
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -542,11 +590,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 {
|
if len(args) == 0 {
|
||||||
return errors.Fatal("wrong number of arguments")
|
return errors.Fatal("wrong number of arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
pat := findPattern{pattern: args}
|
pat := findPattern{pattern: args}
|
||||||
if opts.CaseInsensitive {
|
if opts.CaseInsensitive {
|
||||||
@@ -568,6 +618,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
|
// Check at most only one kind of IDs is provided: currently we
|
||||||
// can't mix types
|
// can't mix types
|
||||||
if (opts.BlobID && opts.TreeID) ||
|
if (opts.BlobID && opts.TreeID) ||
|
||||||
@@ -576,7 +630,7 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
|||||||
return errors.Fatal("cannot have several ID types")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -586,15 +640,15 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
if err = repo.LoadIndex(ctx, printer); err != nil {
|
||||||
if err = repo.LoadIndex(ctx, bar); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &Finder{
|
f := &Finder{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
pat: pat,
|
pat: pat,
|
||||||
out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON},
|
out: statefulOutput{ListLong: opts.ListLong, HumanReadable: opts.HumanReadable, JSON: gopts.JSON, printer: printer, stdout: term.OutputRaw()},
|
||||||
|
printer: printer,
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.BlobID {
|
if opts.BlobID {
|
||||||
@@ -617,8 +671,8 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var filteredSnapshots []*restic.Snapshot
|
var filteredSnapshots []*data.Snapshot
|
||||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots) {
|
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, opts.Snapshots, printer) {
|
||||||
filteredSnapshots = append(filteredSnapshots, sn)
|
filteredSnapshots = append(filteredSnapshots, sn)
|
||||||
}
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
@@ -626,7 +680,10 @@ func runFind(ctx context.Context, opts FindOptions, gopts GlobalOptions, args []
|
|||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(filteredSnapshots, func(i, j int) bool {
|
sort.Slice(filteredSnapshots, func(i, j int) bool {
|
||||||
return filteredSnapshots[i].Time.Before(filteredSnapshots[j].Time)
|
if opts.Reverse {
|
||||||
|
return filteredSnapshots[i].Time.Before(filteredSnapshots[j].Time)
|
||||||
|
}
|
||||||
|
return filteredSnapshots[i].Time.After(filteredSnapshots[j].Time)
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, sn := range filteredSnapshots {
|
for _, sn := range filteredSnapshots {
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunFind(t testing.TB, wantJSON bool, gopts GlobalOptions, pattern string) []byte {
|
func testRunFind(t testing.TB, wantJSON bool, opts FindOptions, gopts global.Options, pattern string) []byte {
|
||||||
buf, err := withCaptureStdout(func() error {
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
gopts.JSON = wantJSON
|
gopts.JSON = wantJSON
|
||||||
|
|
||||||
opts := FindOptions{}
|
return runFind(ctx, opts, gopts, []string{pattern}, gopts.Term)
|
||||||
return runFind(context.TODO(), opts, gopts, []string{pattern})
|
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
@@ -29,16 +33,15 @@ func TestFind(t *testing.T) {
|
|||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
|
||||||
|
|
||||||
results := testRunFind(t, false, env.gopts, "unexistingfile")
|
results := testRunFind(t, false, FindOptions{}, env.gopts, "unexistingfile")
|
||||||
rtest.Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile)
|
rtest.Assert(t, len(results) == 0, "unexisting file found in repo (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, false, env.gopts, "testfile")
|
results = testRunFind(t, false, FindOptions{}, env.gopts, "testfile")
|
||||||
lines := strings.Split(string(results), "\n")
|
lines := strings.Split(string(results), "\n")
|
||||||
rtest.Assert(t, len(lines) == 2, "expected one file found in repo (%v)", datafile)
|
rtest.Assert(t, len(lines) == 2, "expected one file found in repo (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, false, env.gopts, "testfile*")
|
results = testRunFind(t, false, FindOptions{}, env.gopts, "testfile*")
|
||||||
lines = strings.Split(string(results), "\n")
|
lines = strings.Split(string(results), "\n")
|
||||||
rtest.Assert(t, len(lines) == 4, "expected three files found in repo (%v)", datafile)
|
rtest.Assert(t, len(lines) == 4, "expected three files found in repo (%v)", datafile)
|
||||||
}
|
}
|
||||||
@@ -67,21 +70,207 @@ func TestFindJSON(t *testing.T) {
|
|||||||
|
|
||||||
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
|
snapshot, _ := testRunSnapshots(t, env.gopts)
|
||||||
|
|
||||||
results := testRunFind(t, true, env.gopts, "unexistingfile")
|
results := testRunFind(t, true, FindOptions{}, env.gopts, "unexistingfile")
|
||||||
matches := []testMatches{}
|
matches := []testMatches{}
|
||||||
rtest.OK(t, json.Unmarshal(results, &matches))
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
rtest.Assert(t, len(matches) == 0, "expected no match in repo (%v)", datafile)
|
rtest.Assert(t, len(matches) == 0, "expected no match in repo (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, true, env.gopts, "testfile")
|
results = testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
|
||||||
rtest.OK(t, json.Unmarshal(results, &matches))
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
||||||
rtest.Assert(t, len(matches[0].Matches) == 1, "expected a single file to match (%v)", datafile)
|
rtest.Assert(t, len(matches[0].Matches) == 1, "expected a single file to match (%v)", datafile)
|
||||||
rtest.Assert(t, matches[0].Hits == 1, "expected hits to show 1 match (%v)", datafile)
|
rtest.Assert(t, matches[0].Hits == 1, "expected hits to show 1 match (%v)", datafile)
|
||||||
|
|
||||||
results = testRunFind(t, true, env.gopts, "testfile*")
|
results = testRunFind(t, true, FindOptions{}, env.gopts, "testfile*")
|
||||||
rtest.OK(t, json.Unmarshal(results, &matches))
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", datafile)
|
||||||
rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", datafile)
|
rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", datafile)
|
||||||
rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
|
rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
|
||||||
|
|
||||||
|
results = testRunFind(t, true, FindOptions{TreeID: true}, env.gopts, snapshot.Tree.String())
|
||||||
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
|
rtest.Assert(t, len(matches) == 1, "expected a single snapshot in repo (%v)", matches)
|
||||||
|
rtest.Assert(t, len(matches[0].Matches) == 3, "expected 3 files to match (%v)", matches[0].Matches)
|
||||||
|
rtest.Assert(t, matches[0].Hits == 3, "expected hits to show 3 matches (%v)", datafile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSorting(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
testSetupBackupData(t, env)
|
||||||
|
opts := BackupOptions{}
|
||||||
|
|
||||||
|
// first backup
|
||||||
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
|
sn1 := testListSnapshots(t, env.gopts, 1)[0]
|
||||||
|
|
||||||
|
// second backup
|
||||||
|
testRunBackup(t, "", []string{env.testdata}, opts, env.gopts)
|
||||||
|
snapshots := testListSnapshots(t, env.gopts, 2)
|
||||||
|
// get id of new snapshot without depending on file order returned by filesystem
|
||||||
|
sn2 := snapshots[0]
|
||||||
|
if sn1.Equal(sn2) {
|
||||||
|
sn2 = snapshots[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// first restic find - with default FindOptions{}
|
||||||
|
results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
|
||||||
|
lines := strings.Split(string(results), "\n")
|
||||||
|
rtest.Assert(t, len(lines) == 2, "expected two 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 lines of output, found %d", len(lines))
|
||||||
|
matchesReverse := []testMatches{}
|
||||||
|
rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse))
|
||||||
|
|
||||||
|
// compare result sets
|
||||||
|
rtest.Assert(t, sn1.String() == matchesReverse[0].SnapshotID, "snapshot[0] must match old snapshot")
|
||||||
|
rtest.Assert(t, sn2.String() == matchesReverse[1].SnapshotID, "snapshot[1] must match new snapshot")
|
||||||
|
rtest.Assert(t, matches[0].SnapshotID == matchesReverse[1].SnapshotID, "matches should be sorted 1")
|
||||||
|
rtest.Assert(t, matches[1].SnapshotID == matchesReverse[0].SnapshotID, "matches should be sorted 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
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:])
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user