mirror of
https://github.com/restic/restic.git
synced 2026-03-01 20:26:24 +00:00
Compare commits
493 Commits
patch-rele
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4871390a81 | ||
|
|
65b21e3348 | ||
|
|
4a7b122fb6 | ||
|
|
86ddee8518 | ||
|
|
2fe271980f | ||
|
|
4f1390436d | ||
|
|
2d7611373e | ||
|
|
f71278138f | ||
|
|
7d5ebdd0b3 | ||
|
|
d6c75ba2dc | ||
|
|
2a9105c050 | ||
|
|
8e87a37df0 | ||
|
|
a8f506ea4d | ||
|
|
0a1ce4f207 | ||
|
|
364271c6c3 | ||
|
|
6b5c8ce14e | ||
|
|
5a16b29177 | ||
|
|
320fb5fb98 | ||
|
|
c14cf48776 | ||
|
|
109a211fbe | ||
|
|
9d3efc2088 | ||
|
|
8b5dbc18ca | ||
|
|
b0eef4b965 | ||
|
|
6c0dccf4a5 | ||
|
|
6b23d0328b | ||
|
|
52f33d2d54 | ||
|
|
d89535634d | ||
|
|
902cd1e9d6 | ||
|
|
51299b8ea7 | ||
|
|
fd8f8d64f5 | ||
|
|
114cc33fe9 | ||
|
|
44dbd4469e | ||
|
|
d8f3e35730 | ||
|
|
333dbd18d8 | ||
|
|
0226e46681 | ||
|
|
74fb43e0c2 | ||
|
|
69186350fc | ||
|
|
3e7aad8916 | ||
|
|
c3912ae7bc | ||
|
|
d3e26f2868 | ||
|
|
2e91e81c83 | ||
|
|
0dcd9bee88 | ||
|
|
a304826b98 | ||
|
|
8510f09225 | ||
|
|
e63aee2ec6 | ||
|
|
94b19d64be | ||
|
|
03600ca509 | ||
|
|
ef9930cce4 | ||
|
|
91ecac8003 | ||
|
|
e9b6149303 | ||
|
|
32b7168a9e | ||
|
|
6cdb9a75e6 | ||
|
|
81fe559222 | ||
|
|
f21fd9d115 | ||
|
|
d757e39992 | ||
|
|
ce089f7e2d | ||
|
|
576d35b37b | ||
|
|
18b8f8870f | ||
|
|
79c41966af | ||
|
|
c0a30e12b4 | ||
|
|
de29d74707 | ||
|
|
424316e016 | ||
|
|
b71b77fa77 | ||
|
|
e7890d7b81 | ||
|
|
529baf50f8 | ||
|
|
d10bd1d321 | ||
|
|
43b5166de8 | ||
|
|
0b0dd07f15 | ||
|
|
93ccc548c8 | ||
|
|
0ab38faa2e | ||
|
|
48cbbf9651 | ||
|
|
6ff7cd9050 | ||
|
|
cc1fe6c111 | ||
|
|
1ed93bd54d | ||
|
|
c55df65bb6 | ||
|
|
289adebbb7 | ||
|
|
eef047cfa4 | ||
|
|
7f1ac5764a | ||
|
|
52aa1cd17f | ||
|
|
42f690dbab | ||
|
|
914bd699be | ||
|
|
4c19d6410f | ||
|
|
2ad703bfd8 | ||
|
|
0864d04c5c | ||
|
|
839c38b4c4 | ||
|
|
e98c44baaf | ||
|
|
01bc60e96f | ||
|
|
484b706dd8 | ||
|
|
350f6452e7 | ||
|
|
484cdf12e5 | ||
|
|
c8bb7bd312 | ||
|
|
a8ce2e45cc | ||
|
|
275507fb3e | ||
|
|
391c27975a | ||
|
|
7d47e60e27 | ||
|
|
5c17c277f3 | ||
|
|
48e5c0984e | ||
|
|
2414771a59 | ||
|
|
0e381bdbf1 | ||
|
|
11cd4a0a88 | ||
|
|
f54989f634 | ||
|
|
af7f10d16b | ||
|
|
6de95a3d58 | ||
|
|
93f436e999 | ||
|
|
6edb199efd | ||
|
|
9b2c0a0c54 | ||
|
|
64273ea027 | ||
|
|
5a00d26431 | ||
|
|
3faad5751d | ||
|
|
f487eb1c66 | ||
|
|
72636238d0 | ||
|
|
51098157e2 | ||
|
|
0b080c44d7 | ||
|
|
b71fe91643 | ||
|
|
9c3b8d171a | ||
|
|
ddb7fb837b | ||
|
|
3433c5abac | ||
|
|
09bc58c950 | ||
|
|
20eb9018a0 | ||
|
|
651f553530 | ||
|
|
aad4b53ead | ||
|
|
f7f6459eb9 | ||
|
|
95a36b55f4 | ||
|
|
2c39b1f84f | ||
|
|
e467496ace | ||
|
|
f2de260524 | ||
|
|
c17d5ab2e1 | ||
|
|
a8535aba58 | ||
|
|
521fbad701 | ||
|
|
15b7d7c3fc | ||
|
|
7d39b1bfe8 | ||
|
|
e4a7f4aadf | ||
|
|
10cfe96cd4 | ||
|
|
2eaa79d33f | ||
|
|
99ee5696f3 | ||
|
|
e8dbb69a94 | ||
|
|
f4e21cdb75 | ||
|
|
e5bdc3c74f | ||
|
|
7e51c928c4 | ||
|
|
21e87851aa | ||
|
|
2bc1bf2702 | ||
|
|
df110060d1 | ||
|
|
337a7d1205 | ||
|
|
322e271dd2 | ||
|
|
1ac224458f | ||
|
|
126ad04568 | ||
|
|
e732bdbfb8 | ||
|
|
2db08fd749 | ||
|
|
debb110a7c | ||
|
|
5eb4f5af61 | ||
|
|
287b601f01 | ||
|
|
64c82a5d9c | ||
|
|
12f36ebf07 | ||
|
|
45e09dca2a | ||
|
|
5bb9d0d996 | ||
|
|
9f39e8a1d3 | ||
|
|
ddd48f1e98 | ||
|
|
6e91ea3397 | ||
|
|
e7c1e4f1ff | ||
|
|
70e1037a49 | ||
|
|
19f48084ea | ||
|
|
3a995172b7 | ||
|
|
0dffa1208d | ||
|
|
6fbcce1d1a | ||
|
|
e8d458be7e | ||
|
|
4471c7847b | ||
|
|
f13e9c10a4 | ||
|
|
f768683162 | ||
|
|
0b7bdfed7e | ||
|
|
a4fe94ec82 | ||
|
|
6684d1d2f5 | ||
|
|
e1f7522174 | ||
|
|
d1649affb2 | ||
|
|
936c783c0f | ||
|
|
5614cf4758 | ||
|
|
6db0d84ab0 | ||
|
|
88b599c4f3 | ||
|
|
eefff0d793 | ||
|
|
3d14e92905 |
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/
|
||||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -36,7 +36,7 @@ Please always follow these steps:
|
|||||||
- Format all commit messages in the same style as [the other commits in the repository](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
|
- Format all commit messages in the same style as [the other commits in the repository](https://github.com/restic/restic/blob/master/CONTRIBUTING.md#git-commits).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
- [ ] I have added tests for all code changes.
|
- [ ] I have added tests for all code changes, see [writing tests](https://restic.readthedocs.io/en/stable/090_participating.html#writing-tests)
|
||||||
- [ ] I have added documentation for relevant changes (in the manual).
|
- [ ] 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'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"
|
||||||
|
|||||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -26,10 +26,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
30
.github/workflows/tests.yml
vendored
30
.github/workflows/tests.yml
vendored
@@ -13,7 +13,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
latest_go: "1.24.x"
|
latest_go: "1.25.x"
|
||||||
GO111MODULE: on
|
GO111MODULE: on
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -23,29 +23,29 @@ jobs:
|
|||||||
# list of jobs to run:
|
# list of jobs to run:
|
||||||
include:
|
include:
|
||||||
- job_name: Windows
|
- job_name: Windows
|
||||||
go: 1.24.x
|
go: 1.25.x
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
|
|
||||||
- job_name: macOS
|
- job_name: macOS
|
||||||
go: 1.24.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.24.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.24.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.23.x
|
go: 1.24.x
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
test_fuse: true
|
test_fuse: true
|
||||||
|
|
||||||
@@ -57,10 +57,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v5
|
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 }}
|
||||||
|
|
||||||
@@ -220,10 +220,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v5
|
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 }}
|
||||||
|
|
||||||
@@ -242,18 +242,18 @@ jobs:
|
|||||||
checks: write
|
checks: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v5
|
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.64.8
|
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
|
||||||
@@ -287,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@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
|||||||
129
.golangci.yml
129
.golangci.yml
@@ -1,70 +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:
|
|
||||||
# revive: do not warn about missing comments for exported stuff
|
|
||||||
- exported (function|method|var|type|const) .* should have comment or be unexported
|
|
||||||
# revive: ignore constants in all caps
|
|
||||||
- don't use ALL_CAPS in Go names; use CamelCase
|
|
||||||
# revive: lots of packages don't have such a comment
|
|
||||||
- "package-comments: should have a package comment"
|
|
||||||
# staticcheck: there's no easy way to replace these packages
|
|
||||||
- "SA1019: \"golang.org/x/crypto/poly1305\" is deprecated"
|
|
||||||
- "SA1019: \"golang.org/x/crypto/openpgp\" is deprecated"
|
|
||||||
- "redefines-builtin-id:"
|
|
||||||
|
|
||||||
exclude-rules:
|
|
||||||
# revive: ignore unused parameters in tests
|
|
||||||
- path: (_test\.go|testing\.go|backend/.*/tests\.go)
|
|
||||||
text: "unused-parameter:"
|
|
||||||
|
|
||||||
linters-settings:
|
|
||||||
importas:
|
importas:
|
||||||
alias:
|
alias:
|
||||||
- pkg: github.com/restic/restic/internal/test
|
- pkg: github.com/restic/restic/internal/test
|
||||||
alias: rtest
|
alias: rtest
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
# default
|
||||||
|
- "all"
|
||||||
|
- "-ST1000"
|
||||||
|
- "-ST1003"
|
||||||
|
- "-ST1016"
|
||||||
|
- "-ST1020"
|
||||||
|
- "-ST1021"
|
||||||
|
- "-ST1022"
|
||||||
|
# extra disables
|
||||||
|
- "-QF1008" # don't warn about specifing name of embedded field on access
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
# revive: ignore unused parameters in tests
|
||||||
|
- path: (_test\.go|testing\.go|backend/.*/tests\.go)
|
||||||
|
text: "unused-parameter:"
|
||||||
|
# revive: do not warn about missing comments for exported stuff
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: exported (function|method|var|type|const) .* should have comment or be unexported
|
||||||
|
# revive: ignore constants in all caps
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: don't use ALL_CAPS in Go names; use CamelCase
|
||||||
|
# revive: lots of packages don't have such a comment
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: "package-comments: should have a package comment"
|
||||||
|
# staticcheck: there's no easy way to replace these packages
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: 'SA1019: "golang.org/x/crypto/poly1305" is deprecated'
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: 'SA1019: "golang.org/x/crypto/openpgp" is deprecated'
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: "redefines-builtin-id:"
|
||||||
|
# revive: collection of helpers to implement a backend, more descriptive names would be too repetitive
|
||||||
|
- path: internal/backend/util/.*.go$
|
||||||
|
text: "var-naming: avoid meaningless package names"
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
# make sure code is formatted
|
||||||
|
- gofmt
|
||||||
|
exclusions:
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
|||||||
@@ -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
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|||||||
3
build.go
3
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
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ var config = Config{
|
|||||||
// see https://github.com/googleapis/google-cloud-go/issues/11448
|
// see https://github.com/googleapis/google-cloud-go/issues/11448
|
||||||
DefaultBuildTags: []string{"selfupdate", "disable_grpc_modules"}, // specify build tags which are always used
|
DefaultBuildTags: []string{"selfupdate", "disable_grpc_modules"}, // specify build tags which are always used
|
||||||
Tests: []string{"./..."}, // tests to run
|
Tests: []string{"./..."}, // tests to run
|
||||||
MinVersion: GoVersion{Major: 1, Minor: 23, Patch: 0}, // minimum Go version supported
|
MinVersion: GoVersion{Major: 1, Minor: 24, Patch: 0}, // minimum Go version supported
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config configures the build.
|
// Config configures the build.
|
||||||
|
|||||||
@@ -1,14 +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.
|
||||||
# If the problem description just says that a feature was missing, then
|
# If the problem description just says that a feature was missing, then
|
||||||
# only explain the new behavior.
|
# only explain the new behavior. Aim for a short and concise description.
|
||||||
# Focus on user-facing behavior, not the implementation.
|
|
||||||
# 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()
|
||||||
|
|||||||
@@ -19,19 +19,20 @@ import (
|
|||||||
"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/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"
|
||||||
"github.com/restic/restic/internal/ui/backup"
|
"github.com/restic/restic/internal/ui/backup"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newBackupCommand() *cobra.Command {
|
func newBackupCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts BackupOptions
|
var opts BackupOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -64,9 +65,7 @@ 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()
|
return runBackup(cmd.Context(), opts, *globalOptions, globalOptions.Term, args)
|
||||||
defer cancel()
|
|
||||||
return runBackup(cmd.Context(), opts, globalOptions, term, args)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +78,7 @@ type BackupOptions struct {
|
|||||||
filter.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
|
||||||
@@ -89,7 +88,7 @@ type BackupOptions struct {
|
|||||||
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
|
||||||
@@ -107,7 +106,7 @@ type BackupOptions struct {
|
|||||||
|
|
||||||
func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
f.StringVar(&opts.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)")
|
f.StringVar(&opts.Parent, "parent", "", "use this parent `snapshot` (default: latest snapshot in the group determined by --group-by and not newer than the timestamp determined by --time)")
|
||||||
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
opts.GroupBy = data.SnapshotGroupByOptions{Host: true, Path: true}
|
||||||
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
|
f.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)`)
|
f.BoolVarP(&opts.Force, "force", "f", false, `force re-reading the source files/directories (overrides the "parent" flag)`)
|
||||||
|
|
||||||
@@ -140,7 +139,9 @@ func (opts *BackupOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
f.BoolVar(&opts.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(&opts.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
|
f.BoolVar(&opts.UseFsSnapshot, "use-fs-snapshot", false, "use filesystem snapshot where possible (currently only Windows VSS)")
|
||||||
f.BoolVar(&opts.ExcludeCloudFiles, "exclude-cloud-files", false, "excludes online-only cloud files (such as OneDrive Files On-Demand)")
|
}
|
||||||
|
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")
|
f.BoolVar(&opts.SkipIfUnchanged, "skip-if-unchanged", false, "skip snapshot creation if identical to parent snapshot")
|
||||||
|
|
||||||
@@ -159,13 +160,16 @@ var backupFSTestHook func(fs fs.FS) fs.FS
|
|||||||
// ErrInvalidSourceData is used to report an incomplete backup
|
// ErrInvalidSourceData is used to report an incomplete backup
|
||||||
var ErrInvalidSourceData = errors.New("at least one source file could not be read")
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,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
|
||||||
@@ -185,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
|
||||||
}
|
}
|
||||||
@@ -196,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)
|
||||||
}
|
}
|
||||||
@@ -221,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
|
||||||
@@ -271,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")
|
||||||
}
|
}
|
||||||
@@ -306,7 +312,7 @@ 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 []archiver.RejectByNameFunc, err error) {
|
func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository, warnf func(msg string, args ...interface{})) (fs []archiver.RejectByNameFunc, err error) {
|
||||||
// exclude restic cache
|
// exclude restic cache
|
||||||
if repo.Cache() != nil {
|
if repo.Cache() != nil {
|
||||||
f, err := rejectResticCache(repo)
|
f, err := rejectResticCache(repo)
|
||||||
@@ -317,7 +323,7 @@ func collectRejectByNameFuncs(opts BackupOptions, repo *repository.Repository) (
|
|||||||
fs = append(fs, f)
|
fs = append(fs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(Warnf)
|
fsPatterns, err := opts.ExcludePatternOptions.CollectPatterns(warnf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -330,7 +336,7 @@ 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 fs.FS) (funcs []archiver.RejectFunc, err error) {
|
func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS, warnf func(msg string, args ...interface{})) (funcs []archiver.RejectFunc, err error) {
|
||||||
// allowed devices
|
// allowed devices
|
||||||
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
|
if opts.ExcludeOtherFS && !opts.Stdin && !opts.StdinCommand {
|
||||||
f, err := archiver.RejectByDevice(targets, fs)
|
f, err := archiver.RejectByDevice(targets, fs)
|
||||||
@@ -354,10 +360,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.ExcludeCloudFiles && !opts.Stdin && !opts.StdinCommand {
|
if opts.ExcludeCloudFiles && !opts.Stdin && !opts.StdinCommand {
|
||||||
if runtime.GOOS != "windows" {
|
f, err := archiver.RejectCloudFiles(warnf)
|
||||||
return nil, errors.Fatalf("exclude-cloud-files is only supported on Windows")
|
|
||||||
}
|
|
||||||
f, err := archiver.RejectCloudFiles(Warnf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -369,7 +372,7 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, spec := range opts.ExcludeIfPresent {
|
for _, spec := range opts.ExcludeIfPresent {
|
||||||
f, err := archiver.RejectIfPresent(spec, Warnf)
|
f, err := archiver.RejectIfPresent(spec, warnf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -381,13 +384,13 @@ func collectRejectFuncs(opts BackupOptions, targets []string, fs fs.FS) (funcs [
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -405,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
|
||||||
}
|
}
|
||||||
@@ -425,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
|
||||||
}
|
}
|
||||||
@@ -439,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
|
||||||
}
|
}
|
||||||
@@ -458,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}
|
||||||
}
|
}
|
||||||
@@ -466,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,47 +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 {
|
||||||
|
if errors.Is(err, ErrInvalidSourceData) {
|
||||||
|
success = false
|
||||||
|
} else {
|
||||||
return err
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var parentSnapshot *restic.Snapshot
|
var parentSnapshot *data.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 {
|
||||||
@@ -541,19 +544,18 @@ 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
|
||||||
}
|
}
|
||||||
@@ -570,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...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,12 +583,12 @@ 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
|
||||||
}
|
}
|
||||||
@@ -606,7 +608,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
// rejectFuncs collect functions that can reject items from the backup based on path and file info
|
||||||
rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS)
|
rejectFuncs, err := collectRejectFuncs(opts, targets, targetFS, printer.E)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -622,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) })
|
||||||
}
|
}
|
||||||
@@ -635,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)
|
||||||
@@ -666,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,33 +3,34 @@ 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: %v", err)
|
rtest.Assert(t, err == nil, "Error while backing up: %v", err)
|
||||||
}
|
}
|
||||||
@@ -56,13 +57,13 @@ func testBackup(t *testing.T, useFsSnapshot bool) {
|
|||||||
testListSnapshots(t, env.gopts, 1)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,9 +263,6 @@ func TestBackupNonExistingFile(t *testing.T) {
|
|||||||
|
|
||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
|
|
||||||
_ = withRestoreGlobalOptions(func() error {
|
|
||||||
globalOptions.stderr = io.Discard
|
|
||||||
|
|
||||||
p := filepath.Join(env.testdata, "0", "0", "9")
|
p := filepath.Join(env.testdata, "0", "0", "9")
|
||||||
dirs := []string{
|
dirs := []string{
|
||||||
filepath.Join(p, "0"),
|
filepath.Join(p, "0"),
|
||||||
@@ -275,9 +273,17 @@ func TestBackupNonExistingFile(t *testing.T) {
|
|||||||
|
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
testRunBackup(t, "", dirs, opts, env.gopts)
|
// mix of existing and non-existing files
|
||||||
return nil
|
err := testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts)
|
||||||
})
|
rtest.Assert(t, err != nil, "expected error for non-existing file")
|
||||||
|
rtest.Assert(t, errors.Is(err, ErrInvalidSourceData), "expected ErrInvalidSourceData; got %v", err)
|
||||||
|
// only non-existing file
|
||||||
|
dirs = []string{
|
||||||
|
filepath.Join(p, "nonexisting"),
|
||||||
|
}
|
||||||
|
err = testRunBackupAssumeFailure(t, "", dirs, opts, env.gopts)
|
||||||
|
rtest.Assert(t, err != nil, "expected error for non-existing file")
|
||||||
|
rtest.Assert(t, errors.Is(err, ErrNoSourceData), "expected ErrNoSourceData; got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackupSelfHealing(t *testing.T) {
|
func TestBackupSelfHealing(t *testing.T) {
|
||||||
@@ -438,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)
|
||||||
}
|
}
|
||||||
@@ -454,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()
|
||||||
@@ -481,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)
|
||||||
@@ -497,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()
|
||||||
@@ -509,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)
|
||||||
}
|
}
|
||||||
@@ -567,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"))
|
||||||
@@ -703,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)
|
||||||
|
|||||||
@@ -67,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,13 +10,14 @@ 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/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"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCacheCommand() *cobra.Command {
|
func newCacheCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts CacheOptions
|
var opts CacheOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -34,7 +35,7 @@ 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(opts, globalOptions, args)
|
return runCache(opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +56,9 @@ func (opts *CacheOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
f.BoolVar(&opts.NoSize, "no-size", false, "do not output the size of the cache directories")
|
f.BoolVar(&opts.NoSize, "no-size", false, "do not output the size of the cache directories")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCache(opts CacheOptions, gopts GlobalOptions, args []string) error {
|
func runCache(opts CacheOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
if len(args) > 0 {
|
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")
|
||||||
}
|
}
|
||||||
@@ -83,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 = os.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,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,14 +7,17 @@ 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"}
|
||||||
|
|
||||||
func newCatCommand() *cobra.Command {
|
func newCatCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
|
Use: "cat [flags] [masterkey|config|pack ID|blob ID|snapshot ID|index ID|key ID|lock ID|tree snapshot:subfolder]",
|
||||||
Short: "Print internal objects to stdout",
|
Short: "Print internal objects to stdout",
|
||||||
@@ -33,7 +36,7 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -63,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
|
||||||
}
|
}
|
||||||
@@ -80,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,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)
|
||||||
@@ -99,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, "", " ")
|
||||||
@@ -112,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)
|
||||||
@@ -125,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(), "", " ")
|
||||||
@@ -133,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)
|
||||||
@@ -146,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":
|
||||||
@@ -158,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
|
||||||
}
|
}
|
||||||
@@ -181,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
|
||||||
}
|
}
|
||||||
@@ -208,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:
|
||||||
|
|||||||
@@ -15,15 +15,16 @@ import (
|
|||||||
|
|
||||||
"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/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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCheckCommand() *cobra.Command {
|
func newCheckCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts CheckOptions
|
var opts CheckOptions
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "check [flags]",
|
Use: "check [flags]",
|
||||||
@@ -47,14 +48,13 @@ 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)
|
||||||
summary, err := runCheck(cmd.Context(), opts, globalOptions, args, term)
|
|
||||||
if globalOptions.JSON {
|
if globalOptions.JSON {
|
||||||
if err != nil && summary.NumErrors == 0 {
|
if err != nil && summary.NumErrors == 0 {
|
||||||
summary.NumErrors = 1
|
summary.NumErrors = 1
|
||||||
}
|
}
|
||||||
term.Print(ui.ToJSONString(summary))
|
globalOptions.Term.Print(ui.ToJSONString(summary))
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
@@ -73,6 +73,7 @@ type CheckOptions struct {
|
|||||||
ReadDataSubset string
|
ReadDataSubset string
|
||||||
CheckUnused bool
|
CheckUnused bool
|
||||||
WithCache bool
|
WithCache bool
|
||||||
|
data.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
@@ -86,6 +87,7 @@ func (opts *CheckOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
f.BoolVar(&opts.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 {
|
||||||
@@ -173,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
|
||||||
@@ -194,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
|
||||||
}
|
}
|
||||||
@@ -220,15 +222,12 @@ 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) (checkSummary, error) {
|
func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args []string, term ui.Terminal) (checkSummary, error) {
|
||||||
summary := checkSummary{MessageType: "summary"}
|
summary := checkSummary{MessageType: "summary"}
|
||||||
if len(args) != 0 {
|
|
||||||
return summary, errors.Fatal("the check command expects no arguments, only options - please see `restic help check` for usage and flags")
|
|
||||||
}
|
|
||||||
|
|
||||||
var printer progress.Printer
|
var printer progress.Printer
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
printer = newTerminalProgressPrinter(gopts.verbosity, term)
|
printer = ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
} else {
|
} else {
|
||||||
printer = newJSONErrorPrinter(term)
|
printer = newJSONErrorPrinter(term)
|
||||||
}
|
}
|
||||||
@@ -239,21 +238,20 @@ 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 summary, 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 summary, 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 summary, ctx.Err()
|
return summary, ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -261,10 +259,10 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
errorsFound := false
|
errorsFound := false
|
||||||
for _, hint := range hints {
|
for _, hint := range hints {
|
||||||
switch hint.(type) {
|
switch hint.(type) {
|
||||||
case *checker.ErrDuplicatePacks:
|
case *repository.ErrDuplicatePacks:
|
||||||
printer.S("%s", hint.Error())
|
printer.S("%s", hint.Error())
|
||||||
summary.HintRepairIndex = true
|
summary.HintRepairIndex = true
|
||||||
case *checker.ErrMixedPack:
|
case *repository.ErrMixedPack:
|
||||||
printer.S("%s", hint.Error())
|
printer.S("%s", hint.Error())
|
||||||
summary.HintPrune = true
|
summary.HintPrune = true
|
||||||
default:
|
default:
|
||||||
@@ -299,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++
|
||||||
@@ -363,6 +361,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
return summary, 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 {
|
||||||
@@ -374,12 +373,16 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doReadData := func(packs map[restic.ID]int64) {
|
readDataFilter, err := buildPacksFilter(opts, printer, chkr.IsFiltered())
|
||||||
|
if err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if readDataFilter != nil {
|
||||||
p := printer.NewCounter("packs")
|
p := printer.NewCounter("packs")
|
||||||
p.SetMax(uint64(len(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
|
||||||
@@ -392,47 +395,6 @@ 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 summary, errors.Fatal("Cannot read from a repository having size 0")
|
|
||||||
}
|
|
||||||
subsetSize, _ := ui.ParseBytes(opts.ReadDataSubset)
|
|
||||||
if subsetSize > repoSize {
|
|
||||||
subsetSize = repoSize
|
|
||||||
}
|
|
||||||
packs = selectRandomPacksByFileSize(chkr.GetPacks(), subsetSize, repoSize)
|
|
||||||
printer.P("read %d bytes of data packs\n", subsetSize)
|
|
||||||
}
|
|
||||||
if packs == nil {
|
|
||||||
return summary, errors.Fatal("internal error: failed to select packs to check")
|
|
||||||
}
|
|
||||||
doReadData(packs)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(salvagePacks) > 0 {
|
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")
|
||||||
for id := range salvagePacks {
|
for id := range salvagePacks {
|
||||||
@@ -456,6 +418,64 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args
|
|||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildPacksFilter(opts CheckOptions, printer progress.Printer,
|
||||||
|
filteredStatus bool) (func(packs map[restic.ID]int64) map[restic.ID]int64, error) {
|
||||||
|
typeData := ""
|
||||||
|
if filteredStatus {
|
||||||
|
typeData = "filtered "
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case opts.ReadData:
|
||||||
|
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
|
||||||
|
printer.P("read all %sdata", typeData)
|
||||||
|
return packs
|
||||||
|
}, nil
|
||||||
|
case opts.ReadDataSubset != "":
|
||||||
|
dataSubset, err := stringToIntSlice(opts.ReadDataSubset)
|
||||||
|
if err == nil {
|
||||||
|
bucket := dataSubset[0]
|
||||||
|
totalBuckets := dataSubset[1]
|
||||||
|
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
|
||||||
|
packCount := uint64(len(packs))
|
||||||
|
packs = selectPacksByBucket(packs, bucket, totalBuckets)
|
||||||
|
printer.P("read group #%d of %d %sdata packs (out of total %d packs in %d groups", bucket, len(packs), typeData, packCount, totalBuckets)
|
||||||
|
return packs
|
||||||
|
}, nil
|
||||||
|
} else if strings.HasSuffix(opts.ReadDataSubset, "%") {
|
||||||
|
percentage, err := parsePercentage(opts.ReadDataSubset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
|
||||||
|
printer.P("read %.1f%% of %spackfiles", percentage, typeData)
|
||||||
|
return selectRandomPacksByPercentage(packs, percentage)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
repoSize := int64(0)
|
||||||
|
return func(packs map[restic.ID]int64) map[restic.ID]int64 {
|
||||||
|
for _, size := range packs {
|
||||||
|
repoSize += size
|
||||||
|
}
|
||||||
|
subsetSize, _ := ui.ParseBytes(opts.ReadDataSubset)
|
||||||
|
if subsetSize > repoSize {
|
||||||
|
subsetSize = repoSize
|
||||||
|
}
|
||||||
|
if repoSize > 0 {
|
||||||
|
packs = selectRandomPacksByFileSize(packs, subsetSize, repoSize)
|
||||||
|
}
|
||||||
|
percentage := float64(subsetSize) / float64(repoSize) * 100.0
|
||||||
|
if repoSize == 0 {
|
||||||
|
percentage = 100
|
||||||
|
}
|
||||||
|
printer.P("read %d bytes (%.1f%%) of %sdata packs\n", subsetSize, percentage, typeData)
|
||||||
|
return packs
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// selectPacksByBucket selects subsets of packs by ranges of buckets.
|
// selectPacksByBucket selects subsets of packs by ranges of buckets.
|
||||||
func selectPacksByBucket(allPacks map[restic.ID]int64, bucket, totalBuckets uint) map[restic.ID]int64 {
|
func selectPacksByBucket(allPacks map[restic.ID]int64, bucket, totalBuckets uint) map[restic.ID]int64 {
|
||||||
packs := make(map[restic.ID]int64)
|
packs := make(map[restic.ID]int64)
|
||||||
@@ -527,6 +547,10 @@ func (*jsonErrorPrinter) NewCounter(_ string) *progress.Counter {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*jsonErrorPrinter) NewCounterTerminalOnly(_ string) *progress.Counter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
||||||
status := checkError{
|
status := checkError{
|
||||||
MessageType: "error",
|
MessageType: "error",
|
||||||
@@ -536,5 +560,6 @@ func (p *jsonErrorPrinter) E(msg string, args ...interface{}) {
|
|||||||
}
|
}
|
||||||
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {}
|
||||||
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {}
|
||||||
|
func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {}
|
||||||
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {}
|
||||||
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
|
func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {}
|
||||||
|
|||||||
@@ -1,39 +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,
|
||||||
}
|
}
|
||||||
_, err := runCheck(context.TODO(), opts, gopts, nil, term)
|
_, err := runCheck(context.TODO(), opts, gopts, nil, gopts.Term)
|
||||||
return err
|
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,18 +3,24 @@ 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"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCopyCommand() *cobra.Command {
|
func newCopyCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts CopyOptions
|
var opts CopyOptions
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "copy [flags] [snapshotID ...]",
|
Use: "copy [flags] [snapshotID ...]",
|
||||||
@@ -46,7 +52,8 @@ 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(), opts, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
return runCopy(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,17 +63,51 @@ Exit status is 12 if the password is incorrect.
|
|||||||
|
|
||||||
// CopyOptions bundles all options for the copy command.
|
// CopyOptions bundles all options for the copy command.
|
||||||
type CopyOptions struct {
|
type CopyOptions struct {
|
||||||
secondaryRepoOptions
|
global.SecondaryRepoOptions
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *CopyOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
opts.secondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from")
|
opts.SecondaryRepoOptions.AddFlags(f, "destination", "to copy snapshots from")
|
||||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
initMultiSnapshotFilter(f, &opts.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
|
||||||
}
|
}
|
||||||
@@ -75,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
|
||||||
}
|
}
|
||||||
@@ -98,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)
|
||||||
}
|
}
|
||||||
@@ -120,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
|
|
||||||
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
|
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 ||
|
||||||
@@ -185,72 +187,158 @@ func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyTreeBatched copies multiple snapshots in one go. Snapshots are written after
|
||||||
|
// data equivalent to at least 10 packfiles was written.
|
||||||
|
func copyTreeBatched(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
||||||
|
selectedSnapshots iter.Seq[*data.Snapshot], printer progress.Printer) error {
|
||||||
|
|
||||||
|
// remember already processed trees across all snapshots
|
||||||
|
visitedTrees := srcRepo.NewAssociatedBlobSet()
|
||||||
|
|
||||||
|
targetSize := uint64(dstRepo.PackSize()) * 100
|
||||||
|
minDuration := 1 * time.Minute
|
||||||
|
|
||||||
|
// use pull-based iterator to allow iteration in multiple steps
|
||||||
|
next, stop := iter.Pull(selectedSnapshots)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
var batch []*data.Snapshot
|
||||||
|
batchSize := uint64(0)
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
// call WithBlobUploader() once and then loop over all selectedSnapshots
|
||||||
|
err := dstRepo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||||
|
for batchSize < targetSize || time.Since(startTime) < minDuration {
|
||||||
|
sn, ok := next()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
batch = append(batch, sn)
|
||||||
|
|
||||||
|
printer.P("\n%v", sn)
|
||||||
|
printer.P(" copy started, this may take a while...")
|
||||||
|
sizeBlobs, err := copyTree(ctx, srcRepo, dstRepo, visitedTrees, *sn.Tree, printer, uploader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
debug.Log("tree copied")
|
||||||
|
batchSize += sizeBlobs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no snapshots were processed in this batch, we're done
|
||||||
|
if len(batch) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a newline to separate saved snapshot messages from the other messages
|
||||||
|
if len(batch) > 1 {
|
||||||
|
printer.P("")
|
||||||
|
}
|
||||||
|
// save all the snapshots
|
||||||
|
for _, sn := range batch {
|
||||||
|
err := copySaveSnapshot(ctx, sn, dstRepo, printer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
func copyTree(ctx context.Context, srcRepo restic.Repository, dstRepo restic.Repository,
|
||||||
visitedTrees restic.IDSet, rootTreeID restic.ID, quiet bool) error {
|
visitedTrees restic.AssociatedBlobSet, rootTreeID restic.ID, printer progress.Printer, uploader restic.BlobSaverWithAsync) (uint64, error) {
|
||||||
|
|
||||||
wg, wgCtx := errgroup.WithContext(ctx)
|
copyBlobs := srcRepo.NewAssociatedBlobSet()
|
||||||
|
|
||||||
treeStream := restic.StreamTrees(wgCtx, wg, srcRepo, restic.IDs{rootTreeID}, func(treeID restic.ID) bool {
|
|
||||||
visited := visitedTrees.Has(treeID)
|
|
||||||
visitedTrees.Insert(treeID)
|
|
||||||
return visited
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
copyBlobs := restic.NewBlobSet()
|
|
||||||
packList := restic.NewIDSet()
|
packList := restic.NewIDSet()
|
||||||
|
var lock sync.Mutex
|
||||||
|
|
||||||
enqueue := func(h restic.BlobHandle) {
|
enqueue := func(h restic.BlobHandle) {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
|
||||||
pb := srcRepo.LookupBlob(h.Type, h.ID)
|
pb := srcRepo.LookupBlob(h.Type, h.ID)
|
||||||
copyBlobs.Insert(h)
|
copyBlobs.Insert(h)
|
||||||
for _, p := range pb {
|
for _, p := range pb {
|
||||||
packList.Insert(p.PackID)
|
packList.Insert(p.PackID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Go(func() error {
|
|
||||||
for tree := range treeStream {
|
|
||||||
if tree.Error != nil {
|
|
||||||
return fmt.Errorf("LoadTree(%v) returned error %v", tree.ID.Str(), tree.Error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do we already have this tree blob?
|
err := data.StreamTrees(ctx, srcRepo, restic.IDs{rootTreeID}, nil, func(treeID restic.ID) bool {
|
||||||
treeHandle := restic.BlobHandle{ID: tree.ID, Type: restic.TreeBlob}
|
handle := restic.BlobHandle{ID: treeID, Type: restic.TreeBlob}
|
||||||
if _, ok := dstRepo.LookupBlobSize(treeHandle.Type, treeHandle.ID); !ok {
|
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
|
// copy raw tree bytes to avoid problems if the serialization changes
|
||||||
enqueue(treeHandle)
|
enqueue(restic.BlobHandle{ID: treeID, Type: restic.TreeBlob})
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range tree.Nodes {
|
for item := range nodes {
|
||||||
|
if item.Error != nil {
|
||||||
|
return item.Error
|
||||||
|
}
|
||||||
// Recursion into directories is handled by StreamTrees
|
// Recursion into directories is handled by StreamTrees
|
||||||
// Copy the blobs for this file.
|
// Copy the blobs for this file.
|
||||||
for _, blobID := range entry.Content {
|
for _, blobID := range item.Node.Content {
|
||||||
h := restic.BlobHandle{Type: restic.DataBlob, ID: blobID}
|
enqueue(restic.BlobHandle{Type: restic.DataBlob, ID: blobID})
|
||||||
if _, ok := dstRepo.LookupBlobSize(h.Type, h.ID); !ok {
|
|
||||||
enqueue(h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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,
|
|
||||||
func(msg string, args ...interface{}) { fmt.Printf(msg+"\n", args...) },
|
|
||||||
)
|
|
||||||
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
|
||||||
|
|
||||||
@@ -22,32 +21,36 @@ import (
|
|||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerDebugCommand(cmd *cobra.Command) {
|
func registerDebugCommand(cmd *cobra.Command, globalOptions *global.Options) {
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newDebugCommand(),
|
newDebugCommand(globalOptions),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDebugCommand() *cobra.Command {
|
func newDebugCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "debug",
|
Use: "debug",
|
||||||
Short: "Debug commands",
|
Short: "Debug commands",
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
}
|
}
|
||||||
cmd.AddCommand(newDebugDumpCommand())
|
cmd.AddCommand(newDebugDumpCommand(globalOptions))
|
||||||
cmd.AddCommand(newDebugExamineCommand())
|
cmd.AddCommand(newDebugExamineCommand(globalOptions))
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDebugDumpCommand() *cobra.Command {
|
func newDebugDumpCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "dump [indexes|snapshots|all|packs]",
|
Use: "dump [indexes|snapshots|all|packs]",
|
||||||
Short: "Dump data structures",
|
Short: "Dump data structures",
|
||||||
@@ -66,13 +69,13 @@ 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
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDebugExamineCommand() *cobra.Command {
|
func newDebugExamineCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts DebugExamineOptions
|
var opts DebugExamineOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -80,7 +83,7 @@ func newDebugExamineCommand() *cobra.Command {
|
|||||||
Short: "Examine a pack file",
|
Short: "Examine a pack file",
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runDebugExamine(cmd.Context(), globalOptions, opts, args)
|
return runDebugExamine(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +116,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -141,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,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, 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
|
||||||
}
|
}
|
||||||
@@ -181,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
|
||||||
}
|
}
|
||||||
@@ -196,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
|
||||||
}
|
}
|
||||||
@@ -220,11 +225,11 @@ func runDebugDump(ctx context.Context, gopts GlobalOptions, args []string) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool, printer progress.Printer) []byte {
|
||||||
if bytewise {
|
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)
|
||||||
@@ -234,7 +239,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
|||||||
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
|
||||||
@@ -248,9 +253,9 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
|||||||
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
|
||||||
@@ -291,7 +296,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +306,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
|||||||
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()
|
||||||
}
|
}
|
||||||
@@ -314,7 +319,7 @@ func tryRepairWithBitflip(key *crypto.Key, input []byte, bytewise bool) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
Printf("\n blob could not be repaired\n")
|
printer.S("\n blob could not be repaired")
|
||||||
}
|
}
|
||||||
return fixed
|
return fixed
|
||||||
}
|
}
|
||||||
@@ -335,7 +340,7 @@ func decryptUnsigned(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)
|
||||||
@@ -347,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]
|
||||||
@@ -368,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(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(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
|
||||||
}
|
}
|
||||||
@@ -388,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
|
||||||
@@ -398,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 {
|
||||||
@@ -446,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
|
||||||
}
|
}
|
||||||
@@ -467,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,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
|
||||||
@@ -496,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
|
||||||
@@ -523,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
|
||||||
|
|
||||||
@@ -557,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)
|
||||||
@@ -567,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
import (
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
func registerDebugCommand(_ *cobra.Command) {
|
func registerDebugCommand(_ *cobra.Command, _ *global.Options) {
|
||||||
// No commands to register in non-debug mode
|
// No commands to register in non-debug mode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,18 @@ 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"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDiffCommand() *cobra.Command {
|
func newDiffCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts DiffOptions
|
var opts DiffOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -52,7 +53,7 @@ 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(), opts, globalOptions, args)
|
return runDiff(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,10 +70,10 @@ func (opts *DiffOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
f.BoolVar(&opts.ShowMetadata, "metadata", false, "print changes in metadata")
|
f.BoolVar(&opts.ShowMetadata, "metadata", false, "print changes in metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSnapshot(ctx context.Context, be restic.Lister, repo restic.LoaderUnpacked, desc string) (*restic.Snapshot, string, error) {
|
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
|
||||||
}
|
}
|
||||||
@@ -82,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 {
|
||||||
@@ -105,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 restic.NodeTypeFile:
|
case data.NodeTypeFile:
|
||||||
s.Files++
|
s.Files++
|
||||||
case restic.NodeTypeDir:
|
case data.NodeTypeDir:
|
||||||
s.Dirs++
|
s.Dirs++
|
||||||
default:
|
default:
|
||||||
s.Others++
|
s.Others++
|
||||||
@@ -121,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 restic.NodeTypeFile:
|
case data.NodeTypeFile:
|
||||||
for _, blob := range node.Content {
|
for _, blob := range node.Content {
|
||||||
h := restic.BlobHandle{
|
h := restic.BlobHandle{
|
||||||
ID: blob,
|
ID: blob,
|
||||||
@@ -135,7 +137,7 @@ func addBlobs(bs restic.BlobSet, node *restic.Node) {
|
|||||||
}
|
}
|
||||||
bs.Insert(h)
|
bs.Insert(h)
|
||||||
}
|
}
|
||||||
case restic.NodeTypeDir:
|
case data.NodeTypeDir:
|
||||||
h := restic.BlobHandle{
|
h := restic.BlobHandle{
|
||||||
ID: *node.Subtree,
|
ID: *node.Subtree,
|
||||||
Type: restic.TreeBlob,
|
Type: restic.TreeBlob,
|
||||||
@@ -151,12 +153,12 @@ type DiffStatsContainer struct {
|
|||||||
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++
|
||||||
@@ -166,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,30 +176,33 @@ func updateBlobs(repo restic.Loader, blobs restic.BlobSet, stats *DiffStat) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, blobs restic.BlobSet, prefix string, id restic.ID) error {
|
func (c *Comparer) printDir(ctx context.Context, mode string, stats *DiffStat, blobs restic.AssociatedBlobSet, prefix string, id restic.ID) error {
|
||||||
debug.Log("print %v tree %v", mode, id)
|
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 == restic.NodeTypeDir {
|
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 == restic.NodeTypeDir {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,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 == restic.NodeTypeDir {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,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 := ""
|
||||||
|
|
||||||
@@ -287,12 +281,12 @@ func (c *Comparer) diffTree(ctx context.Context, stats *DiffStatsContainer, pref
|
|||||||
mod += "T"
|
mod += "T"
|
||||||
}
|
}
|
||||||
|
|
||||||
if node2.Type == restic.NodeTypeDir {
|
if node2.Type == data.NodeTypeDir {
|
||||||
name += "/"
|
name += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
if node1.Type == restic.NodeTypeFile &&
|
if node1.Type == data.NodeTypeFile &&
|
||||||
node2.Type == restic.NodeTypeFile &&
|
node2.Type == data.NodeTypeFile &&
|
||||||
!reflect.DeepEqual(node1.Content, node2.Content) {
|
!reflect.DeepEqual(node1.Content, node2.Content) {
|
||||||
mod += "M"
|
mod += "M"
|
||||||
stats.ChangedFiles++
|
stats.ChangedFiles++
|
||||||
@@ -314,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 == restic.NodeTypeDir && node2.Type == restic.NodeTypeDir {
|
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)
|
||||||
@@ -322,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 == restic.NodeTypeDir {
|
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 == restic.NodeTypeDir {
|
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 == restic.NodeTypeDir {
|
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 == restic.NodeTypeDir {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,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
|
||||||
}
|
}
|
||||||
@@ -386,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,12 +396,12 @@ 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
|
||||||
}
|
}
|
||||||
@@ -414,17 +409,18 @@ func runDiff(ctx context.Context, opts DiffOptions, gopts GlobalOptions, args []
|
|||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,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})
|
||||||
@@ -450,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(stats.Added.Bytes))
|
printer.S(" Added: %-5s", ui.FormatBytes(stats.Added.Bytes))
|
||||||
Printf(" Removed: %-5s\n", ui.FormatBytes(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,16 +7,19 @@ 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"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDumpCommand() *cobra.Command {
|
func newDumpCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts DumpOptions
|
var opts DumpOptions
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "dump [flags] snapshotID file",
|
Use: "dump [flags] snapshotID file",
|
||||||
@@ -46,7 +49,8 @@ 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(), opts, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
return runDump(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +60,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
|
|
||||||
// DumpOptions collects all options for the dump command.
|
// 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
|
||||||
}
|
}
|
||||||
@@ -76,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] == "" {
|
||||||
@@ -88,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 && node.Type == restic.NodeTypeFile:
|
case l == 1 && node.Type == data.NodeTypeFile:
|
||||||
return d.WriteNode(ctx, node)
|
return d.WriteNode(ctx, node)
|
||||||
case l > 1 && node.Type == restic.NodeTypeDir:
|
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 node.Type == restic.NodeTypeDir:
|
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 node.Type != restic.NodeTypeFile:
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,11 +131,13 @@ func printFromTree(ctx context.Context, tree *restic.Tree, repo restic.BlobLoade
|
|||||||
return fmt.Errorf("path %q not found in snapshot", item)
|
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:
|
||||||
@@ -142,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)
|
||||||
@@ -198,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,16 +1,15 @@
|
|||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newFeaturesCommand() *cobra.Command {
|
func newFeaturesCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "features",
|
Use: "features",
|
||||||
Short: "Print list of feature flags",
|
Short: "Print list of feature flags",
|
||||||
@@ -39,7 +38,7 @@ Exit status is 1 if there was any error.
|
|||||||
return errors.Fatal("the feature command expects no arguments")
|
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()
|
||||||
@@ -51,7 +50,7 @@ Exit status is 1 if there was any error.
|
|||||||
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())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,14 +12,17 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newFindCommand() *cobra.Command {
|
func newFindCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts FindOptions
|
var opts FindOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -48,7 +53,8 @@ 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(), opts, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
return runFind(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +73,7 @@ type FindOptions struct {
|
|||||||
ListLong bool
|
ListLong bool
|
||||||
HumanReadable bool
|
HumanReadable bool
|
||||||
Reverse bool
|
Reverse bool
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *FindOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *FindOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
@@ -121,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"`
|
||||||
@@ -148,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 {
|
||||||
@@ -189,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"`
|
||||||
@@ -207,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 {
|
||||||
@@ -244,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
|
||||||
}
|
}
|
||||||
@@ -263,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 {
|
||||||
@@ -273,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
|
||||||
}
|
}
|
||||||
@@ -305,7 +323,7 @@ func (f *Finder) findInSnapshot(ctx context.Context, sn *restic.Snapshot) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
var errIfNoMatch error
|
var errIfNoMatch error
|
||||||
if node.Type == restic.NodeTypeDir {
|
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)
|
||||||
@@ -363,7 +381,7 @@ func (f *Finder) findTree(treeID restic.ID, nodepath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
func (f *Finder) findIDs(ctx context.Context, sn *data.Snapshot) error {
|
||||||
debug.Log("searching IDs in snapshot %s", sn.ID())
|
debug.Log("searching IDs in snapshot %s", sn.ID())
|
||||||
|
|
||||||
if sn.Tree == nil {
|
if sn.Tree == nil {
|
||||||
@@ -371,11 +389,12 @@ 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
|
||||||
}
|
}
|
||||||
@@ -395,7 +414,7 @@ func (f *Finder) findIDs(ctx context.Context, sn *restic.Snapshot) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == restic.NodeTypeFile && f.blobIDs != nil {
|
if node.Type == data.NodeTypeFile && f.blobIDs != nil {
|
||||||
for _, id := range node.Content {
|
for _, id := range node.Content {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
@@ -431,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 {
|
||||||
@@ -451,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 {
|
||||||
|
switch b.Type {
|
||||||
|
case restic.DataBlob:
|
||||||
f.blobIDs[b.ID.String()] = struct{}{}
|
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 {
|
||||||
@@ -524,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
|
||||||
}
|
}
|
||||||
@@ -532,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,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 {
|
||||||
@@ -586,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) ||
|
||||||
@@ -594,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
|
||||||
}
|
}
|
||||||
@@ -604,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 {
|
||||||
@@ -635,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 {
|
||||||
|
|||||||
@@ -3,18 +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, opts FindOptions, 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
|
||||||
|
|
||||||
return runFind(context.TODO(), opts, gopts, []string{pattern})
|
return runFind(ctx, opts, gopts, []string{pattern}, gopts.Term)
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
@@ -95,7 +100,7 @@ func TestFindSorting(t *testing.T) {
|
|||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
opts := BackupOptions{}
|
opts := BackupOptions{}
|
||||||
|
|
||||||
// first backup
|
// first backup
|
||||||
@@ -114,14 +119,14 @@ func TestFindSorting(t *testing.T) {
|
|||||||
// first restic find - with default FindOptions{}
|
// first restic find - with default FindOptions{}
|
||||||
results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
|
results := testRunFind(t, true, FindOptions{}, env.gopts, "testfile")
|
||||||
lines := strings.Split(string(results), "\n")
|
lines := strings.Split(string(results), "\n")
|
||||||
rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines))
|
rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines))
|
||||||
matches := []testMatches{}
|
matches := []testMatches{}
|
||||||
rtest.OK(t, json.Unmarshal(results, &matches))
|
rtest.OK(t, json.Unmarshal(results, &matches))
|
||||||
|
|
||||||
// run second restic find with --reverse, sort oldest to newest
|
// run second restic find with --reverse, sort oldest to newest
|
||||||
resultsReverse := testRunFind(t, true, FindOptions{Reverse: true}, env.gopts, "testfile")
|
resultsReverse := testRunFind(t, true, FindOptions{Reverse: true}, env.gopts, "testfile")
|
||||||
lines = strings.Split(string(resultsReverse), "\n")
|
lines = strings.Split(string(resultsReverse), "\n")
|
||||||
rtest.Assert(t, len(lines) == 2, "expected two files found in repo (%v), found %d", datafile, len(lines))
|
rtest.Assert(t, len(lines) == 2, "expected two lines of output, found %d", len(lines))
|
||||||
matchesReverse := []testMatches{}
|
matchesReverse := []testMatches{}
|
||||||
rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse))
|
rtest.OK(t, json.Unmarshal(resultsReverse, &matchesReverse))
|
||||||
|
|
||||||
@@ -131,3 +136,141 @@ func TestFindSorting(t *testing.T) {
|
|||||||
rtest.Assert(t, matches[0].SnapshotID == matchesReverse[1].SnapshotID, "matches should be sorted 1")
|
rtest.Assert(t, matches[0].SnapshotID == matchesReverse[1].SnapshotID, "matches should be sorted 1")
|
||||||
rtest.Assert(t, matches[1].SnapshotID == matchesReverse[0].SnapshotID, "matches should be sorted 2")
|
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:])
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"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/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newForgetCommand() *cobra.Command {
|
func newForgetCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts ForgetOptions
|
var opts ForgetOptions
|
||||||
var pruneOpts PruneOptions
|
var pruneOpts PruneOptions
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ EXIT STATUS
|
|||||||
|
|
||||||
Exit status is 0 if the command was successful.
|
Exit status is 0 if the command was successful.
|
||||||
Exit status is 1 if there was any error.
|
Exit status is 1 if there was any error.
|
||||||
|
Exit status is 3 if there was an error removing one or more snapshots.
|
||||||
Exit status is 10 if the repository does not exist.
|
Exit status is 10 if the repository does not exist.
|
||||||
Exit status is 11 if the repository is already locked.
|
Exit status is 11 if the repository is already locked.
|
||||||
Exit status is 12 if the password is incorrect.
|
Exit status is 12 if the password is incorrect.
|
||||||
@@ -48,9 +51,8 @@ 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()
|
return runForget(cmd.Context(), opts, pruneOpts, *globalOptions, globalOptions.Term, args)
|
||||||
return runForget(cmd.Context(), opts, pruneOpts, globalOptions, term, args)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
type ForgetPolicyCount int
|
type ForgetPolicyCount int
|
||||||
|
|
||||||
var ErrNegativePolicyCount = errors.New("negative values not allowed, use 'unlimited' instead")
|
var ErrNegativePolicyCount = errors.New("negative values not allowed, use 'unlimited' instead")
|
||||||
|
var ErrFailedToRemoveOneOrMoreSnapshots = errors.New("failed to remove one or more snapshots")
|
||||||
|
|
||||||
func (c *ForgetPolicyCount) Set(s string) error {
|
func (c *ForgetPolicyCount) Set(s string) error {
|
||||||
switch s {
|
switch s {
|
||||||
@@ -102,21 +105,21 @@ type ForgetOptions struct {
|
|||||||
Weekly ForgetPolicyCount
|
Weekly ForgetPolicyCount
|
||||||
Monthly ForgetPolicyCount
|
Monthly ForgetPolicyCount
|
||||||
Yearly ForgetPolicyCount
|
Yearly ForgetPolicyCount
|
||||||
Within restic.Duration
|
Within data.Duration
|
||||||
WithinHourly restic.Duration
|
WithinHourly data.Duration
|
||||||
WithinDaily restic.Duration
|
WithinDaily data.Duration
|
||||||
WithinWeekly restic.Duration
|
WithinWeekly data.Duration
|
||||||
WithinMonthly restic.Duration
|
WithinMonthly data.Duration
|
||||||
WithinYearly restic.Duration
|
WithinYearly data.Duration
|
||||||
KeepTags restic.TagLists
|
KeepTags data.TagLists
|
||||||
|
|
||||||
UnsafeAllowRemoveAll bool
|
UnsafeAllowRemoveAll bool
|
||||||
|
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
Compact bool
|
Compact bool
|
||||||
|
|
||||||
// Grouping
|
// Grouping
|
||||||
GroupBy restic.SnapshotGroupByOptions
|
GroupBy data.SnapshotGroupByOptions
|
||||||
DryRun bool
|
DryRun bool
|
||||||
Prune bool
|
Prune bool
|
||||||
}
|
}
|
||||||
@@ -147,7 +150,7 @@ func (opts *ForgetOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, false)
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, false)
|
||||||
|
|
||||||
f.BoolVarP(&opts.Compact, "compact", "c", false, "use compact output format")
|
f.BoolVarP(&opts.Compact, "compact", "c", false, "use compact output format")
|
||||||
opts.GroupBy = restic.SnapshotGroupByOptions{Host: true, Path: true}
|
opts.GroupBy = data.SnapshotGroupByOptions{Host: true, Path: true}
|
||||||
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
|
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma (disable grouping with '')")
|
||||||
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
|
f.BoolVarP(&opts.DryRun, "dry-run", "n", false, "do not delete anything, just print what would be done")
|
||||||
f.BoolVar(&opts.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
|
f.BoolVar(&opts.Prune, "prune", false, "automatically run the 'prune' command if snapshots have been removed")
|
||||||
@@ -161,7 +164,7 @@ func verifyForgetOptions(opts *ForgetOptions) error {
|
|||||||
return errors.Fatal("negative values other than -1 are not allowed for --keep-*")
|
return errors.Fatal("negative values other than -1 are not allowed for --keep-*")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range []restic.Duration{opts.Within, opts.WithinHourly, opts.WithinDaily,
|
for _, d := range []data.Duration{opts.Within, opts.WithinHourly, opts.WithinDaily,
|
||||||
opts.WithinMonthly, opts.WithinWeekly, opts.WithinYearly} {
|
opts.WithinMonthly, opts.WithinWeekly, opts.WithinYearly} {
|
||||||
if d.Hours < 0 || d.Days < 0 || d.Months < 0 || d.Years < 0 {
|
if d.Hours < 0 || d.Days < 0 || d.Months < 0 || d.Years < 0 {
|
||||||
return errors.Fatal("durations containing negative values are not allowed for --keep-within*")
|
return errors.Fatal("durations containing negative values are not allowed for --keep-within*")
|
||||||
@@ -171,7 +174,7 @@ func verifyForgetOptions(opts *ForgetOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error {
|
func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOptions, gopts global.Options, term ui.Terminal, args []string) error {
|
||||||
err := verifyForgetOptions(&opts)
|
err := verifyForgetOptions(&opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -186,22 +189,17 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for forget command")
|
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for forget command")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock)
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
verbosity := gopts.verbosity
|
var snapshots data.Snapshots
|
||||||
if gopts.JSON {
|
|
||||||
verbosity = 0
|
|
||||||
}
|
|
||||||
printer := newTerminalProgressPrinter(verbosity, term)
|
|
||||||
|
|
||||||
var snapshots restic.Snapshots
|
|
||||||
removeSnIDs := restic.NewIDSet()
|
removeSnIDs := restic.NewIDSet()
|
||||||
|
|
||||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) {
|
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
|
||||||
snapshots = append(snapshots, sn)
|
snapshots = append(snapshots, sn)
|
||||||
}
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
@@ -216,12 +214,12 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
removeSnIDs.Insert(*sn.ID())
|
removeSnIDs.Insert(*sn.ID())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
snapshotGroups, _, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
snapshotGroups, _, err := data.GroupSnapshots(snapshots, opts.GroupBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
policy := restic.ExpirePolicy{
|
policy := data.ExpirePolicy{
|
||||||
Last: int(opts.Last),
|
Last: int(opts.Last),
|
||||||
Hourly: int(opts.Hourly),
|
Hourly: int(opts.Hourly),
|
||||||
Daily: int(opts.Daily),
|
Daily: int(opts.Daily),
|
||||||
@@ -256,13 +254,13 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
}
|
}
|
||||||
|
|
||||||
if gopts.Verbose >= 1 && !gopts.JSON {
|
if gopts.Verbose >= 1 && !gopts.JSON {
|
||||||
err = PrintSnapshotGroupHeader(globalOptions.stdout, k)
|
err = PrintSnapshotGroupHeader(gopts.Term.OutputWriter(), k)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var key restic.SnapshotGroupKey
|
var key data.SnapshotGroupKey
|
||||||
if json.Unmarshal([]byte(k), &key) != nil {
|
if json.Unmarshal([]byte(k), &key) != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -272,21 +270,25 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
fg.Host = key.Hostname
|
fg.Host = key.Hostname
|
||||||
fg.Paths = key.Paths
|
fg.Paths = key.Paths
|
||||||
|
|
||||||
keep, remove, reasons := restic.ApplyPolicy(snapshotGroup, policy)
|
keep, remove, reasons := data.ApplyPolicy(snapshotGroup, policy)
|
||||||
|
|
||||||
if !policy.Empty() && len(keep) == 0 {
|
if !policy.Empty() && len(keep) == 0 {
|
||||||
return fmt.Errorf("refusing to delete last snapshot of snapshot group \"%v\"", key.String())
|
return fmt.Errorf("refusing to delete last snapshot of snapshot group \"%v\"", key.String())
|
||||||
}
|
}
|
||||||
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
|
if len(keep) != 0 && !gopts.Quiet && !gopts.JSON {
|
||||||
printer.P("keep %d snapshots:\n", len(keep))
|
printer.P("keep %d snapshots:\n", len(keep))
|
||||||
PrintSnapshots(globalOptions.stdout, keep, reasons, opts.Compact)
|
if err := PrintSnapshots(gopts.Term.OutputWriter(), keep, reasons, opts.Compact); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printer.P("\n")
|
printer.P("\n")
|
||||||
}
|
}
|
||||||
fg.Keep = asJSONSnapshots(keep)
|
fg.Keep = asJSONSnapshots(keep)
|
||||||
|
|
||||||
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
|
if len(remove) != 0 && !gopts.Quiet && !gopts.JSON {
|
||||||
printer.P("remove %d snapshots:\n", len(remove))
|
printer.P("remove %d snapshots:\n", len(remove))
|
||||||
PrintSnapshots(globalOptions.stdout, remove, nil, opts.Compact)
|
if err := PrintSnapshots(gopts.Term.OutputWriter(), remove, nil, opts.Compact); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
printer.P("\n")
|
printer.P("\n")
|
||||||
}
|
}
|
||||||
fg.Remove = asJSONSnapshots(remove)
|
fg.Remove = asJSONSnapshots(remove)
|
||||||
@@ -305,12 +307,15 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// these are the snapshots that failed to be removed
|
||||||
|
failedSnIDs := restic.NewIDSet()
|
||||||
if len(removeSnIDs) > 0 {
|
if len(removeSnIDs) > 0 {
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
bar := printer.NewCounter("files deleted")
|
bar := printer.NewCounter("files deleted")
|
||||||
err := restic.ParallelRemove(ctx, repo, removeSnIDs, restic.WriteableSnapshotFile, func(id restic.ID, err error) error {
|
err := restic.ParallelRemove(ctx, repo, removeSnIDs, restic.WriteableSnapshotFile, func(id restic.ID, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
printer.E("unable to remove %v/%v from the repository\n", restic.SnapshotFile, id)
|
printer.E("unable to remove %v/%v from the repository\n", restic.SnapshotFile, id)
|
||||||
|
failedSnIDs.Insert(id)
|
||||||
} else {
|
} else {
|
||||||
printer.VV("removed %v/%v\n", restic.SnapshotFile, id)
|
printer.VV("removed %v/%v\n", restic.SnapshotFile, id)
|
||||||
}
|
}
|
||||||
@@ -326,12 +331,16 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
}
|
}
|
||||||
|
|
||||||
if gopts.JSON && len(jsonGroups) > 0 {
|
if gopts.JSON && len(jsonGroups) > 0 {
|
||||||
err = printJSONForget(globalOptions.stdout, jsonGroups)
|
err = printJSONForget(gopts.Term.OutputWriter(), jsonGroups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(failedSnIDs) > 0 {
|
||||||
|
return ErrFailedToRemoveOneOrMoreSnapshots
|
||||||
|
}
|
||||||
|
|
||||||
if len(removeSnIDs) > 0 && opts.Prune {
|
if len(removeSnIDs) > 0 && opts.Prune {
|
||||||
if opts.DryRun {
|
if opts.DryRun {
|
||||||
printer.P("%d snapshots would be removed, running prune dry run\n", len(removeSnIDs))
|
printer.P("%d snapshots would be removed, running prune dry run\n", len(removeSnIDs))
|
||||||
@@ -339,7 +348,7 @@ func runForget(ctx context.Context, opts ForgetOptions, pruneOptions PruneOption
|
|||||||
printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs))
|
printer.P("%d snapshots have been removed, running prune\n", len(removeSnIDs))
|
||||||
}
|
}
|
||||||
pruneOptions.DryRun = opts.DryRun
|
pruneOptions.DryRun = opts.DryRun
|
||||||
return runPruneWithRepo(ctx, pruneOptions, gopts, repo, removeSnIDs, term)
|
return runPruneWithRepo(ctx, pruneOptions, repo, removeSnIDs, printer)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -355,7 +364,7 @@ type ForgetGroup struct {
|
|||||||
Reasons []KeepReason `json:"reasons"`
|
Reasons []KeepReason `json:"reasons"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func asJSONSnapshots(list restic.Snapshots) []Snapshot {
|
func asJSONSnapshots(list data.Snapshots) []Snapshot {
|
||||||
var resultList []Snapshot
|
var resultList []Snapshot
|
||||||
for _, sn := range list {
|
for _, sn := range list {
|
||||||
k := Snapshot{
|
k := Snapshot{
|
||||||
@@ -374,7 +383,7 @@ type KeepReason struct {
|
|||||||
Matches []string `json:"matches"`
|
Matches []string `json:"matches"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func asJSONKeeps(list []restic.KeepReason) []KeepReason {
|
func asJSONKeeps(list []data.KeepReason) []KeepReason {
|
||||||
var resultList []KeepReason
|
var resultList []KeepReason
|
||||||
for _, keep := range list {
|
for _, keep := range list {
|
||||||
k := KeepReason{
|
k := KeepReason{
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/data"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunForgetMayFail(gopts GlobalOptions, opts ForgetOptions, args ...string) error {
|
func testRunForgetMayFail(t testing.TB, gopts global.Options, opts ForgetOptions, args ...string) error {
|
||||||
pruneOpts := PruneOptions{
|
pruneOpts := PruneOptions{
|
||||||
MaxUnused: "5%",
|
MaxUnused: "5%",
|
||||||
}
|
}
|
||||||
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
return withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return runForget(context.TODO(), opts, pruneOpts, gopts, term, args)
|
return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.Term, args)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunForget(t testing.TB, gopts GlobalOptions, opts ForgetOptions, args ...string) {
|
func testRunForget(t testing.TB, gopts global.Options, opts ForgetOptions, args ...string) {
|
||||||
rtest.OK(t, testRunForgetMayFail(gopts, opts, args...))
|
rtest.OK(t, testRunForgetMayFail(t, gopts, opts, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunForgetSafetyNet(t *testing.T) {
|
func TestRunForgetSafetyNet(t *testing.T) {
|
||||||
@@ -38,27 +38,27 @@ func TestRunForgetSafetyNet(t *testing.T) {
|
|||||||
testListSnapshots(t, env.gopts, 2)
|
testListSnapshots(t, env.gopts, 2)
|
||||||
|
|
||||||
// --keep-tags invalid
|
// --keep-tags invalid
|
||||||
err := testRunForgetMayFail(env.gopts, ForgetOptions{
|
err := testRunForgetMayFail(t, env.gopts, ForgetOptions{
|
||||||
KeepTags: restic.TagLists{restic.TagList{"invalid"}},
|
KeepTags: data.TagLists{data.TagList{"invalid"}},
|
||||||
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true},
|
GroupBy: data.SnapshotGroupByOptions{Host: true, Path: true},
|
||||||
})
|
})
|
||||||
rtest.Assert(t, strings.Contains(err.Error(), `refusing to delete last snapshot of snapshot group "host example, path`), "wrong error message got %v", err)
|
rtest.Assert(t, strings.Contains(err.Error(), `refusing to delete last snapshot of snapshot group "host example, path`), "wrong error message got %v", err)
|
||||||
|
|
||||||
// disallow `forget --unsafe-allow-remove-all`
|
// disallow `forget --unsafe-allow-remove-all`
|
||||||
err = testRunForgetMayFail(env.gopts, ForgetOptions{
|
err = testRunForgetMayFail(t, env.gopts, ForgetOptions{
|
||||||
UnsafeAllowRemoveAll: true,
|
UnsafeAllowRemoveAll: true,
|
||||||
})
|
})
|
||||||
rtest.Assert(t, strings.Contains(err.Error(), `--unsafe-allow-remove-all is not allowed unless a snapshot filter option is specified`), "wrong error message got %v", err)
|
rtest.Assert(t, strings.Contains(err.Error(), `--unsafe-allow-remove-all is not allowed unless a snapshot filter option is specified`), "wrong error message got %v", err)
|
||||||
|
|
||||||
// disallow `forget` without options
|
// disallow `forget` without options
|
||||||
err = testRunForgetMayFail(env.gopts, ForgetOptions{})
|
err = testRunForgetMayFail(t, env.gopts, ForgetOptions{})
|
||||||
rtest.Assert(t, strings.Contains(err.Error(), `no policy was specified, no snapshots will be removed`), "wrong error message got %v", err)
|
rtest.Assert(t, strings.Contains(err.Error(), `no policy was specified, no snapshots will be removed`), "wrong error message got %v", err)
|
||||||
|
|
||||||
// `forget --host example --unsafe-allow-remove-all` should work
|
// `forget --host example --unsafe-allow-remove-all` should work
|
||||||
testRunForget(t, env.gopts, ForgetOptions{
|
testRunForget(t, env.gopts, ForgetOptions{
|
||||||
UnsafeAllowRemoveAll: true,
|
UnsafeAllowRemoveAll: true,
|
||||||
GroupBy: restic.SnapshotGroupByOptions{Host: true, Path: true},
|
GroupBy: data.SnapshotGroupByOptions{Host: true, Path: true},
|
||||||
SnapshotFilter: restic.SnapshotFilter{
|
SnapshotFilter: data.SnapshotFilter{
|
||||||
Hosts: []string{opts.Host},
|
Hosts: []string{opts.Host},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/data"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
@@ -69,18 +69,18 @@ func TestForgetOptionValues(t *testing.T) {
|
|||||||
{ForgetOptions{Weekly: -2}, negValErrorMsg},
|
{ForgetOptions{Weekly: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Monthly: -2}, negValErrorMsg},
|
{ForgetOptions{Monthly: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Yearly: -2}, negValErrorMsg},
|
{ForgetOptions{Yearly: -2}, negValErrorMsg},
|
||||||
{ForgetOptions{Within: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
{ForgetOptions{Within: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||||
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
{ForgetOptions{WithinHourly: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||||
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
{ForgetOptions{WithinDaily: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||||
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
{ForgetOptions{WithinWeekly: data.ParseDurationOrPanic("1y2m3d3h")}, ""},
|
||||||
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
{ForgetOptions{WithinMonthly: data.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
||||||
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
{ForgetOptions{WithinYearly: data.ParseDurationOrPanic("2y4m6d8h")}, ""},
|
||||||
{ForgetOptions{Within: restic.ParseDurationOrPanic("-1y2m3d3h")}, negDurationValErrorMsg},
|
{ForgetOptions{Within: data.ParseDurationOrPanic("-1y2m3d3h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinHourly: restic.ParseDurationOrPanic("1y-2m3d3h")}, negDurationValErrorMsg},
|
{ForgetOptions{WithinHourly: data.ParseDurationOrPanic("1y-2m3d3h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinDaily: restic.ParseDurationOrPanic("1y2m-3d3h")}, negDurationValErrorMsg},
|
{ForgetOptions{WithinDaily: data.ParseDurationOrPanic("1y2m-3d3h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinWeekly: restic.ParseDurationOrPanic("1y2m3d-3h")}, negDurationValErrorMsg},
|
{ForgetOptions{WithinWeekly: data.ParseDurationOrPanic("1y2m3d-3h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinMonthly: restic.ParseDurationOrPanic("-2y4m6d8h")}, negDurationValErrorMsg},
|
{ForgetOptions{WithinMonthly: data.ParseDurationOrPanic("-2y4m6d8h")}, negDurationValErrorMsg},
|
||||||
{ForgetOptions{WithinYearly: restic.ParseDurationOrPanic("2y-4m6d8h")}, negDurationValErrorMsg},
|
{ForgetOptions{WithinYearly: data.ParseDurationOrPanic("2y-4m6d8h")}, negDurationValErrorMsg},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
@@ -96,7 +96,38 @@ func TestForgetOptionValues(t *testing.T) {
|
|||||||
|
|
||||||
func TestForgetHostnameDefaulting(t *testing.T) {
|
func TestForgetHostnameDefaulting(t *testing.T) {
|
||||||
t.Setenv("RESTIC_HOST", "testhost")
|
t.Setenv("RESTIC_HOST", "testhost")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "env default when flag not set",
|
||||||
|
args: nil,
|
||||||
|
want: []string{"testhost"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "flag overrides env",
|
||||||
|
args: []string{"--host", "flaghost"},
|
||||||
|
want: []string{"flaghost"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty flag clears env",
|
||||||
|
args: []string{"--host", ""},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
set := pflag.NewFlagSet(tt.name, pflag.ContinueOnError)
|
||||||
opts := ForgetOptions{}
|
opts := ForgetOptions{}
|
||||||
opts.AddFlags(pflag.NewFlagSet("test", pflag.ContinueOnError))
|
opts.AddFlags(set)
|
||||||
rtest.Equals(t, []string{"testhost"}, opts.Hosts)
|
err := set.Parse(tt.args)
|
||||||
|
rtest.Assert(t, err == nil, "expected no error for input")
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
rtest.Equals(t, tt.want, opts.Hosts)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/cobra/doc"
|
"github.com/spf13/cobra/doc"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newGenerateCommand() *cobra.Command {
|
func newGenerateCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts generateOptions
|
var opts generateOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -29,7 +32,7 @@ Exit status is 1 if there was any error.
|
|||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(_ *cobra.Command, args []string) error {
|
RunE: func(_ *cobra.Command, args []string) error {
|
||||||
return runGenerate(opts, args)
|
return runGenerate(opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
opts.AddFlags(cmd.Flags())
|
opts.AddFlags(cmd.Flags())
|
||||||
@@ -52,7 +55,7 @@ func (opts *generateOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
|
f.StringVar(&opts.PowerShellCompletionFile, "powershell-completion", "", "write powershell completion `file` (`-` for stdout)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeManpages(root *cobra.Command, dir string) error {
|
func writeManpages(root *cobra.Command, dir string, printer progress.Printer) error {
|
||||||
// use a fixed date for the man pages so that generating them is deterministic
|
// use a fixed date for the man pages so that generating them is deterministic
|
||||||
date, err := time.Parse("Jan 2006", "Jan 2017")
|
date, err := time.Parse("Jan 2006", "Jan 2017")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,14 +69,12 @@ func writeManpages(root *cobra.Command, dir string) error {
|
|||||||
Date: &date,
|
Date: &date,
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("writing man pages to directory %v\n", dir)
|
printer.P("writing man pages to directory %v", dir)
|
||||||
return doc.GenManTree(root, header, dir)
|
return doc.GenManTree(root, header, dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeCompletion(filename string, shell string, generate func(w io.Writer) error) (err error) {
|
func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts global.Options) (err error) {
|
||||||
if stdoutIsTerminal() {
|
printer.PT("writing %s completion file to %v", shell, filename)
|
||||||
Verbosef("writing %s completion file to %v\n", shell, filename)
|
|
||||||
}
|
|
||||||
var outWriter io.Writer
|
var outWriter io.Writer
|
||||||
if filename != "-" {
|
if filename != "-" {
|
||||||
var outFile *os.File
|
var outFile *os.File
|
||||||
@@ -84,7 +85,7 @@ func writeCompletion(filename string, shell string, generate func(w io.Writer) e
|
|||||||
defer func() { err = outFile.Close() }()
|
defer func() { err = outFile.Close() }()
|
||||||
outWriter = outFile
|
outWriter = outFile
|
||||||
} else {
|
} else {
|
||||||
outWriter = globalOptions.stdout
|
outWriter = gopts.Term.OutputWriter()
|
||||||
}
|
}
|
||||||
|
|
||||||
err = generate(outWriter)
|
err = generate(outWriter)
|
||||||
@@ -110,15 +111,16 @@ func checkStdoutForSingleShell(opts generateOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runGenerate(opts generateOptions, args []string) error {
|
func runGenerate(opts generateOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
|
return errors.Fatal("the generate command expects no arguments, only options - please see `restic help generate` for usage and flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdRoot := newRootCommand()
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
cmdRoot := newRootCommand(&global.Options{})
|
||||||
|
|
||||||
if opts.ManDir != "" {
|
if opts.ManDir != "" {
|
||||||
err := writeManpages(cmdRoot, opts.ManDir)
|
err := writeManpages(cmdRoot, opts.ManDir, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -130,28 +132,28 @@ func runGenerate(opts generateOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.BashCompletionFile != "" {
|
if opts.BashCompletionFile != "" {
|
||||||
err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion)
|
err := writeCompletion(opts.BashCompletionFile, "bash", cmdRoot.GenBashCompletion, printer, gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.FishCompletionFile != "" {
|
if opts.FishCompletionFile != "" {
|
||||||
err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) })
|
err := writeCompletion(opts.FishCompletionFile, "fish", func(w io.Writer) error { return cmdRoot.GenFishCompletion(w, true) }, printer, gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.ZSHCompletionFile != "" {
|
if opts.ZSHCompletionFile != "" {
|
||||||
err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion)
|
err := writeCompletion(opts.ZSHCompletionFile, "zsh", cmdRoot.GenZshCompletion, printer, gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.PowerShellCompletionFile != "" {
|
if opts.PowerShellCompletionFile != "" {
|
||||||
err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion)
|
err := writeCompletion(opts.PowerShellCompletionFile, "powershell", cmdRoot.GenPowerShellCompletion, printer, gopts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"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 testRunGenerate(t testing.TB, gopts global.Options, opts generateOptions) ([]byte, error) {
|
||||||
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runGenerate(opts, gopts, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateStdout(t *testing.T) {
|
func TestGenerateStdout(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -21,20 +29,14 @@ func TestGenerateStdout(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
buf := bytes.NewBuffer(nil)
|
output, err := testRunGenerate(t, global.Options{}, tc.opts)
|
||||||
globalOptions.stdout = buf
|
|
||||||
err := runGenerate(tc.opts, []string{})
|
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
completionString := buf.String()
|
rtest.Assert(t, strings.Contains(string(output), "# "+tc.name+" completion for restic"), "has no expected completion header")
|
||||||
rtest.Assert(t, strings.Contains(completionString, "# "+tc.name+" completion for restic"), "has no expected completion header")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Generate shell completions to stdout for two shells", func(t *testing.T) {
|
t.Run("Generate shell completions to stdout for two shells", func(t *testing.T) {
|
||||||
buf := bytes.NewBuffer(nil)
|
_, err := testRunGenerate(t, global.Options{}, generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"})
|
||||||
globalOptions.stdout = buf
|
|
||||||
opts := generateOptions{BashCompletionFile: "-", FishCompletionFile: "-"}
|
|
||||||
err := runGenerate(opts, []string{})
|
|
||||||
rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails")
|
rtest.Assert(t, err != nil, "generate shell completions to stdout for two shells fails")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ import (
|
|||||||
"github.com/restic/chunker"
|
"github.com/restic/chunker"
|
||||||
"github.com/restic/restic/internal/backend/location"
|
"github.com/restic/restic/internal/backend/location"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"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"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newInitCommand() *cobra.Command {
|
func newInitCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts InitOptions
|
var opts InitOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -33,7 +35,7 @@ Exit status is 1 if there was any error.
|
|||||||
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 runInit(cmd.Context(), opts, globalOptions, args)
|
return runInit(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
opts.AddFlags(cmd.Flags())
|
opts.AddFlags(cmd.Flags())
|
||||||
@@ -42,105 +44,78 @@ Exit status is 1 if there was any error.
|
|||||||
|
|
||||||
// InitOptions bundles all options for the init command.
|
// InitOptions bundles all options for the init command.
|
||||||
type InitOptions struct {
|
type InitOptions struct {
|
||||||
secondaryRepoOptions
|
global.SecondaryRepoOptions
|
||||||
CopyChunkerParameters bool
|
CopyChunkerParameters bool
|
||||||
RepositoryVersion string
|
RepositoryVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *InitOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *InitOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
opts.secondaryRepoOptions.AddFlags(f, "secondary", "to copy chunker parameters from")
|
opts.SecondaryRepoOptions.AddFlags(f, "secondary", "to copy chunker parameters from")
|
||||||
f.BoolVar(&opts.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
|
f.BoolVar(&opts.CopyChunkerParameters, "copy-chunker-params", false, "copy chunker parameters from the secondary repository (useful with the copy command)")
|
||||||
f.StringVar(&opts.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'")
|
f.StringVar(&opts.RepositoryVersion, "repository-version", "stable", "repository format version to use, allowed values are a format version, 'latest' and 'stable'")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []string) error {
|
func runInit(ctx context.Context, opts InitOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return errors.Fatal("the init command expects no arguments, only options - please see `restic help init` for usage and flags")
|
return errors.Fatal("the init command expects no arguments, only options - please see `restic help init` for usage and flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
|
||||||
var version uint
|
var version uint
|
||||||
if opts.RepositoryVersion == "latest" || opts.RepositoryVersion == "" {
|
switch opts.RepositoryVersion {
|
||||||
|
case "latest", "":
|
||||||
version = restic.MaxRepoVersion
|
version = restic.MaxRepoVersion
|
||||||
} else if opts.RepositoryVersion == "stable" {
|
case "stable":
|
||||||
version = restic.StableRepoVersion
|
version = restic.StableRepoVersion
|
||||||
} else {
|
default:
|
||||||
v, err := strconv.ParseUint(opts.RepositoryVersion, 10, 32)
|
v, err := strconv.ParseUint(opts.RepositoryVersion, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatal("invalid repository version")
|
return errors.Fatal("invalid repository version")
|
||||||
}
|
}
|
||||||
version = uint(v)
|
version = uint(v)
|
||||||
}
|
}
|
||||||
if version < restic.MinRepoVersion || version > restic.MaxRepoVersion {
|
|
||||||
return errors.Fatalf("only repository versions between %v and %v are allowed", restic.MinRepoVersion, restic.MaxRepoVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkerPolynomial, err := maybeReadChunkerPolynomial(ctx, opts, gopts)
|
chunkerPolynomial, err := maybeReadChunkerPolynomial(ctx, opts, gopts, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
gopts.Repo, err = ReadRepo(gopts)
|
s, err := global.CreateRepository(ctx, gopts, version, chunkerPolynomial, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Fatalf("%s", err)
|
||||||
}
|
|
||||||
|
|
||||||
gopts.password, err = ReadPasswordTwice(ctx, gopts,
|
|
||||||
"enter password for new repository: ",
|
|
||||||
"enter password again: ")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
be, err := create(ctx, gopts.Repo, gopts, gopts.extended)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Fatalf("create repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := repository.New(be, repository.Options{
|
|
||||||
Compression: gopts.Compression,
|
|
||||||
PackSize: gopts.PackSize * 1024 * 1024,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Init(ctx, version, gopts.password, chunkerPolynomial)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Fatalf("create key in repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
Verbosef("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.backends, gopts.Repo))
|
printer.P("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.Backends, gopts.Repo))
|
||||||
if opts.CopyChunkerParameters && chunkerPolynomial != nil {
|
if opts.CopyChunkerParameters && chunkerPolynomial != nil {
|
||||||
Verbosef(" with chunker parameters copied from secondary repository\n")
|
printer.P(" with chunker parameters copied from secondary repository")
|
||||||
} else {
|
|
||||||
Verbosef("\n")
|
|
||||||
}
|
}
|
||||||
Verbosef("\n")
|
printer.P("")
|
||||||
Verbosef("Please note that knowledge of your password is required to access\n")
|
printer.P("Please note that knowledge of your password is required to access")
|
||||||
Verbosef("the repository. Losing your password means that your data is\n")
|
printer.P("the repository. Losing your password means that your data is")
|
||||||
Verbosef("irrecoverably lost.\n")
|
printer.P("irrecoverably lost.")
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
status := initSuccess{
|
status := initSuccess{
|
||||||
MessageType: "initialized",
|
MessageType: "initialized",
|
||||||
ID: s.Config().ID,
|
ID: s.Config().ID,
|
||||||
Repository: location.StripPassword(gopts.backends, gopts.Repo),
|
Repository: location.StripPassword(gopts.Backends, gopts.Repo),
|
||||||
}
|
}
|
||||||
return json.NewEncoder(globalOptions.stdout).Encode(status)
|
return json.NewEncoder(gopts.Term.OutputWriter()).Encode(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts GlobalOptions) (*chunker.Pol, error) {
|
func maybeReadChunkerPolynomial(ctx context.Context, opts InitOptions, gopts global.Options, printer progress.Printer) (*chunker.Pol, error) {
|
||||||
if opts.CopyChunkerParameters {
|
if opts.CopyChunkerParameters {
|
||||||
otherGopts, _, err := fillSecondaryGlobalOpts(ctx, opts.secondaryRepoOptions, gopts, "secondary")
|
otherGopts, _, err := opts.SecondaryRepoOptions.FillGlobalOpts(ctx, gopts, "secondary")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
otherRepo, err := OpenRepository(ctx, otherGopts)
|
otherRepo, err := global.OpenRepository(ctx, otherGopts, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,27 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunInit(t testing.TB, opts GlobalOptions) {
|
func testRunInit(t testing.TB, gopts global.Options) {
|
||||||
repository.TestUseLowSecurityKDFParameters(t)
|
repository.TestUseLowSecurityKDFParameters(t)
|
||||||
restic.TestDisableCheckPolynomial(t)
|
restic.TestDisableCheckPolynomial(t)
|
||||||
restic.TestSetLockTimeout(t, 0)
|
restic.TestSetLockTimeout(t, 0)
|
||||||
|
|
||||||
rtest.OK(t, runInit(context.TODO(), InitOptions{}, opts, nil))
|
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
t.Logf("repository initialized at %v", opts.Repo)
|
return runInit(ctx, InitOptions{}, gopts, nil, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
t.Logf("repository initialized at %v", gopts.Repo)
|
||||||
|
|
||||||
// create temporary junk files to verify that restic does not trip over them
|
// create temporary junk files to verify that restic does not trip over them
|
||||||
for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
|
for _, path := range []string{"index", "snapshots", "keys", "locks", filepath.Join("data", "00")} {
|
||||||
rtest.OK(t, os.WriteFile(filepath.Join(opts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
|
rtest.OK(t, os.WriteFile(filepath.Join(gopts.Repo, path, "tmp12345"), []byte("junk file"), 0o600))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,20 +39,34 @@ func TestInitCopyChunkerParams(t *testing.T) {
|
|||||||
testRunInit(t, env2.gopts)
|
testRunInit(t, env2.gopts)
|
||||||
|
|
||||||
initOpts := InitOptions{
|
initOpts := InitOptions{
|
||||||
secondaryRepoOptions: secondaryRepoOptions{
|
SecondaryRepoOptions: global.SecondaryRepoOptions{
|
||||||
Repo: env2.gopts.Repo,
|
Repo: env2.gopts.Repo,
|
||||||
password: env2.gopts.password,
|
Password: env2.gopts.Password,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rtest.Assert(t, runInit(context.TODO(), initOpts, env.gopts, nil) != nil, "expected invalid init options to fail")
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runInit(ctx, initOpts, gopts, nil, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.Assert(t, err != nil, "expected invalid init options to fail")
|
||||||
|
|
||||||
initOpts.CopyChunkerParameters = true
|
initOpts.CopyChunkerParameters = true
|
||||||
rtest.OK(t, runInit(context.TODO(), initOpts, env.gopts, nil))
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runInit(ctx, initOpts, gopts, nil, gopts.Term)
|
||||||
repo, err := OpenRepository(context.TODO(), env.gopts)
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
otherRepo, err := OpenRepository(context.TODO(), env2.gopts)
|
var repo *repository.Repository
|
||||||
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
repo, err = global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
|
||||||
|
var otherRepo *repository.Repository
|
||||||
|
err = withTermStatus(t, env2.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
otherRepo, err = global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||||
|
return err
|
||||||
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial,
|
rtest.Assert(t, repo.Config().ChunkerPolynomial == otherRepo.Config().ChunkerPolynomial,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newKeyCommand() *cobra.Command {
|
func newKeyCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "key",
|
Use: "key",
|
||||||
Short: "Manage keys (passwords)",
|
Short: "Manage keys (passwords)",
|
||||||
@@ -17,10 +18,10 @@ per repository.
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newKeyAddCommand(),
|
newKeyAddCommand(globalOptions),
|
||||||
newKeyListCommand(),
|
newKeyListCommand(globalOptions),
|
||||||
newKeyPasswdCommand(),
|
newKeyPasswdCommand(globalOptions),
|
||||||
newKeyRemoveCommand(),
|
newKeyRemoveCommand(globalOptions),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newKeyAddCommand() *cobra.Command {
|
func newKeyAddCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts KeyAddOptions
|
var opts KeyAddOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -30,7 +33,7 @@ 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 runKeyAdd(cmd.Context(), globalOptions, opts, args)
|
return runKeyAdd(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,21 +55,22 @@ func (opts *KeyAddOptions) Add(flags *pflag.FlagSet) {
|
|||||||
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
|
flags.StringVarP(&opts.Hostname, "host", "", "", "the hostname for new key")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeyAdd(ctx context.Context, gopts GlobalOptions, opts KeyAddOptions, args []string) error {
|
func runKeyAdd(ctx context.Context, gopts global.Options, opts KeyAddOptions, args []string, term ui.Terminal) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")
|
return fmt.Errorf("the key add command expects no arguments, only options - please see `restic help key add` for usage and flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
ctx, repo, unlock, err := openWithAppendLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
return addKey(ctx, repo, gopts, opts)
|
return addKey(ctx, repo, gopts, opts, printer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyAddOptions) error {
|
func addKey(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyAddOptions, printer progress.Printer) error {
|
||||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -74,7 +78,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
|
|||||||
|
|
||||||
id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key())
|
id, err := repository.AddKey(ctx, repo, pw, opts.Username, opts.Hostname, repo.Key())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("creating new key failed: %v\n", err)
|
return errors.Fatalf("creating new key failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw)
|
err = switchToNewKeyAndRemoveIfBroken(ctx, repo, id, pw)
|
||||||
@@ -82,7 +86,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("saved new key with ID %s\n", id.ID())
|
printer.P("saved new key with ID %s", id.ID())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -90,7 +94,7 @@ func addKey(ctx context.Context, repo *repository.Repository, gopts GlobalOption
|
|||||||
// testKeyNewPassword is used to set a new password during integration testing.
|
// testKeyNewPassword is used to set a new password during integration testing.
|
||||||
var testKeyNewPassword string
|
var testKeyNewPassword string
|
||||||
|
|
||||||
func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile string, insecureNoPassword bool) (string, error) {
|
func getNewPassword(ctx context.Context, gopts global.Options, newPasswordFile string, insecureNoPassword bool) (string, error) {
|
||||||
if testKeyNewPassword != "" {
|
if testKeyNewPassword != "" {
|
||||||
return testKeyNewPassword, nil
|
return testKeyNewPassword, nil
|
||||||
}
|
}
|
||||||
@@ -103,7 +107,7 @@ func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile st
|
|||||||
}
|
}
|
||||||
|
|
||||||
if newPasswordFile != "" {
|
if newPasswordFile != "" {
|
||||||
password, err := loadPasswordFromFile(newPasswordFile)
|
password, err := global.LoadPasswordFromFile(newPasswordFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -116,11 +120,11 @@ func getNewPassword(ctx context.Context, gopts GlobalOptions, newPasswordFile st
|
|||||||
// Since we already have an open repository, temporary remove the password
|
// Since we already have an open repository, temporary remove the password
|
||||||
// to prompt the user for the passwd.
|
// to prompt the user for the passwd.
|
||||||
newopts := gopts
|
newopts := gopts
|
||||||
newopts.password = ""
|
newopts.Password = ""
|
||||||
// empty passwords are already handled above
|
// empty passwords are already handled above
|
||||||
newopts.InsecureNoPassword = false
|
newopts.InsecureNoPassword = false
|
||||||
|
|
||||||
return ReadPasswordTwice(ctx, newopts,
|
return global.ReadPasswordTwice(ctx, newopts,
|
||||||
"enter new password: ",
|
"enter new password: ",
|
||||||
"enter password again: ")
|
"enter password again: ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend"
|
"github.com/restic/restic/internal/backend"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
|
func testRunKeyListOtherIDs(t testing.TB, gopts global.Options) []string {
|
||||||
buf, err := withCaptureStdout(func() error {
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return runKeyList(context.TODO(), gopts, []string{})
|
return runKeyList(ctx, gopts, []string{}, gopts.Term)
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
@@ -33,49 +35,64 @@ func testRunKeyListOtherIDs(t testing.TB, gopts GlobalOptions) []string {
|
|||||||
return IDs
|
return IDs
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions) {
|
func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts global.Options) {
|
||||||
testKeyNewPassword = newPassword
|
testKeyNewPassword = newPassword
|
||||||
defer func() {
|
defer func() {
|
||||||
testKeyNewPassword = ""
|
testKeyNewPassword = ""
|
||||||
}()
|
}()
|
||||||
|
|
||||||
rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{}, []string{}))
|
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) {
|
func testRunKeyAddNewKeyUserHost(t testing.TB, gopts global.Options) {
|
||||||
testKeyNewPassword = "john's geheimnis"
|
testKeyNewPassword = "john's geheimnis"
|
||||||
defer func() {
|
defer func() {
|
||||||
testKeyNewPassword = ""
|
testKeyNewPassword = ""
|
||||||
}()
|
}()
|
||||||
|
|
||||||
t.Log("adding key for john@example.com")
|
t.Log("adding key for john@example.com")
|
||||||
rtest.OK(t, runKeyAdd(context.TODO(), gopts, KeyAddOptions{
|
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyAdd(ctx, gopts, KeyAddOptions{
|
||||||
Username: "john",
|
Username: "john",
|
||||||
Hostname: "example.com",
|
Hostname: "example.com",
|
||||||
}, []string{}))
|
}, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
repo, err := OpenRepository(context.TODO(), gopts)
|
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
key, err := repository.SearchKey(context.TODO(), repo, testKeyNewPassword, 2, "")
|
|
||||||
|
_ = withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
repo, err := global.OpenRepository(ctx, gopts, &progress.NoopPrinter{})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
key, err := repository.SearchKey(ctx, repo, testKeyNewPassword, 2, "")
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Equals(t, "john", key.Username)
|
rtest.Equals(t, "john", key.Username)
|
||||||
rtest.Equals(t, "example.com", key.Hostname)
|
rtest.Equals(t, "example.com", key.Hostname)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
|
func testRunKeyPasswd(t testing.TB, newPassword string, gopts global.Options) {
|
||||||
testKeyNewPassword = newPassword
|
testKeyNewPassword = newPassword
|
||||||
defer func() {
|
defer func() {
|
||||||
testKeyNewPassword = ""
|
testKeyNewPassword = ""
|
||||||
}()
|
}()
|
||||||
|
|
||||||
rtest.OK(t, runKeyPasswd(context.TODO(), gopts, KeyPasswdOptions{}, []string{}))
|
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunKeyRemove(t testing.TB, gopts GlobalOptions, IDs []string) {
|
func testRunKeyRemove(t testing.TB, gopts global.Options, IDs []string) {
|
||||||
t.Logf("remove %d keys: %q\n", len(IDs), IDs)
|
t.Logf("remove %d keys: %q\n", len(IDs), IDs)
|
||||||
for _, id := range IDs {
|
for _, id := range IDs {
|
||||||
rtest.OK(t, runKeyRemove(context.TODO(), gopts, []string{id}))
|
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyRemove(ctx, gopts, []string{id}, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,25 +104,28 @@ func TestKeyAddRemove(t *testing.T) {
|
|||||||
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
// must list keys more than once
|
// must list keys more than once
|
||||||
env.gopts.backendTestHook = nil
|
env.gopts.BackendTestHook = nil
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
testRunInit(t, env.gopts)
|
||||||
|
|
||||||
testRunKeyPasswd(t, "geheim2", env.gopts)
|
testRunKeyPasswd(t, "geheim2", env.gopts)
|
||||||
env.gopts.password = "geheim2"
|
env.gopts.Password = "geheim2"
|
||||||
t.Logf("changed password to %q", env.gopts.password)
|
t.Logf("changed password to %q", env.gopts.Password)
|
||||||
|
|
||||||
for _, newPassword := range passwordList {
|
for _, newPassword := range passwordList {
|
||||||
testRunKeyAddNewKey(t, newPassword, env.gopts)
|
testRunKeyAddNewKey(t, newPassword, env.gopts)
|
||||||
t.Logf("added new password %q", newPassword)
|
t.Logf("added new password %q", newPassword)
|
||||||
env.gopts.password = newPassword
|
env.gopts.Password = newPassword
|
||||||
testRunKeyRemove(t, env.gopts, testRunKeyListOtherIDs(t, env.gopts))
|
testRunKeyRemove(t, env.gopts, testRunKeyListOtherIDs(t, env.gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
env.gopts.password = passwordList[len(passwordList)-1]
|
env.gopts.Password = passwordList[len(passwordList)-1]
|
||||||
t.Logf("testing access with last password %q\n", env.gopts.password)
|
t.Logf("testing access with last password %q\n", env.gopts.Password)
|
||||||
rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{}))
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyList(ctx, gopts, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
|
|
||||||
testRunKeyAddNewKeyUserHost(t, env.gopts)
|
testRunKeyAddNewKeyUserHost(t, env.gopts)
|
||||||
@@ -116,33 +136,40 @@ func TestKeyAddInvalid(t *testing.T) {
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
testRunInit(t, env.gopts)
|
testRunInit(t, env.gopts)
|
||||||
|
|
||||||
err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyAdd(ctx, gopts, KeyAddOptions{
|
||||||
NewPasswordFile: "some-file",
|
NewPasswordFile: "some-file",
|
||||||
InsecureNoPassword: true,
|
InsecureNoPassword: true,
|
||||||
}, []string{})
|
}, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
rtest.Assert(t, strings.Contains(err.Error(), "only either"), "unexpected error message, got %q", err)
|
rtest.Assert(t, strings.Contains(err.Error(), "only either"), "unexpected error message, got %q", err)
|
||||||
|
|
||||||
pwfile := filepath.Join(t.TempDir(), "pwfile")
|
pwfile := filepath.Join(t.TempDir(), "pwfile")
|
||||||
rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666))
|
rtest.OK(t, os.WriteFile(pwfile, []byte{}, 0o666))
|
||||||
|
|
||||||
err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyAdd(ctx, gopts, KeyAddOptions{
|
||||||
NewPasswordFile: pwfile,
|
NewPasswordFile: pwfile,
|
||||||
}, []string{})
|
}, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err)
|
rtest.Assert(t, strings.Contains(err.Error(), "an empty password is not allowed by default"), "unexpected error message, got %q", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeyAddEmpty(t *testing.T) {
|
func TestKeyAddEmpty(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
// must list keys more than once
|
// must list keys more than once
|
||||||
env.gopts.backendTestHook = nil
|
env.gopts.BackendTestHook = nil
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
testRunInit(t, env.gopts)
|
testRunInit(t, env.gopts)
|
||||||
|
|
||||||
rtest.OK(t, runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyAdd(ctx, gopts, KeyAddOptions{
|
||||||
InsecureNoPassword: true,
|
InsecureNoPassword: true,
|
||||||
}, []string{}))
|
}, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
|
||||||
env.gopts.password = ""
|
env.gopts.Password = ""
|
||||||
env.gopts.InsecureNoPassword = true
|
env.gopts.InsecureNoPassword = true
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
@@ -161,7 +188,7 @@ func TestKeyProblems(t *testing.T) {
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
testRunInit(t, env.gopts)
|
||||||
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
env.gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
||||||
return &emptySaveBackend{r}, nil
|
return &emptySaveBackend{r}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,16 +197,23 @@ func TestKeyProblems(t *testing.T) {
|
|||||||
testKeyNewPassword = ""
|
testKeyNewPassword = ""
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{})
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
rtest.Assert(t, err != nil, "expected passwd change to fail")
|
rtest.Assert(t, err != nil, "expected passwd change to fail")
|
||||||
|
|
||||||
err = runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{})
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
rtest.Assert(t, err != nil, "expected key adding to fail")
|
rtest.Assert(t, err != nil, "expected key adding to fail")
|
||||||
|
|
||||||
t.Logf("testing access with initial password %q\n", env.gopts.password)
|
t.Logf("testing access with initial password %q\n", env.gopts.Password)
|
||||||
rtest.OK(t, runKeyList(context.TODO(), env.gopts, []string{}))
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyList(ctx, gopts, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,27 +222,37 @@ func TestKeyCommandInvalidArguments(t *testing.T) {
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
testRunInit(t, env.gopts)
|
||||||
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
env.gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
||||||
return &emptySaveBackend{r}, nil
|
return &emptySaveBackend{r}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := runKeyAdd(context.TODO(), env.gopts, KeyAddOptions{}, []string{"johndoe"})
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyAdd(ctx, gopts, KeyAddOptions{}, []string{"johndoe"}, gopts.Term)
|
||||||
|
})
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key add: %v", err)
|
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key add: %v", err)
|
||||||
|
|
||||||
err = runKeyPasswd(context.TODO(), env.gopts, KeyPasswdOptions{}, []string{"johndoe"})
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyPasswd(ctx, gopts, KeyPasswdOptions{}, []string{"johndoe"}, gopts.Term)
|
||||||
|
})
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key passwd: %v", err)
|
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key passwd: %v", err)
|
||||||
|
|
||||||
err = runKeyList(context.TODO(), env.gopts, []string{"johndoe"})
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyList(ctx, gopts, []string{"johndoe"}, gopts.Term)
|
||||||
|
})
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key list: %v", err)
|
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "no arguments"), "unexpected error for key list: %v", err)
|
||||||
|
|
||||||
err = runKeyRemove(context.TODO(), env.gopts, []string{})
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyRemove(ctx, gopts, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
|
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
|
||||||
|
|
||||||
err = runKeyRemove(context.TODO(), env.gopts, []string{"john", "doe"})
|
err = withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runKeyRemove(ctx, gopts, []string{"john", "doe"}, gopts.Term)
|
||||||
|
})
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
|
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "one argument"), "unexpected error for key remove: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"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/progress"
|
||||||
"github.com/restic/restic/internal/ui/table"
|
"github.com/restic/restic/internal/ui/table"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newKeyListCommand() *cobra.Command {
|
func newKeyListCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List keys (passwords)",
|
Short: "List keys (passwords)",
|
||||||
@@ -32,27 +35,28 @@ Exit status is 12 if the password is incorrect.
|
|||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runKeyList(cmd.Context(), globalOptions, args)
|
return runKeyList(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeyList(ctx context.Context, gopts GlobalOptions, args []string) error {
|
func runKeyList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags")
|
return fmt.Errorf("the key list command expects no arguments, only options - please see `restic help key list` for usage and flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
return listKeys(ctx, repo, gopts)
|
return listKeys(ctx, repo, gopts, printer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error {
|
func listKeys(ctx context.Context, s *repository.Repository, gopts global.Options, printer progress.Printer) error {
|
||||||
type keyInfo struct {
|
type keyInfo struct {
|
||||||
Current bool `json:"current"`
|
Current bool `json:"current"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -68,7 +72,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
|||||||
err := restic.ParallelList(ctx, s, restic.KeyFile, s.Connections(), func(ctx context.Context, id restic.ID, _ int64) error {
|
err := restic.ParallelList(ctx, s, restic.KeyFile, s.Connections(), func(ctx context.Context, id restic.ID, _ int64) error {
|
||||||
k, err := repository.LoadKey(ctx, s, id)
|
k, err := repository.LoadKey(ctx, s, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("LoadKey() failed: %v\n", err)
|
printer.E("LoadKey() failed: %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
|||||||
ShortID: id.Str(),
|
ShortID: id.Str(),
|
||||||
UserName: k.Username,
|
UserName: k.Username,
|
||||||
HostName: k.Hostname,
|
HostName: k.Hostname,
|
||||||
Created: k.Created.Local().Format(TimeFormat),
|
Created: k.Created.Local().Format(global.TimeFormat),
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Lock()
|
m.Lock()
|
||||||
@@ -92,7 +96,7 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
return json.NewEncoder(globalOptions.stdout).Encode(keys)
|
return json.NewEncoder(gopts.Term.OutputWriter()).Encode(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
tab := table.New()
|
tab := table.New()
|
||||||
@@ -105,5 +109,5 @@ func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions
|
|||||||
tab.AddRow(key)
|
tab.AddRow(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tab.Write(globalOptions.stdout)
|
return tab.Write(gopts.Term.OutputWriter())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newKeyPasswdCommand() *cobra.Command {
|
func newKeyPasswdCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts KeyPasswdOptions
|
var opts KeyPasswdOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -31,7 +34,7 @@ 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 runKeyPasswd(cmd.Context(), globalOptions, opts, args)
|
return runKeyPasswd(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,21 +50,22 @@ func (opts *KeyPasswdOptions) AddFlags(flags *pflag.FlagSet) {
|
|||||||
opts.KeyAddOptions.Add(flags)
|
opts.KeyAddOptions.Add(flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeyPasswd(ctx context.Context, gopts GlobalOptions, opts KeyPasswdOptions, args []string) error {
|
func runKeyPasswd(ctx context.Context, gopts global.Options, opts KeyPasswdOptions, args []string, term ui.Terminal) error {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags")
|
return fmt.Errorf("the key passwd command expects no arguments, only options - please see `restic help key passwd` for usage and flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
return changePassword(ctx, repo, gopts, opts)
|
return changePassword(ctx, repo, gopts, opts, printer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func changePassword(ctx context.Context, repo *repository.Repository, gopts GlobalOptions, opts KeyPasswdOptions) error {
|
func changePassword(ctx context.Context, repo *repository.Repository, gopts global.Options, opts KeyPasswdOptions, printer progress.Printer) error {
|
||||||
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
pw, err := getNewPassword(ctx, gopts, opts.NewPasswordFile, opts.InsecureNoPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -69,7 +73,7 @@ func changePassword(ctx context.Context, repo *repository.Repository, gopts Glob
|
|||||||
|
|
||||||
id, err := repository.AddKey(ctx, repo, pw, "", "", repo.Key())
|
id, err := repository.AddKey(ctx, repo, pw, "", "", repo.Key())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("creating new key failed: %v\n", err)
|
return errors.Fatalf("creating new key failed: %v", err)
|
||||||
}
|
}
|
||||||
oldID := repo.KeyID()
|
oldID := repo.KeyID()
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ func changePassword(ctx context.Context, repo *repository.Repository, gopts Glob
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("saved new key as %s\n", id)
|
printer.P("saved new key as %s", id)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newKeyRemoveCommand() *cobra.Command {
|
func newKeyRemoveCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "remove [ID]",
|
Use: "remove [ID]",
|
||||||
Short: "Remove key ID (password) from the repository.",
|
Short: "Remove key ID (password) from the repository.",
|
||||||
@@ -29,27 +32,28 @@ 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 runKeyRemove(cmd.Context(), globalOptions, args)
|
return runKeyRemove(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeyRemove(ctx context.Context, gopts GlobalOptions, args []string) error {
|
func runKeyRemove(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return fmt.Errorf("key remove expects one argument as the key id")
|
return fmt.Errorf("key remove expects one argument as the key id")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
return deleteKey(ctx, repo, args[0])
|
return deleteKey(ctx, repo, args[0], printer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string) error {
|
func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string, printer progress.Printer) error {
|
||||||
id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix)
|
id, err := restic.Find(ctx, repo, restic.KeyFile, idPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -64,6 +68,6 @@ func deleteKey(ctx context.Context, repo *repository.Repository, idPrefix string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("removed key %v\n", id)
|
printer.P("removed key %v", id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository/index"
|
"github.com/restic/restic/internal/repository/index"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newListCommand() *cobra.Command {
|
func newListCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var listAllowedArgs = []string{"blobs", "packs", "index", "snapshots", "keys", "locks"}
|
var listAllowedArgs = []string{"blobs", "packs", "index", "snapshots", "keys", "locks"}
|
||||||
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
|
var listAllowedArgsUseString = strings.Join(listAllowedArgs, "|")
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runList(cmd.Context(), globalOptions, args)
|
return runList(cmd.Context(), *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
ValidArgs: listAllowedArgs,
|
ValidArgs: listAllowedArgs,
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||||
@@ -41,12 +43,14 @@ Exit status is 12 if the password is incorrect.
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runList(ctx context.Context, gopts GlobalOptions, args []string) error {
|
func runList(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
if len(args) != 1 {
|
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 || args[0] == "locks")
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock || args[0] == "locks", printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -69,16 +73,20 @@ func runList(ctx context.Context, gopts GlobalOptions, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return idx.Each(ctx, func(blobs restic.PackedBlob) {
|
for blobs := range idx.Values() {
|
||||||
Printf("%v %v\n", blobs.Type, blobs.ID)
|
if ctx.Err() != nil {
|
||||||
})
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
printer.S("%v %v", blobs.Type, blobs.ID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
return errors.Fatal("invalid type")
|
return errors.Fatal("invalid type")
|
||||||
}
|
}
|
||||||
|
|
||||||
return repo.List(ctx, t, func(id restic.ID, _ int64) error {
|
return repo.List(ctx, t, func(id restic.ID, _ int64) error {
|
||||||
Printf("%s\n", id)
|
printer.S("%s", id)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs {
|
func testRunList(t testing.TB, gopts global.Options, tpe string) restic.IDs {
|
||||||
buf, err := withCaptureStdout(func() error {
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return runList(context.TODO(), opts, []string{tpe})
|
return runList(ctx, gopts, []string{tpe}, gopts.Term)
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
return parseIDsFromReader(t, buf)
|
return parseIDsFromReader(t, buf)
|
||||||
@@ -24,21 +28,77 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs {
|
|||||||
sc := bufio.NewScanner(rd)
|
sc := bufio.NewScanner(rd)
|
||||||
|
|
||||||
for sc.Scan() {
|
for sc.Scan() {
|
||||||
|
if len(sc.Text()) == 64 {
|
||||||
id, err := restic.ParseID(sc.Text())
|
id, err := restic.ParseID(sc.Text())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("parse id %v: %v", sc.Text(), err)
|
t.Logf("parse id %v: %v", sc.Text(), err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
IDs = append(IDs, id)
|
IDs = append(IDs, id)
|
||||||
|
} else {
|
||||||
|
// 'list blobs' is different because it lists the blobs together with the blob type
|
||||||
|
// e.g. "tree ac08ce34ba4f8123618661bef2425f7028ffb9ac740578a3ee88684d2523fee8"
|
||||||
|
parts := strings.Split(sc.Text(), " ")
|
||||||
|
id, err := restic.ParseID(parts[len(parts)-1])
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("parse id %v: %v", sc.Text(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
IDs = append(IDs, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return IDs
|
return IDs
|
||||||
}
|
}
|
||||||
|
|
||||||
func testListSnapshots(t testing.TB, opts GlobalOptions, expected int) restic.IDs {
|
func testListSnapshots(t testing.TB, gopts global.Options, expected int) restic.IDs {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
snapshotIDs := testRunList(t, "snapshots", opts)
|
snapshotIDs := testRunList(t, gopts, "snapshots")
|
||||||
rtest.Assert(t, len(snapshotIDs) == expected, "expected %v snapshot, got %v", expected, snapshotIDs)
|
rtest.Assert(t, len(snapshotIDs) == expected, "expected %v snapshot, got %v", expected, snapshotIDs)
|
||||||
return snapshotIDs
|
return snapshotIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extract blob set from repository index
|
||||||
|
func testListBlobs(t testing.TB, gopts global.Options) (blobSetFromIndex restic.IDSet) {
|
||||||
|
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||||
|
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
// make sure the index is loaded
|
||||||
|
rtest.OK(t, repo.LoadIndex(ctx, nil))
|
||||||
|
|
||||||
|
// get blobs from index
|
||||||
|
blobSetFromIndex = restic.NewIDSet()
|
||||||
|
rtest.OK(t, repo.ListBlobs(ctx, func(blob restic.PackedBlob) {
|
||||||
|
blobSetFromIndex.Insert(blob.ID)
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
|
||||||
|
return blobSetFromIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListBlobs(t *testing.T) {
|
||||||
|
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
testSetupBackupData(t, env)
|
||||||
|
opts := BackupOptions{}
|
||||||
|
|
||||||
|
// first backup
|
||||||
|
testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts)
|
||||||
|
testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
|
// run the `list blobs` command
|
||||||
|
resticIDs := testRunList(t, env.gopts, "blobs")
|
||||||
|
|
||||||
|
// convert to set
|
||||||
|
testIDSet := restic.NewIDSet(resticIDs...)
|
||||||
|
blobSetFromIndex := testListBlobs(t, env.gopts)
|
||||||
|
|
||||||
|
rtest.Assert(t, blobSetFromIndex.Equals(testIDSet), "the set of restic.ID s should be equal")
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,13 +15,16 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"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/fs"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newLsCommand() *cobra.Command {
|
func newLsCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts LsOptions
|
var opts LsOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -59,7 +62,8 @@ Exit status is 12 if the password is incorrect.
|
|||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runLs(cmd.Context(), opts, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
return runLs(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
opts.AddFlags(cmd.Flags())
|
opts.AddFlags(cmd.Flags())
|
||||||
@@ -69,7 +73,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
// LsOptions collects all options for the ls command.
|
// LsOptions collects all options for the ls command.
|
||||||
type LsOptions struct {
|
type LsOptions struct {
|
||||||
ListLong bool
|
ListLong bool
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
Recursive bool
|
Recursive bool
|
||||||
HumanReadable bool
|
HumanReadable bool
|
||||||
Ncdu bool
|
Ncdu bool
|
||||||
@@ -88,8 +92,8 @@ func (opts *LsOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type lsPrinter interface {
|
type lsPrinter interface {
|
||||||
Snapshot(sn *restic.Snapshot) error
|
Snapshot(sn *data.Snapshot) error
|
||||||
Node(path string, node *restic.Node, isPrefixDirectory bool) error
|
Node(path string, node *data.Node, isPrefixDirectory bool) error
|
||||||
LeaveDir(path string) error
|
LeaveDir(path string) error
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
@@ -98,9 +102,9 @@ type jsonLsPrinter struct {
|
|||||||
enc *json.Encoder
|
enc *json.Encoder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *jsonLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
func (p *jsonLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||||
type lsSnapshot struct {
|
type lsSnapshot struct {
|
||||||
*restic.Snapshot
|
*data.Snapshot
|
||||||
ID *restic.ID `json:"id"`
|
ID *restic.ID `json:"id"`
|
||||||
ShortID string `json:"short_id"` // deprecated
|
ShortID string `json:"short_id"` // deprecated
|
||||||
MessageType string `json:"message_type"` // "snapshot"
|
MessageType string `json:"message_type"` // "snapshot"
|
||||||
@@ -117,14 +121,14 @@ func (p *jsonLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Node formats node in our custom JSON format, followed by a newline.
|
// Node formats node in our custom JSON format, followed by a newline.
|
||||||
func (p *jsonLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error {
|
func (p *jsonLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||||
if isPrefixDirectory {
|
if isPrefixDirectory {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return lsNodeJSON(p.enc, path, node)
|
return lsNodeJSON(p.enc, path, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
func lsNodeJSON(enc *json.Encoder, path string, node *restic.Node) error {
|
func lsNodeJSON(enc *json.Encoder, path string, node *data.Node) error {
|
||||||
n := &struct {
|
n := &struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -160,7 +164,7 @@ func lsNodeJSON(enc *json.Encoder, path string, node *restic.Node) error {
|
|||||||
}
|
}
|
||||||
// Always print size for regular files, even when empty,
|
// Always print size for regular files, even when empty,
|
||||||
// but never for other types.
|
// but never for other types.
|
||||||
if node.Type == restic.NodeTypeFile {
|
if node.Type == data.NodeTypeFile {
|
||||||
n.Size = &n.size
|
n.Size = &n.size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +182,7 @@ type ncduLsPrinter struct {
|
|||||||
// Snapshot prints a restic snapshot in Ncdu save format.
|
// Snapshot prints a restic snapshot in Ncdu save format.
|
||||||
// It opens the JSON list. Nodes are added with lsNodeNcdu and the list is closed by lsCloseNcdu.
|
// It opens the JSON list. Nodes are added with lsNodeNcdu and the list is closed by lsCloseNcdu.
|
||||||
// Format documentation: https://dev.yorhel.nl/ncdu/jsonfmt
|
// Format documentation: https://dev.yorhel.nl/ncdu/jsonfmt
|
||||||
func (p *ncduLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
func (p *ncduLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||||
const NcduMajorVer = 1
|
const NcduMajorVer = 1
|
||||||
const NcduMinorVer = 2
|
const NcduMinorVer = 2
|
||||||
|
|
||||||
@@ -191,7 +195,7 @@ func (p *ncduLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
|
func lsNcduNode(_ string, node *data.Node) ([]byte, error) {
|
||||||
type NcduNode struct {
|
type NcduNode struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Asize uint64 `json:"asize"`
|
Asize uint64 `json:"asize"`
|
||||||
@@ -216,7 +220,7 @@ func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
|
|||||||
Dev: node.DeviceID,
|
Dev: node.DeviceID,
|
||||||
Ino: node.Inode,
|
Ino: node.Inode,
|
||||||
NLink: node.Links,
|
NLink: node.Links,
|
||||||
NotReg: node.Type != restic.NodeTypeDir && node.Type != restic.NodeTypeFile,
|
NotReg: node.Type != data.NodeTypeDir && node.Type != data.NodeTypeFile,
|
||||||
UID: node.UID,
|
UID: node.UID,
|
||||||
GID: node.GID,
|
GID: node.GID,
|
||||||
Mode: uint16(node.Mode & os.ModePerm),
|
Mode: uint16(node.Mode & os.ModePerm),
|
||||||
@@ -240,13 +244,13 @@ func lsNcduNode(_ string, node *restic.Node) ([]byte, error) {
|
|||||||
return json.Marshal(outNode)
|
return json.Marshal(outNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ncduLsPrinter) Node(path string, node *restic.Node, _ bool) error {
|
func (p *ncduLsPrinter) Node(path string, node *data.Node, _ bool) error {
|
||||||
out, err := lsNcduNode(path, node)
|
out, err := lsNcduNode(path, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Type == restic.NodeTypeDir {
|
if node.Type == data.NodeTypeDir {
|
||||||
_, err = fmt.Fprintf(p.out, ",\n%s[\n%s%s", strings.Repeat(" ", p.depth), strings.Repeat(" ", p.depth+1), string(out))
|
_, err = fmt.Fprintf(p.out, ",\n%s[\n%s%s", strings.Repeat(" ", p.depth), strings.Repeat(" ", p.depth+1), string(out))
|
||||||
p.depth++
|
p.depth++
|
||||||
} else {
|
} else {
|
||||||
@@ -270,15 +274,19 @@ type textLsPrinter struct {
|
|||||||
dirs []string
|
dirs []string
|
||||||
ListLong bool
|
ListLong bool
|
||||||
HumanReadable bool
|
HumanReadable bool
|
||||||
|
termPrinter interface {
|
||||||
|
P(msg string, args ...interface{})
|
||||||
|
S(msg string, args ...interface{})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *textLsPrinter) Snapshot(sn *restic.Snapshot) error {
|
func (p *textLsPrinter) Snapshot(sn *data.Snapshot) error {
|
||||||
Verbosef("%v filtered by %v:\n", sn, p.dirs)
|
p.termPrinter.P("%v filtered by %v:", sn, p.dirs)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (p *textLsPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error {
|
func (p *textLsPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||||
if !isPrefixDirectory {
|
if !isPrefixDirectory {
|
||||||
Printf("%s\n", formatNode(path, node, p.ListLong, p.HumanReadable))
|
p.termPrinter.S("%s", formatNode(path, node, p.ListLong, p.HumanReadable))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -293,10 +301,12 @@ func (p *textLsPrinter) Close() error {
|
|||||||
// for ls -l output sorting
|
// for ls -l output sorting
|
||||||
type toSortOutput struct {
|
type toSortOutput struct {
|
||||||
nodepath string
|
nodepath string
|
||||||
node *restic.Node
|
node *data.Node
|
||||||
}
|
}
|
||||||
|
|
||||||
func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []string) error {
|
func runLs(ctx context.Context, opts LsOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
|
termPrinter := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, term)
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
|
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
|
||||||
}
|
}
|
||||||
@@ -355,7 +365,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock)
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, gopts.NoLock, termPrinter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -366,8 +376,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bar := newIndexProgress(gopts.Quiet, gopts.JSON)
|
if err = repo.LoadIndex(ctx, termPrinter); err != nil {
|
||||||
if err = repo.LoadIndex(ctx, bar); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,17 +384,18 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
|||||||
|
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
printer = &jsonLsPrinter{
|
printer = &jsonLsPrinter{
|
||||||
enc: json.NewEncoder(globalOptions.stdout),
|
enc: json.NewEncoder(gopts.Term.OutputWriter()),
|
||||||
}
|
}
|
||||||
} else if opts.Ncdu {
|
} else if opts.Ncdu {
|
||||||
printer = &ncduLsPrinter{
|
printer = &ncduLsPrinter{
|
||||||
out: globalOptions.stdout,
|
out: gopts.Term.OutputWriter(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
printer = &textLsPrinter{
|
printer = &textLsPrinter{
|
||||||
dirs: dirs,
|
dirs: dirs,
|
||||||
ListLong: opts.ListLong,
|
ListLong: opts.ListLong,
|
||||||
HumanReadable: opts.HumanReadable,
|
HumanReadable: opts.HumanReadable,
|
||||||
|
termPrinter: termPrinter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opts.Sort != SortModeName || opts.Reverse {
|
if opts.Sort != SortModeName || opts.Reverse {
|
||||||
@@ -396,16 +406,12 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sn, subfolder, err := (&restic.SnapshotFilter{
|
sn, subfolder, err := opts.SnapshotFilter.FindLatest(ctx, snapshotLister, repo, args[0])
|
||||||
Hosts: opts.Hosts,
|
|
||||||
Paths: opts.Paths,
|
|
||||||
Tags: opts.Tags,
|
|
||||||
}).FindLatest(ctx, snapshotLister, repo, args[0])
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -414,7 +420,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
processNode := func(_ restic.ID, nodepath string, node *restic.Node, err error) error {
|
processNode := func(_ restic.ID, nodepath string, node *data.Node, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -449,7 +455,7 @@ func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []stri
|
|||||||
|
|
||||||
// otherwise, signal the walker to not walk recursively into any
|
// otherwise, signal the walker to not walk recursively into any
|
||||||
// subdirs
|
// subdirs
|
||||||
if node.Type == restic.NodeTypeDir {
|
if node.Type == data.NodeTypeDir {
|
||||||
// immediately generate leaveDir if the directory is skipped
|
// immediately generate leaveDir if the directory is skipped
|
||||||
if printedDir {
|
if printedDir {
|
||||||
if err := printer.LeaveDir(nodepath); err != nil {
|
if err := printer.LeaveDir(nodepath); err != nil {
|
||||||
@@ -486,10 +492,10 @@ type sortedPrinter struct {
|
|||||||
reverse bool
|
reverse bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *sortedPrinter) Snapshot(sn *restic.Snapshot) error {
|
func (p *sortedPrinter) Snapshot(sn *data.Snapshot) error {
|
||||||
return p.printer.Snapshot(sn)
|
return p.printer.Snapshot(sn)
|
||||||
}
|
}
|
||||||
func (p *sortedPrinter) Node(path string, node *restic.Node, isPrefixDirectory bool) error {
|
func (p *sortedPrinter) Node(path string, node *data.Node, isPrefixDirectory bool) error {
|
||||||
if !isPrefixDirectory {
|
if !isPrefixDirectory {
|
||||||
p.collector = append(p.collector, toSortOutput{path, node})
|
p.collector = append(p.collector, toSortOutput{path, node})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,22 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunLsWithOpts(t testing.TB, gopts GlobalOptions, opts LsOptions, args []string) []byte {
|
func testRunLsWithOpts(t testing.TB, gopts global.Options, opts LsOptions, args []string) []byte {
|
||||||
buf, err := withCaptureStdout(func() error {
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
gopts.Quiet = true
|
gopts.Quiet = true
|
||||||
return runLs(context.TODO(), opts, gopts, args)
|
return runLs(context.TODO(), opts, gopts, args, gopts.Term)
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunLs(t testing.TB, gopts GlobalOptions, snapshotID string) []string {
|
func testRunLs(t testing.TB, gopts global.Options, snapshotID string) []string {
|
||||||
out := testRunLsWithOpts(t, gopts, LsOptions{}, []string{snapshotID})
|
out := testRunLsWithOpts(t, gopts, LsOptions{}, []string{snapshotID})
|
||||||
return strings.Split(string(out), "\n")
|
return strings.Split(string(out), "\n")
|
||||||
}
|
}
|
||||||
@@ -129,7 +131,7 @@ func TestRunLsJson(t *testing.T) {
|
|||||||
|
|
||||||
// partial copy of snapshot structure from cmd_ls
|
// partial copy of snapshot structure from cmd_ls
|
||||||
type lsSnapshot struct {
|
type lsSnapshot struct {
|
||||||
*restic.Snapshot
|
*data.Snapshot
|
||||||
ID *restic.ID `json:"id"`
|
ID *restic.ID `json:"id"`
|
||||||
ShortID string `json:"short_id"` // deprecated
|
ShortID string `json:"short_id"` // deprecated
|
||||||
MessageType string `json:"message_type"` // "snapshot"
|
MessageType string `json:"message_type"` // "snapshot"
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/data"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
type lsTestNode struct {
|
type lsTestNode struct {
|
||||||
path string
|
path string
|
||||||
restic.Node
|
data.Node
|
||||||
}
|
}
|
||||||
|
|
||||||
var lsTestNodes = []lsTestNode{
|
var lsTestNodes = []lsTestNode{
|
||||||
@@ -21,9 +21,9 @@ var lsTestNodes = []lsTestNode{
|
|||||||
// Permissions, by convention is "-" per mode bit
|
// Permissions, by convention is "-" per mode bit
|
||||||
{
|
{
|
||||||
path: "/bar/baz",
|
path: "/bar/baz",
|
||||||
Node: restic.Node{
|
Node: data.Node{
|
||||||
Name: "baz",
|
Name: "baz",
|
||||||
Type: restic.NodeTypeFile,
|
Type: data.NodeTypeFile,
|
||||||
Size: 12345,
|
Size: 12345,
|
||||||
UID: 10000000,
|
UID: 10000000,
|
||||||
GID: 20000000,
|
GID: 20000000,
|
||||||
@@ -37,9 +37,9 @@ var lsTestNodes = []lsTestNode{
|
|||||||
// Even empty files get an explicit size.
|
// Even empty files get an explicit size.
|
||||||
{
|
{
|
||||||
path: "/foo/empty",
|
path: "/foo/empty",
|
||||||
Node: restic.Node{
|
Node: data.Node{
|
||||||
Name: "empty",
|
Name: "empty",
|
||||||
Type: restic.NodeTypeFile,
|
Type: data.NodeTypeFile,
|
||||||
Size: 0,
|
Size: 0,
|
||||||
UID: 1001,
|
UID: 1001,
|
||||||
GID: 1001,
|
GID: 1001,
|
||||||
@@ -54,9 +54,9 @@ var lsTestNodes = []lsTestNode{
|
|||||||
// Mode is printed in decimal, including the type bits.
|
// Mode is printed in decimal, including the type bits.
|
||||||
{
|
{
|
||||||
path: "/foo/link",
|
path: "/foo/link",
|
||||||
Node: restic.Node{
|
Node: data.Node{
|
||||||
Name: "link",
|
Name: "link",
|
||||||
Type: restic.NodeTypeSymlink,
|
Type: data.NodeTypeSymlink,
|
||||||
Mode: os.ModeSymlink | 0777,
|
Mode: os.ModeSymlink | 0777,
|
||||||
LinkTarget: "not printed",
|
LinkTarget: "not printed",
|
||||||
},
|
},
|
||||||
@@ -64,9 +64,9 @@ var lsTestNodes = []lsTestNode{
|
|||||||
|
|
||||||
{
|
{
|
||||||
path: "/some/directory",
|
path: "/some/directory",
|
||||||
Node: restic.Node{
|
Node: data.Node{
|
||||||
Name: "directory",
|
Name: "directory",
|
||||||
Type: restic.NodeTypeDir,
|
Type: data.NodeTypeDir,
|
||||||
Mode: os.ModeDir | 0755,
|
Mode: os.ModeDir | 0755,
|
||||||
ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC),
|
ModTime: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC),
|
||||||
AccessTime: time.Date(2021, 2, 3, 4, 5, 6, 7, time.UTC),
|
AccessTime: time.Date(2021, 2, 3, 4, 5, 6, 7, time.UTC),
|
||||||
@@ -77,9 +77,9 @@ var lsTestNodes = []lsTestNode{
|
|||||||
// Test encoding of setuid/setgid/sticky bit
|
// Test encoding of setuid/setgid/sticky bit
|
||||||
{
|
{
|
||||||
path: "/some/sticky",
|
path: "/some/sticky",
|
||||||
Node: restic.Node{
|
Node: data.Node{
|
||||||
Name: "sticky",
|
Name: "sticky",
|
||||||
Type: restic.NodeTypeDir,
|
Type: data.NodeTypeDir,
|
||||||
Mode: os.ModeDir | 0755 | os.ModeSetuid | os.ModeSetgid | os.ModeSticky,
|
Mode: os.ModeDir | 0755 | os.ModeSetuid | os.ModeSetgid | os.ModeSticky,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -134,24 +134,24 @@ func TestLsNcdu(t *testing.T) {
|
|||||||
}
|
}
|
||||||
modTime := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
|
modTime := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||||
|
|
||||||
rtest.OK(t, printer.Snapshot(&restic.Snapshot{
|
rtest.OK(t, printer.Snapshot(&data.Snapshot{
|
||||||
Hostname: "host",
|
Hostname: "host",
|
||||||
Paths: []string{"/example"},
|
Paths: []string{"/example"},
|
||||||
}))
|
}))
|
||||||
rtest.OK(t, printer.Node("/directory", &restic.Node{
|
rtest.OK(t, printer.Node("/directory", &data.Node{
|
||||||
Type: restic.NodeTypeDir,
|
Type: data.NodeTypeDir,
|
||||||
Name: "directory",
|
Name: "directory",
|
||||||
ModTime: modTime,
|
ModTime: modTime,
|
||||||
}, false))
|
}, false))
|
||||||
rtest.OK(t, printer.Node("/directory/data", &restic.Node{
|
rtest.OK(t, printer.Node("/directory/data", &data.Node{
|
||||||
Type: restic.NodeTypeFile,
|
Type: data.NodeTypeFile,
|
||||||
Name: "data",
|
Name: "data",
|
||||||
Size: 42,
|
Size: 42,
|
||||||
ModTime: modTime,
|
ModTime: modTime,
|
||||||
}, false))
|
}, false))
|
||||||
rtest.OK(t, printer.LeaveDir("/directory"))
|
rtest.OK(t, printer.LeaveDir("/directory"))
|
||||||
rtest.OK(t, printer.Node("/file", &restic.Node{
|
rtest.OK(t, printer.Node("/file", &data.Node{
|
||||||
Type: restic.NodeTypeFile,
|
Type: data.NodeTypeFile,
|
||||||
Name: "file",
|
Name: "file",
|
||||||
Size: 12345,
|
Size: 12345,
|
||||||
ModTime: modTime,
|
ModTime: modTime,
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/migrations"
|
"github.com/restic/restic/internal/migrations"
|
||||||
"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"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newMigrateCommand() *cobra.Command {
|
func newMigrateCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts MigrateOptions
|
var opts MigrateOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -35,9 +36,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
term, cancel := setupTermstatus()
|
return runMigrate(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
defer cancel()
|
|
||||||
return runMigrate(cmd.Context(), opts, globalOptions, args, term)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ func checkMigrations(ctx context.Context, repo restic.Repository, printer progre
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyMigrations(ctx context.Context, opts MigrateOptions, gopts GlobalOptions, repo restic.Repository, args []string, term *termstatus.Terminal, printer progress.Printer) error {
|
func applyMigrations(ctx context.Context, opts MigrateOptions, gopts global.Options, repo restic.Repository, args []string, term ui.Terminal, printer progress.Printer) error {
|
||||||
var firsterr error
|
var firsterr error
|
||||||
for _, name := range args {
|
for _, name := range args {
|
||||||
found := false
|
found := false
|
||||||
@@ -135,10 +134,10 @@ func applyMigrations(ctx context.Context, opts MigrateOptions, gopts GlobalOptio
|
|||||||
return firsterr
|
return firsterr
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMigrate(ctx context.Context, opts MigrateOptions, gopts GlobalOptions, args []string, term *termstatus.Terminal) error {
|
func runMigrate(ctx context.Context, opts MigrateOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
printer := newTerminalProgressPrinter(gopts.verbosity, term)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
//go:build darwin || freebsd || linux
|
//go:build darwin || freebsd || linux
|
||||||
// +build darwin freebsd linux
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
"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/restic"
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/fuse"
|
"github.com/restic/restic/internal/fuse"
|
||||||
|
|
||||||
@@ -22,19 +25,19 @@ import (
|
|||||||
"github.com/anacrolix/fuse/fs"
|
"github.com/anacrolix/fuse/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerMountCommand(cmdRoot *cobra.Command) {
|
func registerMountCommand(cmdRoot *cobra.Command, globalOptions *global.Options) {
|
||||||
cmdRoot.AddCommand(newMountCommand())
|
cmdRoot.AddCommand(newMountCommand(globalOptions))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMountCommand() *cobra.Command {
|
func newMountCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts MountOptions
|
var opts MountOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "mount [flags] mountpoint",
|
Use: "mount [flags] mountpoint",
|
||||||
Short: "Mount the repository",
|
Short: "Mount the repository",
|
||||||
Long: `
|
Long: `
|
||||||
The "mount" command mounts the repository via fuse to a directory. This is a
|
The "mount" command mounts the repository via fuse over a writeable directory.
|
||||||
read-only mount.
|
The repository will be mounted read-only.
|
||||||
|
|
||||||
Snapshot Directories
|
Snapshot Directories
|
||||||
====================
|
====================
|
||||||
@@ -80,7 +83,8 @@ Exit status is 12 if the password is incorrect.
|
|||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runMount(cmd.Context(), opts, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
return runMount(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +97,7 @@ type MountOptions struct {
|
|||||||
OwnerRoot bool
|
OwnerRoot bool
|
||||||
AllowOther bool
|
AllowOther bool
|
||||||
NoDefaultPermissions bool
|
NoDefaultPermissions bool
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
TimeTemplate string
|
TimeTemplate string
|
||||||
PathTemplates []string
|
PathTemplates []string
|
||||||
}
|
}
|
||||||
@@ -111,7 +115,9 @@ func (opts *MountOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
_ = f.MarkDeprecated("snapshot-template", "use --time-template")
|
_ = f.MarkDeprecated("snapshot-template", "use --time-template")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args []string) error {
|
func runMount(ctx context.Context, opts MountOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
if opts.TimeTemplate == "" {
|
if opts.TimeTemplate == "" {
|
||||||
return errors.Fatal("time template string cannot be empty")
|
return errors.Fatal("time template string cannot be empty")
|
||||||
}
|
}
|
||||||
@@ -128,29 +134,40 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args
|
|||||||
|
|
||||||
// Check the existence of the mount point at the earliest stage to
|
// Check the existence of the mount point at the earliest stage to
|
||||||
// prevent unnecessary computations while opening the repository.
|
// prevent unnecessary computations while opening the repository.
|
||||||
if _, err := os.Stat(mountpoint); errors.Is(err, os.ErrNotExist) {
|
stat, err := os.Stat(mountpoint)
|
||||||
Verbosef("Mountpoint %s doesn't exist\n", mountpoint)
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
return err
|
printer.P("Mountpoint %s doesn't exist", mountpoint)
|
||||||
|
return errors.Fatal("invalid mountpoint")
|
||||||
|
} else if !stat.IsDir() {
|
||||||
|
printer.P("Mountpoint %s is not a directory", mountpoint)
|
||||||
|
return errors.Fatal("invalid mountpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = unix.Access(mountpoint, unix.W_OK|unix.X_OK)
|
||||||
|
if err != nil {
|
||||||
|
printer.P("Mountpoint %s is not writeable or not excutable", mountpoint)
|
||||||
|
return errors.Fatal("inaccessible mountpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.Log("start mount")
|
debug.Log("start mount")
|
||||||
defer debug.Log("finish mount")
|
defer debug.Log("finish mount")
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fuseMountName := fmt.Sprintf("restic:%s", repo.Config().ID[:10])
|
||||||
|
|
||||||
mountOptions := []systemFuse.MountOption{
|
mountOptions := []systemFuse.MountOption{
|
||||||
systemFuse.ReadOnly(),
|
systemFuse.ReadOnly(),
|
||||||
systemFuse.FSName("restic"),
|
systemFuse.FSName(fuseMountName),
|
||||||
systemFuse.MaxReadahead(128 * 1024),
|
systemFuse.MaxReadahead(128 * 1024),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,9 +197,9 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args
|
|||||||
}
|
}
|
||||||
root := fuse.NewRoot(repo, cfg)
|
root := fuse.NewRoot(repo, cfg)
|
||||||
|
|
||||||
Printf("Now serving the repository at %s\n", mountpoint)
|
printer.S("Now serving the repository at %s", mountpoint)
|
||||||
Printf("Use another terminal or tool to browse the contents of this folder.\n")
|
printer.S("Use another terminal or tool to browse the contents of this folder.")
|
||||||
Printf("When finished, quit with Ctrl-c here or umount the mountpoint.\n")
|
printer.S("When finished, quit with Ctrl-c here or umount the mountpoint.")
|
||||||
|
|
||||||
debug.Log("serving mount at %v", mountpoint)
|
debug.Log("serving mount at %v", mountpoint)
|
||||||
|
|
||||||
@@ -198,7 +215,7 @@ func runMount(ctx context.Context, opts MountOptions, gopts GlobalOptions, args
|
|||||||
debug.Log("running umount cleanup handler for mount at %v", mountpoint)
|
debug.Log("running umount cleanup handler for mount at %v", mountpoint)
|
||||||
err := systemFuse.Unmount(mountpoint)
|
err := systemFuse.Unmount(mountpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("unable to umount (maybe already umounted or still in use?): %v\n", err)
|
printer.E("unable to umount (maybe already umounted or still in use?): %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ErrOK
|
return ErrOK
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
//go:build !darwin && !freebsd && !linux
|
//go:build !darwin && !freebsd && !linux
|
||||||
// +build !darwin,!freebsd,!linux
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
import (
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
func registerMountCommand(_ *cobra.Command) {
|
func registerMountCommand(_ *cobra.Command, _ *global.Options) {
|
||||||
// Mount command not supported on these platforms
|
// Mount command not supported on these platforms
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build darwin || freebsd || linux
|
//go:build darwin || freebsd || linux
|
||||||
// +build darwin freebsd linux
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -13,9 +12,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
systemFuse "github.com/anacrolix/fuse"
|
systemFuse "github.com/anacrolix/fuse"
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -56,12 +58,14 @@ func waitForMount(t testing.TB, dir string) {
|
|||||||
t.Errorf("subdir %q of dir %s never appeared", mountTestSubdir, dir)
|
t.Errorf("subdir %q of dir %s never appeared", mountTestSubdir, dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunMount(t testing.TB, gopts GlobalOptions, dir string, wg *sync.WaitGroup) {
|
func testRunMount(t testing.TB, gopts global.Options, dir string, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
opts := MountOptions{
|
opts := MountOptions{
|
||||||
TimeTemplate: time.RFC3339,
|
TimeTemplate: time.RFC3339,
|
||||||
}
|
}
|
||||||
rtest.OK(t, runMount(context.TODO(), opts, gopts, []string{dir}))
|
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runMount(context.TODO(), opts, gopts, []string{dir}, gopts.Term)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunUmount(t testing.TB, dir string) {
|
func testRunUmount(t testing.TB, dir string) {
|
||||||
@@ -87,7 +91,7 @@ func listSnapshots(t testing.TB, dir string) []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkSnapshots(t testing.TB, gopts GlobalOptions, mountpoint string, snapshotIDs restic.IDs, expectedSnapshotsInFuseDir int) {
|
func checkSnapshots(t testing.TB, gopts global.Options, mountpoint string, snapshotIDs restic.IDs, expectedSnapshotsInFuseDir int) {
|
||||||
t.Logf("checking for %d snapshots: %v", len(snapshotIDs), snapshotIDs)
|
t.Logf("checking for %d snapshots: %v", len(snapshotIDs), snapshotIDs)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -125,12 +129,16 @@ func checkSnapshots(t testing.TB, gopts GlobalOptions, mountpoint string, snapsh
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, repo, unlock, err := openWithReadLock(context.TODO(), gopts, false)
|
err := withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
rtest.OK(t, err)
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||||
|
_, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
for _, id := range snapshotIDs {
|
for _, id := range snapshotIDs {
|
||||||
snapshot, err := restic.LoadSnapshot(context.TODO(), repo, id)
|
snapshot, err := data.LoadSnapshot(ctx, repo, id)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
ts := snapshot.Time.Format(time.RFC3339)
|
ts := snapshot.Time.Format(time.RFC3339)
|
||||||
@@ -153,6 +161,9 @@ func checkSnapshots(t testing.TB, gopts GlobalOptions, mountpoint string, snapsh
|
|||||||
|
|
||||||
namesMap[ts] = true
|
namesMap[ts] = true
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
|
||||||
for name, present := range namesMap {
|
for name, present := range namesMap {
|
||||||
rtest.Assert(t, present, "Directory %s is present in fuse dir but is not a snapshot", name)
|
rtest.Assert(t, present, "Directory %s is present in fuse dir but is not a snapshot", name)
|
||||||
@@ -166,7 +177,7 @@ func TestMount(t *testing.T) {
|
|||||||
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
// must list snapshots more than once
|
// must list snapshots more than once
|
||||||
env.gopts.backendTestHook = nil
|
env.gopts.BackendTestHook = nil
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testRunInit(t, env.gopts)
|
testRunInit(t, env.gopts)
|
||||||
@@ -177,7 +188,7 @@ func TestMount(t *testing.T) {
|
|||||||
|
|
||||||
// first backup
|
// first backup
|
||||||
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
||||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
snapshotIDs := testRunList(t, env.gopts, "snapshots")
|
||||||
rtest.Assert(t, len(snapshotIDs) == 1,
|
rtest.Assert(t, len(snapshotIDs) == 1,
|
||||||
"expected one snapshot, got %v", snapshotIDs)
|
"expected one snapshot, got %v", snapshotIDs)
|
||||||
|
|
||||||
@@ -185,7 +196,7 @@ func TestMount(t *testing.T) {
|
|||||||
|
|
||||||
// second backup, implicit incremental
|
// second backup, implicit incremental
|
||||||
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
||||||
snapshotIDs = testRunList(t, "snapshots", env.gopts)
|
snapshotIDs = testRunList(t, env.gopts, "snapshots")
|
||||||
rtest.Assert(t, len(snapshotIDs) == 2,
|
rtest.Assert(t, len(snapshotIDs) == 2,
|
||||||
"expected two snapshots, got %v", snapshotIDs)
|
"expected two snapshots, got %v", snapshotIDs)
|
||||||
|
|
||||||
@@ -194,7 +205,7 @@ func TestMount(t *testing.T) {
|
|||||||
// third backup, explicit incremental
|
// third backup, explicit incremental
|
||||||
bopts := BackupOptions{Parent: snapshotIDs[0].String()}
|
bopts := BackupOptions{Parent: snapshotIDs[0].String()}
|
||||||
testRunBackup(t, "", []string{env.testdata}, bopts, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, bopts, env.gopts)
|
||||||
snapshotIDs = testRunList(t, "snapshots", env.gopts)
|
snapshotIDs = testRunList(t, env.gopts, "snapshots")
|
||||||
rtest.Assert(t, len(snapshotIDs) == 3,
|
rtest.Assert(t, len(snapshotIDs) == 3,
|
||||||
"expected three snapshots, got %v", snapshotIDs)
|
"expected three snapshots, got %v", snapshotIDs)
|
||||||
|
|
||||||
@@ -213,7 +224,7 @@ func TestMountSameTimestamps(t *testing.T) {
|
|||||||
|
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
// must list snapshots more than once
|
// must list snapshots more than once
|
||||||
env.gopts.backendTestHook = nil
|
env.gopts.BackendTestHook = nil
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
rtest.SetupTarTestFixture(t, env.base, filepath.Join("testdata", "repo-same-timestamps.tar.gz"))
|
rtest.SetupTarTestFixture(t, env.base, filepath.Join("testdata", "repo-same-timestamps.tar.gz"))
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/options"
|
"github.com/restic/restic/internal/options"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newOptionsCommand() *cobra.Command {
|
func newOptionsCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "options",
|
Use: "options",
|
||||||
Short: "Print list of extended options",
|
Short: "Print list of extended options",
|
||||||
@@ -24,7 +25,7 @@ Exit status is 1 if there was any error.
|
|||||||
GroupID: cmdGroupAdvanced,
|
GroupID: cmdGroupAdvanced,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
fmt.Printf("All Extended Options:\n")
|
globalOptions.Term.Print("All Extended Options:")
|
||||||
var maxLen int
|
var maxLen int
|
||||||
for _, opt := range options.List() {
|
for _, opt := range options.List() {
|
||||||
if l := len(opt.Namespace + "." + opt.Name); l > maxLen {
|
if l := len(opt.Namespace + "." + opt.Name); l > maxLen {
|
||||||
@@ -32,7 +33,7 @@ Exit status is 1 if there was any error.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, opt := range options.List() {
|
for _, opt := range options.List() {
|
||||||
fmt.Printf(" %*s %s\n", -maxLen, opt.Namespace+"."+opt.Name, opt.Text)
|
globalOptions.Term.Print(fmt.Sprintf(" %*s %s", -maxLen, opt.Namespace+"."+opt.Name, opt.Text))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,19 +7,20 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
||||||
"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"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newPruneCommand() *cobra.Command {
|
func newPruneCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts PruneOptions
|
var opts PruneOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -41,9 +42,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
term, cancel := setupTermstatus()
|
return runPrune(cmd.Context(), opts, *globalOptions, globalOptions.Term)
|
||||||
defer cancel()
|
|
||||||
return runPrune(cmd.Context(), opts, globalOptions, term)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +154,7 @@ func verifyPruneOptions(opts *PruneOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPrune(ctx context.Context, opts PruneOptions, gopts GlobalOptions, term *termstatus.Terminal) error {
|
func runPrune(ctx context.Context, opts PruneOptions, gopts global.Options, term ui.Terminal) error {
|
||||||
err := verifyPruneOptions(&opts)
|
err := verifyPruneOptions(&opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -169,7 +168,8 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts GlobalOptions, term
|
|||||||
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for prune command")
|
return errors.Fatal("--no-lock is only applicable in combination with --dry-run for prune command")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun && gopts.NoLock, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -183,20 +183,16 @@ func runPrune(ctx context.Context, opts PruneOptions, gopts GlobalOptions, term
|
|||||||
opts.unsafeRecovery = true
|
opts.unsafeRecovery = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return runPruneWithRepo(ctx, opts, gopts, repo, restic.NewIDSet(), term)
|
return runPruneWithRepo(ctx, opts, repo, restic.NewIDSet(), printer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPruneWithRepo(ctx context.Context, opts PruneOptions, gopts GlobalOptions, repo *repository.Repository, ignoreSnapshots restic.IDSet, term *termstatus.Terminal) error {
|
func runPruneWithRepo(ctx context.Context, opts PruneOptions, repo *repository.Repository, ignoreSnapshots restic.IDSet, printer progress.Printer) error {
|
||||||
if repo.Cache() == nil {
|
if repo.Cache() == nil {
|
||||||
Print("warning: running prune without a cache, this may be very slow!\n")
|
printer.S("warning: running prune without a cache, this may be very slow!")
|
||||||
}
|
}
|
||||||
|
|
||||||
printer := newTerminalProgressPrinter(gopts.verbosity, term)
|
|
||||||
|
|
||||||
printer.P("loading indexes...\n")
|
|
||||||
// loading the index before the snapshots is ok, as we use an exclusive lock here
|
// loading the index before the snapshots is ok, as we use an exclusive lock here
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -284,8 +280,8 @@ func printPruneStats(printer progress.Printer, stats repository.PruneStats) erro
|
|||||||
func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet, ignoreSnapshots restic.IDSet, printer progress.Printer) error {
|
func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.FindBlobSet, ignoreSnapshots restic.IDSet, printer progress.Printer) error {
|
||||||
var snapshotTrees restic.IDs
|
var snapshotTrees restic.IDs
|
||||||
printer.P("loading all snapshots...\n")
|
printer.P("loading all snapshots...\n")
|
||||||
err := restic.ForAllSnapshots(ctx, repo, repo, ignoreSnapshots,
|
err := data.ForAllSnapshots(ctx, repo, repo, ignoreSnapshots,
|
||||||
func(id restic.ID, sn *restic.Snapshot, err error) error {
|
func(id restic.ID, sn *data.Snapshot, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debug.Log("failed to load snapshot %v (error %v)", id, err)
|
debug.Log("failed to load snapshot %v (error %v)", id, err)
|
||||||
return err
|
return err
|
||||||
@@ -304,5 +300,10 @@ func getUsedBlobs(ctx context.Context, repo restic.Repository, usedBlobs restic.
|
|||||||
bar.SetMax(uint64(len(snapshotTrees)))
|
bar.SetMax(uint64(len(snapshotTrees)))
|
||||||
defer bar.Done()
|
defer bar.Done()
|
||||||
|
|
||||||
return restic.FindUsedBlobs(ctx, repo, snapshotTrees, usedBlobs, bar)
|
err = data.FindUsedBlobs(ctx, repo, snapshotTrees, usedBlobs, bar)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Fatalf("failed finding blobs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,30 +7,30 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/backend"
|
"github.com/restic/restic/internal/backend"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunPrune(t testing.TB, gopts GlobalOptions, opts PruneOptions) {
|
func testRunPrune(t testing.TB, gopts global.Options, opts PruneOptions) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
rtest.OK(t, testRunPruneOutput(gopts, opts))
|
rtest.OK(t, testRunPruneOutput(t, gopts, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunPruneMustFail(t testing.TB, gopts GlobalOptions, opts PruneOptions) {
|
func testRunPruneMustFail(t testing.TB, gopts global.Options, opts PruneOptions) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
err := testRunPruneOutput(gopts, opts)
|
err := testRunPruneOutput(t, gopts, opts)
|
||||||
rtest.Assert(t, err != nil, "expected non nil error")
|
rtest.Assert(t, err != nil, "expected non nil error")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunPruneOutput(gopts GlobalOptions, opts PruneOptions) error {
|
func testRunPruneOutput(t testing.TB, gopts global.Options, opts PruneOptions) error {
|
||||||
oldHook := gopts.backendTestHook
|
oldHook := gopts.BackendTestHook
|
||||||
gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) { return newListOnceBackend(r), nil }
|
gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) { return newListOnceBackend(r), nil }
|
||||||
defer func() {
|
defer func() {
|
||||||
gopts.backendTestHook = oldHook
|
gopts.BackendTestHook = oldHook
|
||||||
}()
|
}()
|
||||||
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
return withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return runPrune(context.TODO(), opts, gopts, term)
|
return runPrune(context.TODO(), opts, gopts, gopts.Term)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ func createPrunableRepo(t *testing.T, env *testEnvironment) {
|
|||||||
testRunForget(t, env.gopts, ForgetOptions{}, firstSnapshot.String())
|
testRunForget(t, env.gopts, ForgetOptions{}, firstSnapshot.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
|
func testRunForgetJSON(t testing.TB, gopts global.Options, args ...string) {
|
||||||
buf, err := withCaptureStdout(func() error {
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
gopts.JSON = true
|
gopts.JSON = true
|
||||||
opts := ForgetOptions{
|
opts := ForgetOptions{
|
||||||
DryRun: true,
|
DryRun: true,
|
||||||
@@ -99,9 +99,7 @@ func testRunForgetJSON(t testing.TB, gopts GlobalOptions, args ...string) {
|
|||||||
pruneOpts := PruneOptions{
|
pruneOpts := PruneOptions{
|
||||||
MaxUnused: "5%",
|
MaxUnused: "5%",
|
||||||
}
|
}
|
||||||
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
return runForget(context.TODO(), opts, pruneOpts, gopts, gopts.Term, args)
|
||||||
return runForget(context.TODO(), opts, pruneOpts, gopts, term, args)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
@@ -122,8 +120,8 @@ func testPrune(t *testing.T, pruneOpts PruneOptions, checkOpts CheckOptions) {
|
|||||||
|
|
||||||
createPrunableRepo(t, env)
|
createPrunableRepo(t, env)
|
||||||
testRunPrune(t, env.gopts, pruneOpts)
|
testRunPrune(t, env.gopts, pruneOpts)
|
||||||
rtest.OK(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
_, err := runCheck(context.TODO(), checkOpts, env.gopts, nil, term)
|
_, err := runCheck(context.TODO(), checkOpts, gopts, nil, gopts.Term)
|
||||||
return err
|
return err
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -152,14 +150,14 @@ func TestPruneWithDamagedRepository(t *testing.T) {
|
|||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
removePacksExcept(env.gopts, t, oldPacks, false)
|
removePacksExcept(env.gopts, t, oldPacks, false)
|
||||||
|
|
||||||
oldHook := env.gopts.backendTestHook
|
oldHook := env.gopts.BackendTestHook
|
||||||
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) { return newListOnceBackend(r), nil }
|
env.gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) { return newListOnceBackend(r), nil }
|
||||||
defer func() {
|
defer func() {
|
||||||
env.gopts.backendTestHook = oldHook
|
env.gopts.BackendTestHook = oldHook
|
||||||
}()
|
}()
|
||||||
// prune should fail
|
// prune should fail
|
||||||
rtest.Equals(t, repository.ErrPacksMissing, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
rtest.Equals(t, repository.ErrPacksMissing, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return runPrune(context.TODO(), pruneDefaultOptions, env.gopts, term)
|
return runPrune(context.TODO(), pruneDefaultOptions, gopts, gopts.Term)
|
||||||
}), "prune should have reported index not complete error")
|
}), "prune should have reported index not complete error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,8 +229,8 @@ func testEdgeCaseRepo(t *testing.T, tarfile string, optionsCheck CheckOptions, o
|
|||||||
if checkOK {
|
if checkOK {
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
} else {
|
} else {
|
||||||
rtest.Assert(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
rtest.Assert(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
_, err := runCheck(context.TODO(), optionsCheck, env.gopts, nil, term)
|
_, err := runCheck(context.TODO(), optionsCheck, gopts, nil, gopts.Term)
|
||||||
return err
|
return err
|
||||||
}) != nil,
|
}) != nil,
|
||||||
"check should have reported an error")
|
"check should have reported an error")
|
||||||
@@ -242,8 +240,8 @@ func testEdgeCaseRepo(t *testing.T, tarfile string, optionsCheck CheckOptions, o
|
|||||||
testRunPrune(t, env.gopts, optionsPrune)
|
testRunPrune(t, env.gopts, optionsPrune)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
} else {
|
} else {
|
||||||
rtest.Assert(t, withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
rtest.Assert(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return runPrune(context.TODO(), optionsPrune, env.gopts, term)
|
return runPrune(context.TODO(), optionsPrune, gopts, gopts.Term)
|
||||||
}) != nil,
|
}) != nil,
|
||||||
"prune should have reported an error")
|
"prune should have reported an error")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
"github.com/restic/restic/internal/ui/progress"
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRecoverCommand() *cobra.Command {
|
func newRecoverCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "recover [flags]",
|
Use: "recover [flags]",
|
||||||
Short: "Recover data from the repository not referenced by snapshots",
|
Short: "Recover data from the repository not referenced by snapshots",
|
||||||
@@ -35,28 +36,25 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
term, cancel := setupTermstatus()
|
return runRecover(cmd.Context(), *globalOptions, globalOptions.Term)
|
||||||
defer cancel()
|
|
||||||
return runRecover(cmd.Context(), globalOptions, term)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Terminal) error {
|
func runRecover(ctx context.Context, gopts global.Options, term ui.Terminal) error {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
printer := newTerminalProgressPrinter(gopts.verbosity, term)
|
|
||||||
|
|
||||||
snapshotLister, err := restic.MemorizeList(ctx, repo, restic.SnapshotFile)
|
snapshotLister, err := restic.MemorizeList(ctx, repo, restic.SnapshotFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -69,8 +67,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
|
|||||||
}
|
}
|
||||||
|
|
||||||
printer.P("load index files\n")
|
printer.P("load index files\n")
|
||||||
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
if err = repo.LoadIndex(ctx, printer); err != nil {
|
||||||
if err = repo.LoadIndex(ctx, bar); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,9 +85,10 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
|
|||||||
}
|
}
|
||||||
|
|
||||||
printer.P("load %d trees\n", len(trees))
|
printer.P("load %d trees\n", len(trees))
|
||||||
bar = newTerminalProgressMax(!gopts.Quiet, uint64(len(trees)), "trees loaded", term)
|
bar := printer.NewCounter("trees loaded")
|
||||||
|
bar.SetMax(uint64(len(trees)))
|
||||||
for id := range trees {
|
for id := range trees {
|
||||||
tree, err := restic.LoadTree(ctx, repo, id)
|
tree, err := data.LoadTree(ctx, repo, id)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -99,8 +97,12 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, node := range tree.Nodes {
|
for item := range tree {
|
||||||
if node.Type == restic.NodeTypeDir && node.Subtree != nil {
|
if item.Error != nil {
|
||||||
|
return item.Error
|
||||||
|
}
|
||||||
|
node := item.Node
|
||||||
|
if node.Type == data.NodeTypeDir && node.Subtree != nil {
|
||||||
trees[*node.Subtree] = true
|
trees[*node.Subtree] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +111,7 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
|
|||||||
bar.Done()
|
bar.Done()
|
||||||
|
|
||||||
printer.P("load snapshots\n")
|
printer.P("load snapshots\n")
|
||||||
err = restic.ForAllSnapshots(ctx, snapshotLister, repo, nil, func(_ restic.ID, sn *restic.Snapshot, _ error) error {
|
err = data.ForAllSnapshots(ctx, snapshotLister, repo, nil, func(_ restic.ID, sn *data.Snapshot, _ error) error {
|
||||||
trees[*sn.Tree] = true
|
trees[*sn.Tree] = true
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -136,11 +138,14 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
tree := restic.NewTree(len(roots))
|
var treeID restic.ID
|
||||||
|
err = repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||||
|
var err error
|
||||||
|
tw := data.NewTreeWriter(uploader)
|
||||||
for id := range roots {
|
for id := range roots {
|
||||||
var subtreeID = id
|
var subtreeID = id
|
||||||
node := restic.Node{
|
node := data.Node{
|
||||||
Type: restic.NodeTypeDir,
|
Type: data.NodeTypeDir,
|
||||||
Name: id.Str(),
|
Name: id.Str(),
|
||||||
Mode: 0755,
|
Mode: 0755,
|
||||||
Subtree: &subtreeID,
|
Subtree: &subtreeID,
|
||||||
@@ -148,30 +153,18 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
|
|||||||
ModTime: time.Now(),
|
ModTime: time.Now(),
|
||||||
ChangeTime: time.Now(),
|
ChangeTime: time.Now(),
|
||||||
}
|
}
|
||||||
err := tree.Insert(&node)
|
err := tw.AddNode(&node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wg, wgCtx := errgroup.WithContext(ctx)
|
treeID, err = tw.Finalize(ctx)
|
||||||
repo.StartPackUploader(wgCtx, wg)
|
|
||||||
|
|
||||||
var treeID restic.ID
|
|
||||||
wg.Go(func() error {
|
|
||||||
var err error
|
|
||||||
treeID, err = restic.SaveTree(wgCtx, repo, tree)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to save new tree to the repository: %v", err)
|
return errors.Fatalf("unable to save new tree to the repository: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = repo.Flush(wgCtx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Fatalf("unable to save blobs to the repository: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
err = wg.Wait()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -181,14 +174,14 @@ func runRecover(ctx context.Context, gopts GlobalOptions, term *termstatus.Termi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createSnapshot(ctx context.Context, printer progress.Printer, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error {
|
func createSnapshot(ctx context.Context, printer progress.Printer, name, hostname string, tags []string, repo restic.SaverUnpacked[restic.WriteableFileType], tree *restic.ID) error {
|
||||||
sn, err := restic.NewSnapshot([]string{name}, tags, hostname, time.Now())
|
sn, err := data.NewSnapshot([]string{name}, tags, hostname, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sn.Tree = tree
|
sn.Tree = tree
|
||||||
|
|
||||||
id, err := restic.SaveSnapshot(ctx, repo, sn)
|
id, err := data.SaveSnapshot(ctx, repo, sn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to save snapshot: %v", err)
|
return errors.Fatalf("unable to save snapshot: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"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 testRunRecover(t testing.TB, gopts GlobalOptions) {
|
func testRunRecover(t testing.TB, gopts global.Options) {
|
||||||
rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return runRecover(context.TODO(), gopts, term)
|
return runRecover(context.TODO(), gopts, gopts.Term)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRecover(t *testing.T) {
|
func TestRecover(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
// must list index more than once
|
// must list index more than once
|
||||||
env.gopts.backendTestHook = nil
|
env.gopts.BackendTestHook = nil
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testSetupBackupData(t, env)
|
testSetupBackupData(t, env)
|
||||||
@@ -33,5 +33,7 @@ func TestRecover(t *testing.T) {
|
|||||||
ids = testListSnapshots(t, env.gopts, 1)
|
ids = testListSnapshots(t, env.gopts, 1)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
// check that the root tree is included in the snapshot
|
// check that the root tree is included in the snapshot
|
||||||
rtest.OK(t, runCat(context.TODO(), env.gopts, []string{"tree", ids[0].String() + ":" + sn.Tree.Str()}))
|
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runCat(context.TODO(), gopts, []string{"tree", ids[0].String() + ":" + sn.Tree.Str()}, gopts.Term)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRepairCommand() *cobra.Command {
|
func newRepairCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "repair",
|
Use: "repair",
|
||||||
Short: "Repair the repository",
|
Short: "Repair the repository",
|
||||||
@@ -13,9 +14,9 @@ func newRepairCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newRepairIndexCommand(),
|
newRepairIndexCommand(globalOptions),
|
||||||
newRepairPacksCommand(),
|
newRepairPacksCommand(globalOptions),
|
||||||
newRepairSnapshotsCommand(),
|
newRepairSnapshotsCommand(globalOptions),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRepairIndexCommand() *cobra.Command {
|
func newRepairIndexCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts RepairIndexOptions
|
var opts RepairIndexOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -30,9 +31,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
term, cancel := setupTermstatus()
|
return runRebuildIndex(cmd.Context(), opts, *globalOptions, globalOptions.Term)
|
||||||
defer cancel()
|
|
||||||
return runRebuildIndex(cmd.Context(), opts, globalOptions, term)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +48,10 @@ func (opts *RepairIndexOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
f.BoolVar(&opts.ReadAllPacks, "read-all-packs", false, "read all pack files to generate new index from scratch")
|
f.BoolVar(&opts.ReadAllPacks, "read-all-packs", false, "read all pack files to generate new index from scratch")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRebuildIndexCommand() *cobra.Command {
|
func newRebuildIndexCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts RepairIndexOptions
|
var opts RepairIndexOptions
|
||||||
|
|
||||||
replacement := newRepairIndexCommand()
|
replacement := newRepairIndexCommand(globalOptions)
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "rebuild-index [flags]",
|
Use: "rebuild-index [flags]",
|
||||||
Short: replacement.Short,
|
Short: replacement.Short,
|
||||||
@@ -62,9 +61,7 @@ func newRebuildIndexCommand() *cobra.Command {
|
|||||||
// must create a new instance of the run function as it captures opts
|
// must create a new instance of the run function as it captures opts
|
||||||
// by reference
|
// by reference
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
term, cancel := setupTermstatus()
|
return runRebuildIndex(cmd.Context(), opts, *globalOptions, globalOptions.Term)
|
||||||
defer cancel()
|
|
||||||
return runRebuildIndex(cmd.Context(), opts, globalOptions, term)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,15 +69,15 @@ func newRebuildIndexCommand() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts GlobalOptions, term *termstatus.Terminal) error {
|
func runRebuildIndex(ctx context.Context, opts RepairIndexOptions, gopts global.Options, term ui.Terminal) error {
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
printer := newTerminalProgressPrinter(gopts.verbosity, term)
|
|
||||||
|
|
||||||
err = repository.RepairIndex(ctx, repo, repository.RepairIndexOptions{
|
err = repository.RepairIndex(ctx, repo, repository.RepairIndexOptions{
|
||||||
ReadAllPacks: opts.ReadAllPacks,
|
ReadAllPacks: opts.ReadAllPacks,
|
||||||
}, printer)
|
}, printer)
|
||||||
|
|||||||
@@ -10,29 +10,27 @@ import (
|
|||||||
|
|
||||||
"github.com/restic/restic/internal/backend"
|
"github.com/restic/restic/internal/backend"
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/repository/index"
|
"github.com/restic/restic/internal/repository/index"
|
||||||
"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 testRunRebuildIndex(t testing.TB, gopts GlobalOptions) {
|
func testRunRebuildIndex(t testing.TB, gopts global.Options) {
|
||||||
rtest.OK(t, withRestoreGlobalOptions(func() error {
|
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
return withTermStatus(gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
gopts.Quiet = true
|
||||||
globalOptions.stdout = io.Discard
|
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.Term)
|
||||||
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, term)
|
|
||||||
})
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRebuildIndex(t *testing.T, backendTestHook backendWrapper) {
|
func testRebuildIndex(t *testing.T, backendTestHook global.BackendWrapper) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
|
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
|
||||||
rtest.SetupTarTestFixture(t, env.base, datafile)
|
rtest.SetupTarTestFixture(t, env.base, datafile)
|
||||||
|
|
||||||
out, err := testRunCheckOutput(env.gopts, false)
|
out, err := testRunCheckOutput(t, env.gopts, false)
|
||||||
if !strings.Contains(out, "contained in several indexes") {
|
if !strings.Contains(out, "contained in several indexes") {
|
||||||
t.Fatalf("did not find checker hint for packs in several indexes")
|
t.Fatalf("did not find checker hint for packs in several indexes")
|
||||||
}
|
}
|
||||||
@@ -45,11 +43,11 @@ func testRebuildIndex(t *testing.T, backendTestHook backendWrapper) {
|
|||||||
t.Fatalf("did not find hint for repair index command")
|
t.Fatalf("did not find hint for repair index command")
|
||||||
}
|
}
|
||||||
|
|
||||||
env.gopts.backendTestHook = backendTestHook
|
env.gopts.BackendTestHook = backendTestHook
|
||||||
testRunRebuildIndex(t, env.gopts)
|
testRunRebuildIndex(t, env.gopts)
|
||||||
|
|
||||||
env.gopts.backendTestHook = nil
|
env.gopts.BackendTestHook = nil
|
||||||
out, err = testRunCheckOutput(env.gopts, false)
|
out, err = testRunCheckOutput(t, env.gopts, false)
|
||||||
if len(out) != 0 {
|
if len(out) != 0 {
|
||||||
t.Fatalf("expected no output from the checker, got: %v", out)
|
t.Fatalf("expected no output from the checker, got: %v", out)
|
||||||
}
|
}
|
||||||
@@ -128,14 +126,12 @@ func TestRebuildIndexFailsOnAppendOnly(t *testing.T) {
|
|||||||
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
|
datafile := filepath.Join("..", "..", "internal", "checker", "testdata", "duplicate-packs-in-index-test-repo.tar.gz")
|
||||||
rtest.SetupTarTestFixture(t, env.base, datafile)
|
rtest.SetupTarTestFixture(t, env.base, datafile)
|
||||||
|
|
||||||
err := withRestoreGlobalOptions(func() error {
|
env.gopts.BackendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
||||||
env.gopts.backendTestHook = func(r backend.Backend) (backend.Backend, error) {
|
|
||||||
return &appendOnlyBackend{r}, nil
|
return &appendOnlyBackend{r}, nil
|
||||||
}
|
}
|
||||||
return withTermStatus(env.gopts, func(ctx context.Context, term *termstatus.Terminal) error {
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
globalOptions.stdout = io.Discard
|
gopts.Quiet = true
|
||||||
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, env.gopts, term)
|
return runRebuildIndex(context.TODO(), RepairIndexOptions{}, gopts, gopts.Term)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"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/termstatus"
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRepairPacksCommand() *cobra.Command {
|
func newRepairPacksCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "packs [packIDs...]",
|
Use: "packs [packIDs...]",
|
||||||
Short: "Salvage damaged pack files",
|
Short: "Salvage damaged pack files",
|
||||||
@@ -32,15 +33,13 @@ 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 {
|
||||||
term, cancel := setupTermstatus()
|
return runRepairPacks(cmd.Context(), *globalOptions, globalOptions.Term, args)
|
||||||
defer cancel()
|
|
||||||
return runRepairPacks(cmd.Context(), globalOptions, term, args)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.Terminal, args []string) error {
|
func runRepairPacks(ctx context.Context, gopts global.Options, term ui.Terminal, args []string) error {
|
||||||
ids := restic.NewIDSet()
|
ids := restic.NewIDSet()
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
id, err := restic.ParseID(arg)
|
id, err := restic.ParseID(arg)
|
||||||
@@ -53,16 +52,15 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.T
|
|||||||
return errors.Fatal("no ids specified")
|
return errors.Fatal("no ids specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
printer := newTerminalProgressPrinter(gopts.verbosity, term)
|
err = repo.LoadIndex(ctx, printer)
|
||||||
|
|
||||||
bar := newIndexTerminalProgress(gopts.Quiet, gopts.JSON, term)
|
|
||||||
err = repo.LoadIndex(ctx, bar)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("%s", err)
|
return errors.Fatalf("%s", err)
|
||||||
}
|
}
|
||||||
@@ -93,6 +91,6 @@ func runRepairPacks(ctx context.Context, gopts GlobalOptions, term *termstatus.T
|
|||||||
return errors.Fatalf("%s", err)
|
return errors.Fatalf("%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
Warnf("\nUse `restic repair snapshots --forget` to remove the corrupted data blobs from all snapshots\n")
|
printer.E("\nUse `restic repair snapshots --forget` to remove the corrupted data blobs from all snapshots")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"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/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"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRepairSnapshotsCommand() *cobra.Command {
|
func newRepairSnapshotsCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts RepairOptions
|
var opts RepairOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -49,7 +53,8 @@ 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 runRepairSnapshots(cmd.Context(), globalOptions, opts, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
return runRepairSnapshots(cmd.Context(), *globalOptions, opts, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +67,7 @@ type RepairOptions struct {
|
|||||||
DryRun bool
|
DryRun bool
|
||||||
Forget bool
|
Forget bool
|
||||||
|
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
@@ -72,8 +77,10 @@ func (opts *RepairOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOptions, args []string) error {
|
func runRepairSnapshots(ctx context.Context, gopts global.Options, opts RepairOptions, args []string, term ui.Terminal) error {
|
||||||
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
|
ctx, repo, unlock, err := openWithExclusiveLock(ctx, gopts, opts.DryRun, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -84,8 +91,7 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,12 +100,12 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt
|
|||||||
// - trees which cannot be loaded (-> the tree contents will be removed)
|
// - trees which cannot be loaded (-> the tree contents will be removed)
|
||||||
// - files whose contents are not fully available (-> file will be modified)
|
// - files whose contents are not fully available (-> file will be modified)
|
||||||
rewriter := walker.NewTreeRewriter(walker.RewriteOpts{
|
rewriter := walker.NewTreeRewriter(walker.RewriteOpts{
|
||||||
RewriteNode: func(node *restic.Node, path string) *restic.Node {
|
RewriteNode: func(node *data.Node, path string) *data.Node {
|
||||||
if node.Type == restic.NodeTypeIrregular || node.Type == restic.NodeTypeInvalid {
|
if node.Type == data.NodeTypeIrregular || node.Type == data.NodeTypeInvalid {
|
||||||
Verbosef(" file %q: removed node with invalid type %q\n", path, node.Type)
|
printer.P(" file %q: removed node with invalid type %q", path, node.Type)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if node.Type != restic.NodeTypeFile {
|
if node.Type != data.NodeTypeFile {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,40 +122,36 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
Verbosef(" file %q: removed missing content\n", path)
|
printer.P(" file %q: removed missing content", path)
|
||||||
} else if newSize != node.Size {
|
} else if newSize != node.Size {
|
||||||
Verbosef(" file %q: fixed incorrect size\n", path)
|
printer.P(" file %q: fixed incorrect size", path)
|
||||||
}
|
}
|
||||||
// no-ops if already correct
|
// no-ops if already correct
|
||||||
node.Content = newContent
|
node.Content = newContent
|
||||||
node.Size = newSize
|
node.Size = newSize
|
||||||
return node
|
return node
|
||||||
},
|
},
|
||||||
RewriteFailedTree: func(_ restic.ID, path string, _ error) (restic.ID, error) {
|
RewriteFailedTree: func(_ restic.ID, path string, _ error) (data.TreeNodeIterator, error) {
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
Verbosef(" dir %q: not readable\n", path)
|
printer.P(" dir %q: not readable", path)
|
||||||
// remove snapshots with invalid root node
|
// remove snapshots with invalid root node
|
||||||
return restic.ID{}, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
// If a subtree fails to load, remove it
|
// If a subtree fails to load, remove it
|
||||||
Verbosef(" dir %q: replaced with empty directory\n", path)
|
printer.P(" dir %q: replaced with empty directory", path)
|
||||||
emptyID, err := restic.SaveTree(ctx, repo, &restic.Tree{})
|
return slices.Values([]data.NodeOrError{}), nil
|
||||||
if err != nil {
|
|
||||||
return restic.ID{}, err
|
|
||||||
}
|
|
||||||
return emptyID, nil
|
|
||||||
},
|
},
|
||||||
AllowUnstableSerialization: true,
|
AllowUnstableSerialization: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
changedCount := 0
|
changedCount := 0
|
||||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) {
|
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) {
|
||||||
Verbosef("\n%v\n", sn)
|
printer.P("\n%v", sn)
|
||||||
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
|
changed, err := filterAndReplaceSnapshot(ctx, repo, sn,
|
||||||
func(ctx context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error) {
|
func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error) {
|
||||||
id, err := rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
|
id, err := rewriter.RewriteTree(ctx, repo, uploader, "/", *sn.Tree)
|
||||||
return id, nil, err
|
return id, nil, err
|
||||||
}, opts.DryRun, opts.Forget, nil, "repaired")
|
}, opts.DryRun, opts.Forget, nil, "repaired", printer, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err)
|
return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err)
|
||||||
}
|
}
|
||||||
@@ -161,18 +163,18 @@ func runRepairSnapshots(ctx context.Context, gopts GlobalOptions, opts RepairOpt
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("\n")
|
printer.P("")
|
||||||
if changedCount == 0 {
|
if changedCount == 0 {
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
Verbosef("no snapshots were modified\n")
|
printer.P("no snapshots were modified")
|
||||||
} else {
|
} else {
|
||||||
Verbosef("no snapshots would be modified\n")
|
printer.P("no snapshots would be modified")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
Verbosef("modified %v snapshots\n", changedCount)
|
printer.P("modified %v snapshots", changedCount)
|
||||||
} else {
|
} else {
|
||||||
Verbosef("would modify %v snapshots\n", changedCount)
|
printer.P("would modify %v snapshots", changedCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,19 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunRepairSnapshot(t testing.TB, gopts GlobalOptions, forget bool) {
|
func testRunRepairSnapshot(t testing.TB, gopts global.Options, forget bool) {
|
||||||
opts := RepairOptions{
|
opts := RepairOptions{
|
||||||
Forget: forget,
|
Forget: forget,
|
||||||
}
|
}
|
||||||
|
|
||||||
rtest.OK(t, runRepairSnapshots(context.TODO(), gopts, opts, nil))
|
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runRepairSnapshots(context.TODO(), gopts, opts, nil, gopts.Term)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRandomFile(t testing.TB, env *testEnvironment, path string, size int) {
|
func createRandomFile(t testing.TB, env *testEnvironment, path string, size int) {
|
||||||
@@ -64,7 +67,7 @@ func TestRepairSnapshotsWithLostData(t *testing.T) {
|
|||||||
// repository must be ok after removing the broken snapshots
|
// repository must be ok after removing the broken snapshots
|
||||||
testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
|
testRunForget(t, env.gopts, ForgetOptions{}, snapshotIDs[0].String(), snapshotIDs[1].String())
|
||||||
testListSnapshots(t, env.gopts, 2)
|
testListSnapshots(t, env.gopts, 2)
|
||||||
_, err := testRunCheckOutput(env.gopts, false)
|
_, err := testRunCheckOutput(t, env.gopts, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) {
|
|||||||
createRandomFile(t, env, "foo/bar/file", 12345)
|
createRandomFile(t, env, "foo/bar/file", 12345)
|
||||||
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
||||||
oldSnapshot := testListSnapshots(t, env.gopts, 1)
|
oldSnapshot := testListSnapshots(t, env.gopts, 1)
|
||||||
oldPacks := testRunList(t, "packs", env.gopts)
|
oldPacks := testRunList(t, env.gopts, "packs")
|
||||||
|
|
||||||
// keep foo/bar unchanged
|
// keep foo/bar unchanged
|
||||||
createRandomFile(t, env, "foo/bar2", 1024)
|
createRandomFile(t, env, "foo/bar2", 1024)
|
||||||
@@ -93,7 +96,7 @@ func TestRepairSnapshotsWithLostTree(t *testing.T) {
|
|||||||
testRunRebuildIndex(t, env.gopts)
|
testRunRebuildIndex(t, env.gopts)
|
||||||
testRunRepairSnapshot(t, env.gopts, true)
|
testRunRepairSnapshot(t, env.gopts, true)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
_, err := testRunCheckOutput(env.gopts, false)
|
_, err := testRunCheckOutput(t, env.gopts, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +109,7 @@ func TestRepairSnapshotsWithLostRootTree(t *testing.T) {
|
|||||||
createRandomFile(t, env, "foo/bar/file", 12345)
|
createRandomFile(t, env, "foo/bar/file", 12345)
|
||||||
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
oldPacks := testRunList(t, "packs", env.gopts)
|
oldPacks := testRunList(t, env.gopts, "packs")
|
||||||
|
|
||||||
// remove all trees
|
// remove all trees
|
||||||
removePacks(env.gopts, t, restic.NewIDSet(oldPacks...))
|
removePacks(env.gopts, t, restic.NewIDSet(oldPacks...))
|
||||||
@@ -116,7 +119,7 @@ func TestRepairSnapshotsWithLostRootTree(t *testing.T) {
|
|||||||
testRunRebuildIndex(t, env.gopts)
|
testRunRebuildIndex(t, env.gopts)
|
||||||
testRunRepairSnapshot(t, env.gopts, true)
|
testRunRepairSnapshot(t, env.gopts, true)
|
||||||
testListSnapshots(t, env.gopts, 0)
|
testListSnapshots(t, env.gopts, 0)
|
||||||
_, err := testRunCheckOutput(env.gopts, false)
|
_, err := testRunCheckOutput(t, env.gopts, false)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,24 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"time"
|
"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/filter"
|
"github.com/restic/restic/internal/filter"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/restorer"
|
"github.com/restic/restic/internal/restorer"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
|
"github.com/restic/restic/internal/ui/progress"
|
||||||
restoreui "github.com/restic/restic/internal/ui/restore"
|
restoreui "github.com/restic/restic/internal/ui/restore"
|
||||||
"github.com/restic/restic/internal/ui/termstatus"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRestoreCommand() *cobra.Command {
|
func newRestoreCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts RestoreOptions
|
var opts RestoreOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -34,6 +36,8 @@ repository.
|
|||||||
To only restore a specific subfolder, you can use the "snapshotID:subfolder"
|
To only restore a specific subfolder, you can use the "snapshotID:subfolder"
|
||||||
syntax, where "subfolder" is a path within the snapshot.
|
syntax, where "subfolder" is a path within the snapshot.
|
||||||
|
|
||||||
|
POSIX ACLs are always restored by their numeric value, while file ownership can optionally be restored by name instead of numeric value.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
===========
|
===========
|
||||||
|
|
||||||
@@ -46,9 +50,8 @@ 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()
|
return runRestore(cmd.Context(), opts, *globalOptions, globalOptions.Term, args)
|
||||||
return runRestore(cmd.Context(), opts, globalOptions, term, args)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +64,7 @@ type RestoreOptions struct {
|
|||||||
filter.ExcludePatternOptions
|
filter.ExcludePatternOptions
|
||||||
filter.IncludePatternOptions
|
filter.IncludePatternOptions
|
||||||
Target string
|
Target string
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
DryRun bool
|
DryRun bool
|
||||||
Sparse bool
|
Sparse bool
|
||||||
Verify bool
|
Verify bool
|
||||||
@@ -69,6 +72,7 @@ type RestoreOptions struct {
|
|||||||
Delete bool
|
Delete bool
|
||||||
ExcludeXattrPattern []string
|
ExcludeXattrPattern []string
|
||||||
IncludeXattrPattern []string
|
IncludeXattrPattern []string
|
||||||
|
OwnershipByName bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *RestoreOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *RestoreOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
@@ -86,17 +90,27 @@ func (opts *RestoreOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
f.BoolVar(&opts.Verify, "verify", false, "verify restored files content")
|
f.BoolVar(&opts.Verify, "verify", false, "verify restored files content")
|
||||||
f.Var(&opts.Overwrite, "overwrite", "overwrite behavior, one of (always|if-changed|if-newer|never)")
|
f.Var(&opts.Overwrite, "overwrite", "overwrite behavior, one of (always|if-changed|if-newer|never)")
|
||||||
f.BoolVar(&opts.Delete, "delete", false, "delete files from target directory if they do not exist in snapshot. Use '--dry-run -vv' to check what would be deleted")
|
f.BoolVar(&opts.Delete, "delete", false, "delete files from target directory if they do not exist in snapshot. Use '--dry-run -vv' to check what would be deleted")
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
f.BoolVar(&opts.OwnershipByName, "ownership-by-name", false, "restore file ownership by user name and group name (except POSIX ACLs)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
func runRestore(ctx context.Context, opts RestoreOptions, gopts global.Options,
|
||||||
term *termstatus.Terminal, args []string) error {
|
term ui.Terminal, args []string) error {
|
||||||
|
|
||||||
excludePatternFns, err := opts.ExcludePatternOptions.CollectPatterns(Warnf)
|
var printer restoreui.ProgressPrinter
|
||||||
|
if gopts.JSON {
|
||||||
|
printer = restoreui.NewJSONProgress(term, gopts.Verbosity)
|
||||||
|
} else {
|
||||||
|
printer = restoreui.NewTextProgress(term, gopts.Verbosity)
|
||||||
|
}
|
||||||
|
|
||||||
|
excludePatternFns, err := opts.ExcludePatternOptions.CollectPatterns(printer.E)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
includePatternFns, err := opts.IncludePatternOptions.CollectPatterns(Warnf)
|
includePatternFns, err := opts.IncludePatternOptions.CollectPatterns(printer.E)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -131,47 +145,35 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
|||||||
|
|
||||||
debug.Log("restore %v to %v", snapshotIDString, opts.Target)
|
debug.Log("restore %v to %v", snapshotIDString, opts.Target)
|
||||||
|
|
||||||
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 := 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
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := ui.NewMessage(term, gopts.verbosity)
|
progress := restoreui.NewProgress(printer, ui.CalculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus()))
|
||||||
var printer restoreui.ProgressPrinter
|
|
||||||
if gopts.JSON {
|
|
||||||
printer = restoreui.NewJSONProgress(term, gopts.verbosity)
|
|
||||||
} else {
|
|
||||||
printer = restoreui.NewTextProgress(term, gopts.verbosity)
|
|
||||||
}
|
|
||||||
|
|
||||||
progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON))
|
|
||||||
res := restorer.NewRestorer(repo, sn, restorer.Options{
|
res := restorer.NewRestorer(repo, sn, restorer.Options{
|
||||||
DryRun: opts.DryRun,
|
DryRun: opts.DryRun,
|
||||||
Sparse: opts.Sparse,
|
Sparse: opts.Sparse,
|
||||||
Progress: progress,
|
Progress: progress,
|
||||||
Overwrite: opts.Overwrite,
|
Overwrite: opts.Overwrite,
|
||||||
Delete: opts.Delete,
|
Delete: opts.Delete,
|
||||||
|
OwnershipByName: opts.OwnershipByName,
|
||||||
})
|
})
|
||||||
|
|
||||||
totalErrors := 0
|
totalErrors := 0
|
||||||
@@ -180,13 +182,13 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
|||||||
return progress.Error(location, err)
|
return progress.Error(location, err)
|
||||||
}
|
}
|
||||||
res.Warn = func(message string) {
|
res.Warn = func(message string) {
|
||||||
msg.E("Warning: %s\n", message)
|
printer.E("Warning: %s\n", message)
|
||||||
}
|
}
|
||||||
res.Info = func(message string) {
|
res.Info = func(message string) {
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg.P("Info: %s\n", message)
|
printer.P("Info: %s\n", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectExcludeFilter := func(item string, isDir bool) (selectedForRestore bool, childMayBeSelected bool) {
|
selectExcludeFilter := func(item string, isDir bool) (selectedForRestore bool, childMayBeSelected bool) {
|
||||||
@@ -234,13 +236,13 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
|||||||
res.SelectFilter = selectIncludeFilter
|
res.SelectFilter = selectIncludeFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
res.XattrSelectFilter, err = getXattrSelectFilter(opts)
|
res.XattrSelectFilter, err = getXattrSelectFilter(opts, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
msg.P("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
printer.P("restoring %s to %s\n", res.Snapshot(), opts.Target)
|
||||||
}
|
}
|
||||||
|
|
||||||
countRestoredFiles, err := res.RestoreTo(ctx, opts.Target)
|
countRestoredFiles, err := res.RestoreTo(ctx, opts.Target)
|
||||||
@@ -251,26 +253,26 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
|||||||
progress.Finish()
|
progress.Finish()
|
||||||
|
|
||||||
if totalErrors > 0 {
|
if totalErrors > 0 {
|
||||||
return errors.Fatalf("There were %d errors\n", totalErrors)
|
return errors.Fatalf("There were %d errors", totalErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Verify {
|
if opts.Verify {
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
msg.P("verifying files in %s\n", opts.Target)
|
printer.P("verifying files in %s\n", opts.Target)
|
||||||
}
|
}
|
||||||
var count int
|
var count int
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
bar := newTerminalProgressMax(!gopts.Quiet && !gopts.JSON && stdoutIsTerminal(), 0, "files verified", term)
|
bar := printer.NewCounterTerminalOnly("files verified")
|
||||||
count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar)
|
count, err = res.VerifyFiles(ctx, opts.Target, countRestoredFiles, bar)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if totalErrors > 0 {
|
if totalErrors > 0 {
|
||||||
return errors.Fatalf("There were %d errors\n", totalErrors)
|
return errors.Fatalf("There were %d errors", totalErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !gopts.JSON {
|
if !gopts.JSON {
|
||||||
msg.P("finished verifying %d files in %s (took %s)\n", count, opts.Target,
|
printer.P("finished verifying %d files in %s (took %s)\n", count, opts.Target,
|
||||||
time.Since(t0).Round(time.Millisecond))
|
time.Since(t0).Round(time.Millisecond))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,7 +280,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getXattrSelectFilter(opts RestoreOptions) (func(xattrName string) bool, error) {
|
func getXattrSelectFilter(opts RestoreOptions, printer progress.Printer) (func(xattrName string) bool, error) {
|
||||||
hasXattrExcludes := len(opts.ExcludeXattrPattern) > 0
|
hasXattrExcludes := len(opts.ExcludeXattrPattern) > 0
|
||||||
hasXattrIncludes := len(opts.IncludeXattrPattern) > 0
|
hasXattrIncludes := len(opts.IncludeXattrPattern) > 0
|
||||||
|
|
||||||
@@ -292,7 +294,7 @@ func getXattrSelectFilter(opts RestoreOptions) (func(xattrName string) bool, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
return func(xattrName string) bool {
|
return func(xattrName string) bool {
|
||||||
shouldReject := filter.RejectByPattern(opts.ExcludeXattrPattern, Warnf)(xattrName)
|
shouldReject := filter.RejectByPattern(opts.ExcludeXattrPattern, printer.E)(xattrName)
|
||||||
return !shouldReject
|
return !shouldReject
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -304,7 +306,7 @@ func getXattrSelectFilter(opts RestoreOptions) (func(xattrName string) bool, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
return func(xattrName string) bool {
|
return func(xattrName string) bool {
|
||||||
shouldInclude, _ := filter.IncludeByPattern(opts.IncludeXattrPattern, Warnf)(xattrName)
|
shouldInclude, _ := filter.IncludeByPattern(opts.IncludeXattrPattern, printer.E)(xattrName)
|
||||||
return shouldInclude
|
return shouldInclude
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -12,67 +11,68 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
|
"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 testRunRestore(t testing.TB, opts GlobalOptions, dir string, snapshotID string) {
|
func testRunRestore(t testing.TB, gopts global.Options, dir string, snapshotID string) {
|
||||||
testRunRestoreExcludes(t, opts, dir, snapshotID, nil)
|
testRunRestoreExcludes(t, gopts, dir, snapshotID, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunRestoreExcludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID string, excludes []string) {
|
func testRunRestoreExcludes(t testing.TB, gopts global.Options, dir string, snapshotID string, excludes []string) {
|
||||||
opts := RestoreOptions{
|
opts := RestoreOptions{
|
||||||
Target: dir,
|
Target: dir,
|
||||||
}
|
}
|
||||||
opts.Excludes = excludes
|
opts.Excludes = excludes
|
||||||
|
|
||||||
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID, opts, gopts))
|
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID, opts, gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunRestoreAssumeFailure(snapshotID string, opts RestoreOptions, gopts GlobalOptions) error {
|
func testRunRestoreAssumeFailure(t testing.TB, snapshotID string, opts RestoreOptions, 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 {
|
||||||
return runRestore(ctx, opts, gopts, term, []string{snapshotID})
|
return runRestore(ctx, opts, gopts, gopts.Term, []string{snapshotID})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunRestoreLatest(t testing.TB, gopts GlobalOptions, dir string, paths []string, hosts []string) {
|
func testRunRestoreLatest(t testing.TB, gopts global.Options, dir string, paths []string, hosts []string) {
|
||||||
opts := RestoreOptions{
|
opts := RestoreOptions{
|
||||||
Target: dir,
|
Target: dir,
|
||||||
SnapshotFilter: restic.SnapshotFilter{
|
SnapshotFilter: data.SnapshotFilter{
|
||||||
Hosts: hosts,
|
Hosts: hosts,
|
||||||
Paths: paths,
|
Paths: paths,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rtest.OK(t, testRunRestoreAssumeFailure("latest", opts, gopts))
|
rtest.OK(t, testRunRestoreAssumeFailure(t, "latest", opts, gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunRestoreIncludes(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, includes []string) {
|
func testRunRestoreIncludes(t testing.TB, gopts global.Options, dir string, snapshotID restic.ID, includes []string) {
|
||||||
opts := RestoreOptions{
|
opts := RestoreOptions{
|
||||||
Target: dir,
|
Target: dir,
|
||||||
}
|
}
|
||||||
opts.Includes = includes
|
opts.Includes = includes
|
||||||
|
|
||||||
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID.String(), opts, gopts))
|
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID.String(), opts, gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunRestoreIncludesFromFile(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, includesFile string) {
|
func testRunRestoreIncludesFromFile(t testing.TB, gopts global.Options, dir string, snapshotID restic.ID, includesFile string) {
|
||||||
opts := RestoreOptions{
|
opts := RestoreOptions{
|
||||||
Target: dir,
|
Target: dir,
|
||||||
}
|
}
|
||||||
opts.IncludeFiles = []string{includesFile}
|
opts.IncludeFiles = []string{includesFile}
|
||||||
|
|
||||||
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID.String(), opts, gopts))
|
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID.String(), opts, gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRunRestoreExcludesFromFile(t testing.TB, gopts GlobalOptions, dir string, snapshotID restic.ID, excludesFile string) {
|
func testRunRestoreExcludesFromFile(t testing.TB, gopts global.Options, dir string, snapshotID restic.ID, excludesFile string) {
|
||||||
opts := RestoreOptions{
|
opts := RestoreOptions{
|
||||||
Target: dir,
|
Target: dir,
|
||||||
}
|
}
|
||||||
opts.ExcludeFiles = []string{excludesFile}
|
opts.ExcludeFiles = []string{excludesFile}
|
||||||
|
|
||||||
rtest.OK(t, testRunRestoreAssumeFailure(snapshotID.String(), opts, gopts))
|
rtest.OK(t, testRunRestoreAssumeFailure(t, snapshotID.String(), opts, gopts))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRestoreMustFailWhenUsingBothIncludesAndExcludes(t *testing.T) {
|
func TestRestoreMustFailWhenUsingBothIncludesAndExcludes(t *testing.T) {
|
||||||
@@ -93,7 +93,7 @@ func TestRestoreMustFailWhenUsingBothIncludesAndExcludes(t *testing.T) {
|
|||||||
restoreOpts.Includes = includePatterns
|
restoreOpts.Includes = includePatterns
|
||||||
restoreOpts.Excludes = excludePatterns
|
restoreOpts.Excludes = excludePatterns
|
||||||
|
|
||||||
err := testRunRestoreAssumeFailure("latest", restoreOpts, env.gopts)
|
err := testRunRestoreAssumeFailure(t, "latest", restoreOpts, env.gopts)
|
||||||
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "exclude and include patterns are mutually exclusive"),
|
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "exclude and include patterns are mutually exclusive"),
|
||||||
"expected: %s error, got %v", "exclude and include patterns are mutually exclusive", err)
|
"expected: %s error, got %v", "exclude and include patterns are mutually exclusive", err)
|
||||||
}
|
}
|
||||||
@@ -257,7 +257,7 @@ func TestRestore(t *testing.T) {
|
|||||||
restoredir := filepath.Join(env.base, "restore")
|
restoredir := filepath.Join(env.base, "restore")
|
||||||
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
|
testRunRestoreLatest(t, env.gopts, restoredir, nil, nil)
|
||||||
|
|
||||||
diff := directoriesContentsDiff(env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata)))
|
diff := directoriesContentsDiff(t, env.testdata, filepath.Join(restoredir, filepath.Base(env.testdata)))
|
||||||
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
rtest.Assert(t, diff == "", "directories are not equal %v", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,11 +337,7 @@ func TestRestoreWithPermissionFailure(t *testing.T) {
|
|||||||
|
|
||||||
snapshots := testListSnapshots(t, env.gopts, 1)
|
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
_ = withRestoreGlobalOptions(func() error {
|
|
||||||
globalOptions.stderr = io.Discard
|
|
||||||
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0].String())
|
testRunRestore(t, env.gopts, filepath.Join(env.base, "restore"), snapshots[0].String())
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure that all files have been restored, regardless of any
|
// make sure that all files have been restored, regardless of any
|
||||||
// permission errors
|
// permission errors
|
||||||
@@ -398,7 +394,7 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) {
|
|||||||
fi, err := os.Stat(f2)
|
fi, err := os.Stat(f2)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Assert(t, fi.ModTime() == time.Unix(0, 0),
|
rtest.Assert(t, fi.ModTime().Equal(time.Unix(0, 0)),
|
||||||
"meta data of intermediate directory hasn't been restore")
|
"meta data of intermediate directory hasn't been restore")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
|
|
||||||
|
"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/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/progress"
|
||||||
"github.com/restic/restic/internal/walker"
|
"github.com/restic/restic/internal/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newRewriteCommand() *cobra.Command {
|
func newRewriteCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts RewriteOptions
|
var opts RewriteOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -27,6 +30,9 @@ The "rewrite" command excludes files from existing snapshots. It creates new
|
|||||||
snapshots containing the same data as the original ones, but without the files
|
snapshots containing the same data as the original ones, but without the files
|
||||||
you specify to exclude. All metadata (time, host, tags) will be preserved.
|
you specify to exclude. All metadata (time, host, tags) will be preserved.
|
||||||
|
|
||||||
|
Alternatively you can use one of the --include variants to only include files
|
||||||
|
in the new snapshot which you want to preserve.
|
||||||
|
|
||||||
The snapshots to rewrite are specified using the --host, --tag and --path options,
|
The snapshots to rewrite are specified using the --host, --tag and --path options,
|
||||||
or by providing a list of snapshot IDs. Please note that specifying neither any of
|
or by providing a list of snapshot IDs. Please note that specifying neither any of
|
||||||
these options nor a snapshot ID will cause the command to rewrite all snapshots.
|
these options nor a snapshot ID will cause the command to rewrite all snapshots.
|
||||||
@@ -43,8 +49,8 @@ When rewrite is used with the --snapshot-summary option, a new snapshot is
|
|||||||
created containing statistics summary data. Only two fields in the summary will
|
created containing statistics summary data. Only two fields in the summary will
|
||||||
be non-zero: TotalFilesProcessed and TotalBytesProcessed.
|
be non-zero: TotalFilesProcessed and TotalBytesProcessed.
|
||||||
|
|
||||||
When rewrite is called with one of the --exclude options, TotalFilesProcessed
|
When rewrite is called with one of the --exclude or --include options,
|
||||||
and TotalBytesProcessed will be updated in the snapshot summary.
|
TotalFilesProcessed and TotalBytesProcessed will be updated in the snapshot summary.
|
||||||
|
|
||||||
EXIT STATUS
|
EXIT STATUS
|
||||||
===========
|
===========
|
||||||
@@ -58,7 +64,8 @@ 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 runRewrite(cmd.Context(), opts, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
return runRewrite(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +94,9 @@ func (sma snapshotMetadataArgs) convert() (*snapshotMetadata, error) {
|
|||||||
|
|
||||||
var timeStamp *time.Time
|
var timeStamp *time.Time
|
||||||
if sma.Time != "" {
|
if sma.Time != "" {
|
||||||
t, err := time.ParseInLocation(TimeFormat, sma.Time, time.Local)
|
t, err := time.ParseInLocation(global.TimeFormat, sma.Time, time.Local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Fatalf("error in time option: %v\n", err)
|
return nil, errors.Fatalf("error in time option: %v", err)
|
||||||
}
|
}
|
||||||
timeStamp = &t
|
timeStamp = &t
|
||||||
}
|
}
|
||||||
@@ -103,8 +110,9 @@ type RewriteOptions struct {
|
|||||||
SnapshotSummary bool
|
SnapshotSummary bool
|
||||||
|
|
||||||
Metadata snapshotMetadataArgs
|
Metadata snapshotMetadataArgs
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
filter.ExcludePatternOptions
|
filter.ExcludePatternOptions
|
||||||
|
filter.IncludePatternOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
@@ -116,18 +124,24 @@ func (opts *RewriteOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
|
|
||||||
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
initMultiSnapshotFilter(f, &opts.SnapshotFilter, true)
|
||||||
opts.ExcludePatternOptions.Add(f)
|
opts.ExcludePatternOptions.Add(f)
|
||||||
|
opts.IncludePatternOptions.Add(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewriteFilterFunc returns the filtered tree ID or an error. If a snapshot summary is returned, the snapshot will
|
// rewriteFilterFunc returns the filtered tree ID or an error. If a snapshot summary is returned, the snapshot will
|
||||||
// be updated accordingly.
|
// be updated accordingly.
|
||||||
type rewriteFilterFunc func(ctx context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error)
|
type rewriteFilterFunc func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error)
|
||||||
|
|
||||||
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *restic.Snapshot, opts RewriteOptions) (bool, error) {
|
func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *data.Snapshot, opts RewriteOptions, printer progress.Printer) (bool, error) {
|
||||||
if sn.Tree == nil {
|
if sn.Tree == nil {
|
||||||
return false, errors.Errorf("snapshot %v has nil tree", sn.ID().Str())
|
return false, errors.Errorf("snapshot %v has nil tree", sn.ID().Str())
|
||||||
}
|
}
|
||||||
|
|
||||||
rejectByNameFuncs, err := opts.ExcludePatternOptions.CollectPatterns(Warnf)
|
rejectByNameFuncs, err := opts.ExcludePatternOptions.CollectPatterns(printer.E)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
includeByNameFuncs, err := opts.IncludePatternOptions.CollectPatterns(printer.E)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -138,35 +152,28 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *resti
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
condInclude := len(includeByNameFuncs) > 0
|
||||||
|
condExclude := len(rejectByNameFuncs) > 0
|
||||||
var filter rewriteFilterFunc
|
var filter rewriteFilterFunc
|
||||||
|
|
||||||
if len(rejectByNameFuncs) > 0 || opts.SnapshotSummary {
|
if condInclude || condExclude || opts.SnapshotSummary {
|
||||||
selectByName := func(nodepath string) bool {
|
var rewriteNode walker.NodeRewriteFunc
|
||||||
for _, reject := range rejectByNameFuncs {
|
var keepEmptyDirectoryFunc walker.NodeKeepEmptyDirectoryFunc
|
||||||
if reject(nodepath) {
|
if condInclude {
|
||||||
return false
|
rewriteNode, keepEmptyDirectoryFunc = gatherIncludeFilters(includeByNameFuncs, printer)
|
||||||
}
|
} else {
|
||||||
}
|
rewriteNode = gatherExcludeFilters(rejectByNameFuncs, printer)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rewriteNode := func(node *restic.Node, path string) *restic.Node {
|
rewriter, querySize := walker.NewSnapshotSizeRewriter(rewriteNode, keepEmptyDirectoryFunc)
|
||||||
if selectByName(path) {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
Verbosef("excluding %s\n", path)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rewriter, querySize := walker.NewSnapshotSizeRewriter(rewriteNode)
|
filter = func(ctx context.Context, sn *data.Snapshot, uploader restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error) {
|
||||||
|
id, err := rewriter.RewriteTree(ctx, repo, uploader, "/", *sn.Tree)
|
||||||
filter = func(ctx context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error) {
|
|
||||||
id, err := rewriter.RewriteTree(ctx, repo, "/", *sn.Tree)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return restic.ID{}, nil, err
|
return restic.ID{}, nil, err
|
||||||
}
|
}
|
||||||
ss := querySize()
|
ss := querySize()
|
||||||
summary := &restic.SnapshotSummary{}
|
summary := &data.SnapshotSummary{}
|
||||||
if sn.Summary != nil {
|
if sn.Summary != nil {
|
||||||
*summary = *sn.Summary
|
*summary = *sn.Summary
|
||||||
}
|
}
|
||||||
@@ -176,46 +183,43 @@ func rewriteSnapshot(ctx context.Context, repo *repository.Repository, sn *resti
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
filter = func(_ context.Context, sn *restic.Snapshot) (restic.ID, *restic.SnapshotSummary, error) {
|
filter = func(_ context.Context, sn *data.Snapshot, _ restic.BlobSaver) (restic.ID, *data.SnapshotSummary, error) {
|
||||||
return *sn.Tree, nil, nil
|
return *sn.Tree, nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterAndReplaceSnapshot(ctx, repo, sn,
|
return filterAndReplaceSnapshot(ctx, repo, sn,
|
||||||
filter, opts.DryRun, opts.Forget, metadata, "rewrite")
|
filter, opts.DryRun, opts.Forget, metadata, "rewrite", printer, len(includeByNameFuncs) > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *restic.Snapshot,
|
func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *data.Snapshot,
|
||||||
filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string) (bool, error) {
|
filter rewriteFilterFunc, dryRun bool, forget bool, newMetadata *snapshotMetadata, addTag string, printer progress.Printer,
|
||||||
|
keepEmptySnapshot bool) (bool, error) {
|
||||||
wg, wgCtx := errgroup.WithContext(ctx)
|
|
||||||
repo.StartPackUploader(wgCtx, wg)
|
|
||||||
|
|
||||||
var filteredTree restic.ID
|
var filteredTree restic.ID
|
||||||
var summary *restic.SnapshotSummary
|
var summary *data.SnapshotSummary
|
||||||
wg.Go(func() error {
|
err := repo.WithBlobUploader(ctx, func(ctx context.Context, uploader restic.BlobSaverWithAsync) error {
|
||||||
var err error
|
var err error
|
||||||
filteredTree, summary, err = filter(ctx, sn)
|
filteredTree, summary, err = filter(ctx, sn, uploader)
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
return repo.Flush(wgCtx)
|
|
||||||
})
|
})
|
||||||
err := wg.Wait()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if filteredTree.IsNull() {
|
if filteredTree.IsNull() {
|
||||||
|
if keepEmptySnapshot {
|
||||||
|
debug.Log("Snapshot %v not modified", sn)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
if dryRun {
|
if dryRun {
|
||||||
Verbosef("would delete empty snapshot\n")
|
printer.P("would delete empty snapshot")
|
||||||
} else {
|
} else {
|
||||||
if err = repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, *sn.ID()); err != nil {
|
if err = repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, *sn.ID()); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
debug.Log("removed empty snapshot %v", sn.ID())
|
debug.Log("removed empty snapshot %v", sn.ID())
|
||||||
Verbosef("removed empty snapshot %v\n", sn.ID().Str())
|
printer.P("removed empty snapshot %v", sn.ID().Str())
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
@@ -232,18 +236,18 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r
|
|||||||
|
|
||||||
debug.Log("Snapshot %v modified", sn)
|
debug.Log("Snapshot %v modified", sn)
|
||||||
if dryRun {
|
if dryRun {
|
||||||
Verbosef("would save new snapshot\n")
|
printer.P("would save new snapshot")
|
||||||
|
|
||||||
if forget {
|
if forget {
|
||||||
Verbosef("would remove old snapshot\n")
|
printer.P("would remove old snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
if newMetadata != nil && newMetadata.Time != nil {
|
if newMetadata != nil && newMetadata.Time != nil {
|
||||||
Verbosef("would set time to %s\n", newMetadata.Time)
|
printer.P("would set time to %s", newMetadata.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
if newMetadata != nil && newMetadata.Hostname != "" {
|
if newMetadata != nil && newMetadata.Hostname != "" {
|
||||||
Verbosef("would set hostname to %s\n", newMetadata.Hostname)
|
printer.P("would set hostname to %s", newMetadata.Hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -261,37 +265,43 @@ func filterAndReplaceSnapshot(ctx context.Context, repo restic.Repository, sn *r
|
|||||||
}
|
}
|
||||||
|
|
||||||
if newMetadata != nil && newMetadata.Time != nil {
|
if newMetadata != nil && newMetadata.Time != nil {
|
||||||
Verbosef("setting time to %s\n", *newMetadata.Time)
|
printer.P("setting time to %s", *newMetadata.Time)
|
||||||
sn.Time = *newMetadata.Time
|
sn.Time = *newMetadata.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
if newMetadata != nil && newMetadata.Hostname != "" {
|
if newMetadata != nil && newMetadata.Hostname != "" {
|
||||||
Verbosef("setting host to %s\n", newMetadata.Hostname)
|
printer.P("setting host to %s", newMetadata.Hostname)
|
||||||
sn.Hostname = newMetadata.Hostname
|
sn.Hostname = newMetadata.Hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the new snapshot.
|
// Save the new snapshot.
|
||||||
id, err := restic.SaveSnapshot(ctx, repo, sn)
|
id, err := data.SaveSnapshot(ctx, repo, sn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
Verbosef("saved new snapshot %v\n", id.Str())
|
printer.P("saved new snapshot %v", id.Str())
|
||||||
|
|
||||||
if forget {
|
if forget {
|
||||||
if err = repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, *sn.ID()); err != nil {
|
if err = repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, *sn.ID()); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
debug.Log("removed old snapshot %v", sn.ID())
|
debug.Log("removed old snapshot %v", sn.ID())
|
||||||
Verbosef("removed old snapshot %v\n", sn.ID().Str())
|
printer.P("removed old snapshot %v", sn.ID().Str())
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, args []string) error {
|
func runRewrite(ctx context.Context, opts RewriteOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
if !opts.SnapshotSummary && opts.ExcludePatternOptions.Empty() && opts.Metadata.empty() {
|
hasExcludes := !opts.ExcludePatternOptions.Empty()
|
||||||
return errors.Fatal("Nothing to do: no excludes provided and no new metadata provided")
|
hasIncludes := !opts.IncludePatternOptions.Empty()
|
||||||
|
if !opts.SnapshotSummary && !hasExcludes && !hasIncludes && opts.Metadata.empty() {
|
||||||
|
return errors.Fatal("Nothing to do: no excludes/includes provided and no new metadata provided")
|
||||||
|
} else if hasExcludes && hasIncludes {
|
||||||
|
return errors.Fatal("exclude and include patterns are mutually exclusive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
repo *repository.Repository
|
repo *repository.Repository
|
||||||
unlock func()
|
unlock func()
|
||||||
@@ -299,10 +309,10 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a
|
|||||||
)
|
)
|
||||||
|
|
||||||
if opts.Forget {
|
if opts.Forget {
|
||||||
Verbosef("create exclusive lock for repository\n")
|
printer.P("create exclusive lock for repository")
|
||||||
ctx, repo, unlock, err = openWithExclusiveLock(ctx, gopts, opts.DryRun)
|
ctx, repo, unlock, err = openWithExclusiveLock(ctx, gopts, opts.DryRun, printer)
|
||||||
} else {
|
} else {
|
||||||
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
|
||||||
@@ -314,15 +324,14 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
changedCount := 0
|
changedCount := 0
|
||||||
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args) {
|
for sn := range FindFilteredSnapshots(ctx, snapshotLister, repo, &opts.SnapshotFilter, args, printer) {
|
||||||
Verbosef("\n%v\n", sn)
|
printer.P("\n%v", sn)
|
||||||
changed, err := rewriteSnapshot(ctx, repo, sn, opts)
|
changed, err := rewriteSnapshot(ctx, repo, sn, opts, printer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err)
|
return errors.Fatalf("unable to rewrite snapshot ID %q: %v", sn.ID().Str(), err)
|
||||||
}
|
}
|
||||||
@@ -334,20 +343,89 @@ func runRewrite(ctx context.Context, opts RewriteOptions, gopts GlobalOptions, a
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("\n")
|
printer.P("")
|
||||||
if changedCount == 0 {
|
if changedCount == 0 {
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
Verbosef("no snapshots were modified\n")
|
printer.P("no snapshots were modified")
|
||||||
} else {
|
} else {
|
||||||
Verbosef("no snapshots would be modified\n")
|
printer.P("no snapshots would be modified")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
Verbosef("modified %v snapshots\n", changedCount)
|
printer.P("modified %v snapshots", changedCount)
|
||||||
} else {
|
} else {
|
||||||
Verbosef("would modify %v snapshots\n", changedCount)
|
printer.P("would modify %v snapshots", changedCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gatherIncludeFilters(includeByNameFuncs []filter.IncludeByNameFunc, printer progress.Printer) (rewriteNode walker.NodeRewriteFunc, keepEmptyDirectory walker.NodeKeepEmptyDirectoryFunc) {
|
||||||
|
inSelectByName := func(nodepath string, node *data.Node) bool {
|
||||||
|
for _, include := range includeByNameFuncs {
|
||||||
|
matched, childMayMatch := include(nodepath)
|
||||||
|
if node.Type == data.NodeTypeDir {
|
||||||
|
// include directories if they or some of their children may be included
|
||||||
|
if matched || childMayMatch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
rewriteNode = func(node *data.Node, path string) *data.Node {
|
||||||
|
if inSelectByName(path, node) {
|
||||||
|
if node.Type != data.NodeTypeDir {
|
||||||
|
printer.VV("including %q\n", path)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
inSelectByNameDir := func(nodepath string) bool {
|
||||||
|
for _, include := range includeByNameFuncs {
|
||||||
|
matched, _ := include(nodepath)
|
||||||
|
if matched {
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
keepEmptyDirectory = func(path string) bool {
|
||||||
|
keep := inSelectByNameDir(path)
|
||||||
|
if keep {
|
||||||
|
printer.VV("including directory %q\n", path)
|
||||||
|
}
|
||||||
|
return keep
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewriteNode, keepEmptyDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatherExcludeFilters(excludeByNameFuncs []filter.RejectByNameFunc, printer progress.Printer) (rewriteNode walker.NodeRewriteFunc) {
|
||||||
|
exSelectByName := func(nodepath string) bool {
|
||||||
|
for _, reject := range excludeByNameFuncs {
|
||||||
|
if reject(nodepath) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
rewriteNode = func(node *data.Node, path string) *data.Node {
|
||||||
|
if exSelectByName(path) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.VV("excluding %q\n", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewriteNode
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
"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"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
"github.com/restic/restic/internal/ui"
|
"github.com/restic/restic/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunRewriteExclude(t testing.TB, gopts GlobalOptions, excludes []string, forget bool, metadata snapshotMetadataArgs) {
|
func testRunRewriteExclude(t testing.TB, gopts global.Options, excludes []string, forget bool, metadata snapshotMetadataArgs) {
|
||||||
opts := RewriteOptions{
|
opts := RewriteOptions{
|
||||||
ExcludePatternOptions: filter.ExcludePatternOptions{
|
ExcludePatternOptions: filter.ExcludePatternOptions{
|
||||||
Excludes: excludes,
|
Excludes: excludes,
|
||||||
@@ -20,7 +24,30 @@ func testRunRewriteExclude(t testing.TB, gopts GlobalOptions, excludes []string,
|
|||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
rtest.OK(t, runRewrite(context.TODO(), opts, gopts, nil))
|
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runRewrite(context.TODO(), opts, gopts, nil, gopts.Term)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRunRewriteWithOpts(t testing.TB, opts RewriteOptions, gopts global.Options, args []string) error {
|
||||||
|
rtest.OK(t, withTermStatus(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runRewrite(context.TODO(), opts, gopts, args, gopts.Term)
|
||||||
|
}))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// testLsOutputContainsCount runs restic ls with the given options and asserts that
|
||||||
|
// exactly expectedCount lines of the output contain substring.
|
||||||
|
func testLsOutputContainsCount(t testing.TB, gopts global.Options, lsOpts LsOptions, lsArgs []string, substring string, expectedCount int) {
|
||||||
|
t.Helper()
|
||||||
|
out := testRunLsWithOpts(t, gopts, lsOpts, lsArgs)
|
||||||
|
count := 0
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
if strings.Contains(line, substring) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rtest.Assert(t, count == expectedCount, "expected %d lines containing %q, but got %d", expectedCount, substring, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createBasicRewriteRepo(t testing.TB, env *testEnvironment) restic.ID {
|
func createBasicRewriteRepo(t testing.TB, env *testEnvironment) restic.ID {
|
||||||
@@ -28,21 +55,40 @@ func createBasicRewriteRepo(t testing.TB, env *testEnvironment) restic.ID {
|
|||||||
|
|
||||||
// create backup
|
// create backup
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, BackupOptions{}, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, BackupOptions{}, env.gopts)
|
||||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
snapshotIDs := testRunList(t, env.gopts, "snapshots")
|
||||||
rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs)
|
rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
|
|
||||||
return snapshotIDs[0]
|
return snapshotIDs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSnapshot(t testing.TB, snapshotID restic.ID, env *testEnvironment) *restic.Snapshot {
|
func createBasicRewriteRepoWithEmptyDirectory(t testing.TB, env *testEnvironment) restic.ID {
|
||||||
|
testSetupBackupData(t, env)
|
||||||
|
|
||||||
|
// make an empty directory named "empty-directory"
|
||||||
|
rtest.OK(t, os.Mkdir(filepath.Join(env.testdata, "/0/tests", "empty-directory"), 0755))
|
||||||
|
|
||||||
|
// create backup
|
||||||
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, BackupOptions{}, env.gopts)
|
||||||
|
snapshotIDs := testRunList(t, env.gopts, "snapshots")
|
||||||
|
rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs)
|
||||||
|
|
||||||
|
return snapshotIDs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSnapshot(t testing.TB, snapshotID restic.ID, env *testEnvironment) *data.Snapshot {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithReadLock(context.TODO(), env.gopts, false)
|
var snapshots []*data.Snapshot
|
||||||
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||||
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
snapshots, err := restic.TestLoadAllSnapshots(ctx, repo, nil)
|
snapshots, err = data.TestLoadAllSnapshots(ctx, repo, nil)
|
||||||
|
return err
|
||||||
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
for _, s := range snapshots {
|
for _, s := range snapshots {
|
||||||
@@ -60,7 +106,7 @@ func TestRewrite(t *testing.T) {
|
|||||||
|
|
||||||
// exclude some data
|
// exclude some data
|
||||||
testRunRewriteExclude(t, env.gopts, []string{"3"}, false, snapshotMetadataArgs{Hostname: "", Time: ""})
|
testRunRewriteExclude(t, env.gopts, []string{"3"}, false, snapshotMetadataArgs{Hostname: "", Time: ""})
|
||||||
snapshotIDs := testRunList(t, "snapshots", env.gopts)
|
snapshotIDs := testRunList(t, env.gopts, "snapshots")
|
||||||
rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs)
|
rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs)
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
@@ -72,7 +118,7 @@ func TestRewriteUnchanged(t *testing.T) {
|
|||||||
|
|
||||||
// use an exclude that will not exclude anything
|
// use an exclude that will not exclude anything
|
||||||
testRunRewriteExclude(t, env.gopts, []string{"3dflkhjgdflhkjetrlkhjgfdlhkj"}, false, snapshotMetadataArgs{Hostname: "", Time: ""})
|
testRunRewriteExclude(t, env.gopts, []string{"3dflkhjgdflhkjetrlkhjgfdlhkj"}, false, snapshotMetadataArgs{Hostname: "", Time: ""})
|
||||||
newSnapshotIDs := testRunList(t, "snapshots", env.gopts)
|
newSnapshotIDs := testRunList(t, env.gopts, "snapshots")
|
||||||
rtest.Assert(t, len(newSnapshotIDs) == 1, "expected one snapshot, got %v", newSnapshotIDs)
|
rtest.Assert(t, len(newSnapshotIDs) == 1, "expected one snapshot, got %v", newSnapshotIDs)
|
||||||
rtest.Assert(t, snapshotID == newSnapshotIDs[0], "snapshot id changed unexpectedly")
|
rtest.Assert(t, snapshotID == newSnapshotIDs[0], "snapshot id changed unexpectedly")
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
@@ -109,17 +155,22 @@ func testRewriteMetadata(t *testing.T, metadata snapshotMetadataArgs) {
|
|||||||
createBasicRewriteRepo(t, env)
|
createBasicRewriteRepo(t, env)
|
||||||
testRunRewriteExclude(t, env.gopts, []string{}, true, metadata)
|
testRunRewriteExclude(t, env.gopts, []string{}, true, metadata)
|
||||||
|
|
||||||
ctx, repo, unlock, err := openWithReadLock(context.TODO(), env.gopts, false)
|
var snapshots []*data.Snapshot
|
||||||
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
printer := ui.NewProgressPrinter(gopts.JSON, gopts.Verbosity, gopts.Term)
|
||||||
|
ctx, repo, unlock, err := openWithReadLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
snapshots, err := restic.TestLoadAllSnapshots(ctx, repo, nil)
|
snapshots, err = data.TestLoadAllSnapshots(ctx, repo, nil)
|
||||||
|
return err
|
||||||
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
rtest.Assert(t, len(snapshots) == 1, "expected one snapshot, got %v", len(snapshots))
|
rtest.Assert(t, len(snapshots) == 1, "expected one snapshot, got %v", len(snapshots))
|
||||||
newSnapshot := snapshots[0]
|
newSnapshot := snapshots[0]
|
||||||
|
|
||||||
if metadata.Time != "" {
|
if metadata.Time != "" {
|
||||||
rtest.Assert(t, newSnapshot.Time.Format(TimeFormat) == metadata.Time, "New snapshot should have time %s", metadata.Time)
|
rtest.Assert(t, newSnapshot.Time.Format(global.TimeFormat) == metadata.Time, "New snapshot should have time %s", metadata.Time)
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata.Hostname != "" {
|
if metadata.Hostname != "" {
|
||||||
@@ -145,30 +196,158 @@ func TestRewriteSnaphotSummary(t *testing.T) {
|
|||||||
defer cleanup()
|
defer cleanup()
|
||||||
createBasicRewriteRepo(t, env)
|
createBasicRewriteRepo(t, env)
|
||||||
|
|
||||||
rtest.OK(t, runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, env.gopts, []string{}))
|
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, gopts, []string{}, gopts.Term)
|
||||||
|
}))
|
||||||
// no new snapshot should be created as the snapshot already has a summary
|
// no new snapshot should be created as the snapshot already has a summary
|
||||||
snapshots := testListSnapshots(t, env.gopts, 1)
|
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
// replace snapshot by one without a summary
|
// replace snapshot by one without a summary
|
||||||
_, repo, unlock, err := openWithExclusiveLock(context.TODO(), env.gopts, false)
|
var oldSummary *data.SnapshotSummary
|
||||||
|
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 := openWithExclusiveLock(ctx, gopts, false, printer)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
sn, err := restic.LoadSnapshot(context.TODO(), repo, snapshots[0])
|
defer unlock()
|
||||||
|
|
||||||
|
sn, err := data.LoadSnapshot(ctx, repo, snapshots[0])
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
oldSummary := sn.Summary
|
oldSummary = sn.Summary
|
||||||
sn.Summary = nil
|
sn.Summary = nil
|
||||||
rtest.OK(t, repo.RemoveUnpacked(context.TODO(), restic.WriteableSnapshotFile, snapshots[0]))
|
rtest.OK(t, repo.RemoveUnpacked(ctx, restic.WriteableSnapshotFile, snapshots[0]))
|
||||||
snapshots[0], err = restic.SaveSnapshot(context.TODO(), repo, sn)
|
snapshots[0], err = data.SaveSnapshot(ctx, repo, sn)
|
||||||
|
return err
|
||||||
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
unlock()
|
|
||||||
|
|
||||||
// rewrite snapshot and lookup ID of new snapshot
|
// rewrite snapshot and lookup ID of new snapshot
|
||||||
rtest.OK(t, runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, env.gopts, []string{}))
|
rtest.OK(t, withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runRewrite(context.TODO(), RewriteOptions{SnapshotSummary: true}, gopts, []string{}, gopts.Term)
|
||||||
|
}))
|
||||||
newSnapshots := testListSnapshots(t, env.gopts, 2)
|
newSnapshots := testListSnapshots(t, env.gopts, 2)
|
||||||
newSnapshot := restic.NewIDSet(newSnapshots...).Sub(restic.NewIDSet(snapshots...)).List()[0]
|
newSnapshot := restic.NewIDSet(newSnapshots...).Sub(restic.NewIDSet(snapshots...)).List()[0]
|
||||||
|
|
||||||
sn, err = restic.LoadSnapshot(context.TODO(), repo, newSnapshot)
|
newSn := testLoadSnapshot(t, env.gopts, newSnapshot)
|
||||||
rtest.OK(t, err)
|
rtest.Assert(t, newSn.Summary != nil, "snapshot should have summary attached")
|
||||||
rtest.Assert(t, sn.Summary != nil, "snapshot should have summary attached")
|
rtest.Equals(t, oldSummary.TotalBytesProcessed, newSn.Summary.TotalBytesProcessed, "unexpected TotalBytesProcessed value")
|
||||||
rtest.Equals(t, oldSummary.TotalBytesProcessed, sn.Summary.TotalBytesProcessed, "unexpected TotalBytesProcessed value")
|
rtest.Equals(t, oldSummary.TotalFilesProcessed, newSn.Summary.TotalFilesProcessed, "unexpected TotalFilesProcessed value")
|
||||||
rtest.Equals(t, oldSummary.TotalFilesProcessed, sn.Summary.TotalFilesProcessed, "unexpected TotalFilesProcessed value")
|
}
|
||||||
|
|
||||||
|
func TestRewriteInclude(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
opts RewriteOptions
|
||||||
|
lsSubstring string
|
||||||
|
lsExpectedCount int
|
||||||
|
summaryFilesExpected uint
|
||||||
|
}{
|
||||||
|
{"relative", RewriteOptions{
|
||||||
|
Forget: true,
|
||||||
|
IncludePatternOptions: filter.IncludePatternOptions{Includes: []string{"*.txt"}},
|
||||||
|
}, ".txt", 2, 2},
|
||||||
|
{"absolute", RewriteOptions{
|
||||||
|
Forget: true,
|
||||||
|
// test that childMatches are working by only matching a subdirectory
|
||||||
|
IncludePatternOptions: filter.IncludePatternOptions{Includes: []string{"/testdata/0/for_cmd_ls"}},
|
||||||
|
}, "/testdata/0", 5, 3},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
createBasicRewriteRepo(t, env)
|
||||||
|
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
|
rtest.OK(t, testRunRewriteWithOpts(t, tc.opts, env.gopts, []string{"latest"}))
|
||||||
|
|
||||||
|
newSnapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
rtest.Assert(t, snapshots[0] != newSnapshots[0], "snapshot id should have changed")
|
||||||
|
|
||||||
|
testLsOutputContainsCount(t, env.gopts, LsOptions{}, []string{"latest"}, tc.lsSubstring, tc.lsExpectedCount)
|
||||||
|
sn := testLoadSnapshot(t, env.gopts, newSnapshots[0])
|
||||||
|
rtest.Assert(t, sn.Summary != nil, "snapshot should have a summary attached")
|
||||||
|
rtest.Assert(t, sn.Summary.TotalFilesProcessed == tc.summaryFilesExpected,
|
||||||
|
"there should be %d files in the snapshot, but there are %d files", tc.summaryFilesExpected, sn.Summary.TotalFilesProcessed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteExcludeFiles(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
createBasicRewriteRepo(t, env)
|
||||||
|
snapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
|
// exclude txt files
|
||||||
|
err := testRunRewriteWithOpts(t,
|
||||||
|
RewriteOptions{
|
||||||
|
Forget: true,
|
||||||
|
ExcludePatternOptions: filter.ExcludePatternOptions{Excludes: []string{"*.txt"}},
|
||||||
|
},
|
||||||
|
env.gopts,
|
||||||
|
[]string{"latest"})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
newSnapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
rtest.Assert(t, snapshots[0] != newSnapshots[0], "snapshot id should have changed")
|
||||||
|
|
||||||
|
testLsOutputContainsCount(t, env.gopts, LsOptions{}, []string{"latest"}, ".txt", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteExcludeIncludeContradiction(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
testRunInit(t, env.gopts)
|
||||||
|
|
||||||
|
// test contradiction
|
||||||
|
err := withTermStatus(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
return runRewrite(ctx,
|
||||||
|
RewriteOptions{
|
||||||
|
ExcludePatternOptions: filter.ExcludePatternOptions{Excludes: []string{"nonsense"}},
|
||||||
|
IncludePatternOptions: filter.IncludePatternOptions{Includes: []string{"not allowed"}},
|
||||||
|
},
|
||||||
|
gopts, []string{"quack"}, env.gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.Assert(t, err != nil && strings.Contains(err.Error(), "exclude and include patterns are mutually exclusive"), `expected to fail command with message "exclude and include patterns are mutually exclusive"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRewriteIncludeEmptyDirectory(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
snapIDEmpty := createBasicRewriteRepoWithEmptyDirectory(t, env)
|
||||||
|
|
||||||
|
// restic rewrite <snapshots[0]> -i empty-directory --forget
|
||||||
|
// exclude txt files
|
||||||
|
err := testRunRewriteWithOpts(t,
|
||||||
|
RewriteOptions{
|
||||||
|
Forget: true,
|
||||||
|
IncludePatternOptions: filter.IncludePatternOptions{Includes: []string{"empty-directory"}},
|
||||||
|
},
|
||||||
|
env.gopts,
|
||||||
|
[]string{"latest"})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
newSnapshots := testListSnapshots(t, env.gopts, 1)
|
||||||
|
rtest.Assert(t, snapIDEmpty != newSnapshots[0], "snapshot id should have changed")
|
||||||
|
|
||||||
|
testLsOutputContainsCount(t, env.gopts, LsOptions{}, []string{"latest"}, "empty-directory", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRewriteIncludeNothing makes sure when nothing is included, the original snapshot stays untouched
|
||||||
|
func TestRewriteIncludeNothing(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
createBasicRewriteRepo(t, env)
|
||||||
|
snapsBefore := testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
|
// restic rewrite latest -i nothing-whatsoever --forget
|
||||||
|
err := testRunRewriteWithOpts(t,
|
||||||
|
RewriteOptions{
|
||||||
|
Forget: true,
|
||||||
|
IncludePatternOptions: filter.IncludePatternOptions{Includes: []string{"nothing-whatsoever"}},
|
||||||
|
},
|
||||||
|
env.gopts,
|
||||||
|
[]string{"latest"})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
|
||||||
|
snapsAfter := testListSnapshots(t, env.gopts, 1)
|
||||||
|
rtest.Assert(t, snapsBefore[0] == snapsAfter[0], "snapshots should be identical but are %s and %s",
|
||||||
|
snapsBefore[0].Str(), snapsAfter[0].Str())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,18 +8,20 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
"github.com/restic/restic/internal/selfupdate"
|
"github.com/restic/restic/internal/selfupdate"
|
||||||
|
"github.com/restic/restic/internal/ui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerSelfUpdateCommand(cmd *cobra.Command) {
|
func registerSelfUpdateCommand(cmd *cobra.Command, globalOptions *global.Options) {
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newSelfUpdateCommand(),
|
newSelfUpdateCommand(globalOptions),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSelfUpdateCommand() *cobra.Command {
|
func newSelfUpdateCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts SelfUpdateOptions
|
var opts SelfUpdateOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -42,7 +44,7 @@ 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 runSelfUpdate(cmd.Context(), opts, globalOptions, args)
|
return runSelfUpdate(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ func (opts *SelfUpdateOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
f.StringVar(&opts.Output, "output", "", "Save the downloaded file as `filename` (default: running binary itself)")
|
f.StringVar(&opts.Output, "output", "", "Save the downloaded file as `filename` (default: running binary itself)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts GlobalOptions, args []string) error {
|
func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
if opts.Output == "" {
|
if opts.Output == "" {
|
||||||
file, err := os.Executable()
|
file, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,15 +87,16 @@ func runSelfUpdate(ctx context.Context, opts SelfUpdateOptions, gopts GlobalOpti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Verbosef("writing restic to %v\n", opts.Output)
|
printer := ui.NewProgressPrinter(false, gopts.Verbosity, term)
|
||||||
|
printer.P("writing restic to %v", opts.Output)
|
||||||
|
|
||||||
v, err := selfupdate.DownloadLatestStableRelease(ctx, opts.Output, version, Verbosef)
|
v, err := selfupdate.DownloadLatestStableRelease(ctx, opts.Output, global.Version, printer.P)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Fatalf("unable to update restic: %v", err)
|
return errors.Fatalf("unable to update restic: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v != version {
|
if v != global.Version {
|
||||||
Printf("successfully updated restic to version %v\n", v)
|
printer.S("successfully updated restic to version %v", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
import (
|
||||||
|
"github.com/restic/restic/internal/global"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
func registerSelfUpdateCommand(_ *cobra.Command) {
|
func registerSelfUpdateCommand(_ *cobra.Command, _ *global.Options) {
|
||||||
// No commands to register in non-selfupdate mode
|
// No commands to register in non-selfupdate mode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
|
"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/restic/restic/internal/ui/table"
|
"github.com/restic/restic/internal/ui/table"
|
||||||
@@ -15,7 +18,7 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newSnapshotsCommand() *cobra.Command {
|
func newSnapshotsCommand(globalOptions *global.Options) *cobra.Command {
|
||||||
var opts SnapshotOptions
|
var opts SnapshotOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -36,7 +39,8 @@ 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 runSnapshots(cmd.Context(), opts, globalOptions, args)
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
return runSnapshots(cmd.Context(), opts, *globalOptions, args, globalOptions.Term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,11 +50,11 @@ Exit status is 12 if the password is incorrect.
|
|||||||
|
|
||||||
// SnapshotOptions bundles all options for the snapshots command.
|
// SnapshotOptions bundles all options for the snapshots command.
|
||||||
type SnapshotOptions struct {
|
type SnapshotOptions struct {
|
||||||
restic.SnapshotFilter
|
data.SnapshotFilter
|
||||||
Compact bool
|
Compact bool
|
||||||
Last bool // This option should be removed in favour of Latest.
|
Last bool // This option should be removed in favour of Latest.
|
||||||
Latest int
|
Latest int
|
||||||
GroupBy restic.SnapshotGroupByOptions
|
GroupBy data.SnapshotGroupByOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *SnapshotOptions) AddFlags(f *pflag.FlagSet) {
|
func (opts *SnapshotOptions) AddFlags(f *pflag.FlagSet) {
|
||||||
@@ -66,21 +70,22 @@ func (opts *SnapshotOptions) AddFlags(f *pflag.FlagSet) {
|
|||||||
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma")
|
f.VarP(&opts.GroupBy, "group-by", "g", "`group` snapshots by host, paths and/or tags, separated by comma")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions, args []string) error {
|
func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts global.Options, args []string, term ui.Terminal) error {
|
||||||
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
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
var snapshots restic.Snapshots
|
var snapshots data.Snapshots
|
||||||
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args) {
|
for sn := range FindFilteredSnapshots(ctx, repo, repo, &opts.SnapshotFilter, args, printer) {
|
||||||
snapshots = append(snapshots, sn)
|
snapshots = append(snapshots, sn)
|
||||||
}
|
}
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
snapshotGroups, grouped, err := restic.GroupSnapshots(snapshots, opts.GroupBy)
|
snapshotGroups, grouped, err := data.GroupSnapshots(snapshots, opts.GroupBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -93,18 +98,18 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions
|
|||||||
if opts.Last {
|
if opts.Last {
|
||||||
// This branch should be removed in the same time
|
// This branch should be removed in the same time
|
||||||
// that --last.
|
// that --last.
|
||||||
list = FilterLatestSnapshots(list, 1)
|
list = filterLatestSnapshotsInGroup(list, 1)
|
||||||
} else if opts.Latest > 0 {
|
} else if opts.Latest > 0 {
|
||||||
list = FilterLatestSnapshots(list, opts.Latest)
|
list = filterLatestSnapshotsInGroup(list, opts.Latest)
|
||||||
}
|
}
|
||||||
sort.Sort(sort.Reverse(list))
|
sort.Sort(sort.Reverse(list))
|
||||||
snapshotGroups[k] = list
|
snapshotGroups[k] = list
|
||||||
}
|
}
|
||||||
|
|
||||||
if gopts.JSON {
|
if gopts.JSON {
|
||||||
err := printSnapshotGroupJSON(globalOptions.stdout, snapshotGroups, grouped)
|
err := printSnapshotGroupJSON(gopts.Term.OutputWriter(), snapshotGroups, grouped)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error printing snapshots: %v\n", err)
|
printer.E("error printing snapshots: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -115,60 +120,37 @@ func runSnapshots(ctx context.Context, opts SnapshotOptions, gopts GlobalOptions
|
|||||||
}
|
}
|
||||||
|
|
||||||
if grouped {
|
if grouped {
|
||||||
err := PrintSnapshotGroupHeader(globalOptions.stdout, k)
|
err := PrintSnapshotGroupHeader(gopts.Term.OutputWriter(), k)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Warnf("error printing snapshots: %v\n", err)
|
return err
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PrintSnapshots(globalOptions.stdout, list, nil, opts.Compact)
|
err := PrintSnapshots(gopts.Term.OutputWriter(), list, nil, opts.Compact)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterLastSnapshotsKey is used by FilterLastSnapshots.
|
// filterLatestSnapshotsInGroup filters a list of snapshots to only return
|
||||||
type filterLastSnapshotsKey struct {
|
// the `limit` last entries. It is assumed that the snapshot list only contains
|
||||||
Hostname string
|
// one group of snapshots.
|
||||||
JoinedPaths string
|
func filterLatestSnapshotsInGroup(list data.Snapshots, limit int) data.Snapshots {
|
||||||
}
|
|
||||||
|
|
||||||
// newFilterLastSnapshotsKey initializes a filterLastSnapshotsKey from a Snapshot
|
|
||||||
func newFilterLastSnapshotsKey(sn *restic.Snapshot) filterLastSnapshotsKey {
|
|
||||||
// Shallow slice copy
|
|
||||||
var paths = make([]string, len(sn.Paths))
|
|
||||||
copy(paths, sn.Paths)
|
|
||||||
sort.Strings(paths)
|
|
||||||
return filterLastSnapshotsKey{sn.Hostname, strings.Join(paths, "|")}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterLatestSnapshots filters a list of snapshots to only return
|
|
||||||
// the limit last entries for each hostname and path. If the snapshot
|
|
||||||
// contains multiple paths, they will be joined and treated as one
|
|
||||||
// item.
|
|
||||||
func FilterLatestSnapshots(list restic.Snapshots, limit int) restic.Snapshots {
|
|
||||||
// Sort the snapshots so that the newer ones are listed first
|
// Sort the snapshots so that the newer ones are listed first
|
||||||
sort.SliceStable(list, func(i, j int) bool {
|
sort.SliceStable(list, func(i, j int) bool {
|
||||||
return list[i].Time.After(list[j].Time)
|
return list[i].Time.After(list[j].Time)
|
||||||
})
|
})
|
||||||
|
|
||||||
var results restic.Snapshots
|
return list[:min(limit, len(list))]
|
||||||
seen := make(map[filterLastSnapshotsKey]int)
|
|
||||||
for _, sn := range list {
|
|
||||||
key := newFilterLastSnapshotsKey(sn)
|
|
||||||
if seen[key] < limit {
|
|
||||||
seen[key]++
|
|
||||||
results = append(results, sn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintSnapshots prints a text table of the snapshots in list to stdout.
|
// PrintSnapshots prints a text table of the snapshots in list to stdout.
|
||||||
func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.KeepReason, compact bool) {
|
func PrintSnapshots(stdout io.Writer, list data.Snapshots, reasons []data.KeepReason, compact bool) error {
|
||||||
// keep the reasons a snasphot is being kept in a map, so that it doesn't
|
// keep the reasons a snasphot is being kept in a map, so that it doesn't
|
||||||
// get lost when the list of snapshots is sorted
|
// get lost when the list of snapshots is sorted
|
||||||
keepReasons := make(map[restic.ID]restic.KeepReason, len(reasons))
|
keepReasons := make(map[restic.ID]data.KeepReason, len(reasons))
|
||||||
if len(reasons) > 0 {
|
if len(reasons) > 0 {
|
||||||
for i, sn := range list {
|
for i, sn := range list {
|
||||||
id := sn.ID()
|
id := sn.ID()
|
||||||
@@ -237,7 +219,7 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
|
|||||||
for _, sn := range list {
|
for _, sn := range list {
|
||||||
data := snapshot{
|
data := snapshot{
|
||||||
ID: sn.ID().Str(),
|
ID: sn.ID().Str(),
|
||||||
Timestamp: sn.Time.Local().Format(TimeFormat),
|
Timestamp: sn.Time.Local().Format(global.TimeFormat),
|
||||||
Hostname: sn.Hostname,
|
Hostname: sn.Hostname,
|
||||||
Tags: sn.Tags,
|
Tags: sn.Tags,
|
||||||
Paths: sn.Paths,
|
Paths: sn.Paths,
|
||||||
@@ -259,7 +241,15 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
|
|||||||
tab.AddRow(data)
|
tab.AddRow(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
tab.AddFooter(fmt.Sprintf("%d snapshots", len(list)))
|
// Add timezone information to prevent confusion:
|
||||||
|
// Each snapshot can be registered in different timezones,
|
||||||
|
// but we display them all in local timezone on this output.
|
||||||
|
footer := fmt.Sprintf("%d snapshots", len(list))
|
||||||
|
zoneName, _ := time.Now().Local().Zone()
|
||||||
|
if zoneName != "" {
|
||||||
|
footer = fmt.Sprintf("Timestamps shown in %s timezone\n%s", zoneName, footer)
|
||||||
|
}
|
||||||
|
tab.AddFooter(footer)
|
||||||
|
|
||||||
if multiline {
|
if multiline {
|
||||||
// print an additional blank line between snapshots
|
// print an additional blank line between snapshots
|
||||||
@@ -277,17 +267,14 @@ func PrintSnapshots(stdout io.Writer, list restic.Snapshots, reasons []restic.Ke
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := tab.Write(stdout)
|
return tab.Write(stdout)
|
||||||
if err != nil {
|
|
||||||
Warnf("error printing: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintSnapshotGroupHeader prints which group of the group-by option the
|
// PrintSnapshotGroupHeader prints which group of the group-by option the
|
||||||
// following snapshots belong to.
|
// following snapshots belong to.
|
||||||
// Prints nothing, if we did not group at all.
|
// Prints nothing, if we did not group at all.
|
||||||
func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
|
func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
|
||||||
var key restic.SnapshotGroupKey
|
var key data.SnapshotGroupKey
|
||||||
|
|
||||||
err := json.Unmarshal([]byte(groupKeyJSON), &key)
|
err := json.Unmarshal([]byte(groupKeyJSON), &key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -299,9 +286,7 @@ func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Info
|
// Info
|
||||||
if _, err := fmt.Fprintf(stdout, "snapshots"); err != nil {
|
header := "snapshots"
|
||||||
return err
|
|
||||||
}
|
|
||||||
var infoStrings []string
|
var infoStrings []string
|
||||||
if key.Hostname != "" {
|
if key.Hostname != "" {
|
||||||
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
|
infoStrings = append(infoStrings, "host ["+key.Hostname+"]")
|
||||||
@@ -313,18 +298,16 @@ func PrintSnapshotGroupHeader(stdout io.Writer, groupKeyJSON string) error {
|
|||||||
infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
|
infoStrings = append(infoStrings, "paths ["+strings.Join(key.Paths, ", ")+"]")
|
||||||
}
|
}
|
||||||
if infoStrings != nil {
|
if infoStrings != nil {
|
||||||
if _, err := fmt.Fprintf(stdout, " for (%s)", strings.Join(infoStrings, ", ")); err != nil {
|
header += " for (" + strings.Join(infoStrings, ", ") + ")"
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
header += ":\n"
|
||||||
_, err = fmt.Fprintf(stdout, ":\n")
|
_, err = stdout.Write([]byte(header))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot helps to print Snapshots as JSON with their ID included.
|
// Snapshot helps to print Snapshots as JSON with their ID included.
|
||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
*restic.Snapshot
|
*data.Snapshot
|
||||||
|
|
||||||
ID *restic.ID `json:"id"`
|
ID *restic.ID `json:"id"`
|
||||||
ShortID string `json:"short_id"` // deprecated
|
ShortID string `json:"short_id"` // deprecated
|
||||||
@@ -332,17 +315,17 @@ type Snapshot struct {
|
|||||||
|
|
||||||
// SnapshotGroup helps to print SnapshotGroups as JSON with their GroupReasons included.
|
// SnapshotGroup helps to print SnapshotGroups as JSON with their GroupReasons included.
|
||||||
type SnapshotGroup struct {
|
type SnapshotGroup struct {
|
||||||
GroupKey restic.SnapshotGroupKey `json:"group_key"`
|
GroupKey data.SnapshotGroupKey `json:"group_key"`
|
||||||
Snapshots []Snapshot `json:"snapshots"`
|
Snapshots []Snapshot `json:"snapshots"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// printSnapshotGroupJSON writes the JSON representation of list to stdout.
|
// printSnapshotGroupJSON writes the JSON representation of list to stdout.
|
||||||
func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]restic.Snapshots, grouped bool) error {
|
func printSnapshotGroupJSON(stdout io.Writer, snGroups map[string]data.Snapshots, grouped bool) error {
|
||||||
if grouped {
|
if grouped {
|
||||||
snapshotGroups := []SnapshotGroup{}
|
snapshotGroups := []SnapshotGroup{}
|
||||||
|
|
||||||
for k, list := range snGroups {
|
for k, list := range snGroups {
|
||||||
var key restic.SnapshotGroupKey
|
var key data.SnapshotGroupKey
|
||||||
var err error
|
var err error
|
||||||
var snapshots []Snapshot
|
var snapshots []Snapshot
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,22 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/restic/restic/internal/data"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snapmap map[restic.ID]Snapshot) {
|
func testRunSnapshots(t testing.TB, gopts global.Options) (newest *Snapshot, snapmap map[restic.ID]Snapshot) {
|
||||||
buf, err := withCaptureStdout(func() error {
|
buf, err := withCaptureStdout(t, gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
gopts.JSON = true
|
gopts.JSON = true
|
||||||
|
|
||||||
opts := SnapshotOptions{}
|
opts := SnapshotOptions{}
|
||||||
return runSnapshots(context.TODO(), opts, gopts, []string{})
|
return runSnapshots(ctx, opts, gopts, []string{}, gopts.Term)
|
||||||
})
|
})
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
@@ -30,3 +34,34 @@ func testRunSnapshots(t testing.TB, gopts GlobalOptions) (newest *Snapshot, snap
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSnapshotsGroupByAndLatest(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
testSetupBackupData(t, env)
|
||||||
|
// two backups on the same host but with different paths
|
||||||
|
opts := BackupOptions{Host: "testhost", TimeStamp: time.Now().Format(time.DateTime)}
|
||||||
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
|
||||||
|
// Use later timestamp for second backup
|
||||||
|
opts.TimeStamp = time.Now().Add(time.Second).Format(time.DateTime)
|
||||||
|
snapshotsIDs := loadSnapshotMap(t, env.gopts)
|
||||||
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata/0"}, opts, env.gopts)
|
||||||
|
_, secondSnapshotID := lastSnapshot(snapshotsIDs, loadSnapshotMap(t, env.gopts))
|
||||||
|
|
||||||
|
buf, err := withCaptureStdout(t, env.gopts, func(ctx context.Context, gopts global.Options) error {
|
||||||
|
gopts.JSON = true
|
||||||
|
// only group by host but not path
|
||||||
|
opts := SnapshotOptions{GroupBy: data.SnapshotGroupByOptions{Host: true}, Latest: 1}
|
||||||
|
return runSnapshots(ctx, opts, gopts, []string{}, gopts.Term)
|
||||||
|
})
|
||||||
|
rtest.OK(t, err)
|
||||||
|
snapshots := []SnapshotGroup{}
|
||||||
|
rtest.OK(t, json.Unmarshal(buf.Bytes(), &snapshots))
|
||||||
|
rtest.Assert(t, len(snapshots) == 1, "expected only one snapshot group, got %d", len(snapshots))
|
||||||
|
rtest.Assert(t, snapshots[0].GroupKey.Hostname == "testhost", "expected group_key.hostname to be set to testhost, got %s", snapshots[0].GroupKey.Hostname)
|
||||||
|
rtest.Assert(t, snapshots[0].GroupKey.Paths == nil, "expected group_key.paths to be set to nil, got %s", snapshots[0].GroupKey.Paths)
|
||||||
|
rtest.Assert(t, snapshots[0].GroupKey.Tags == nil, "expected group_key.tags to be set to nil, got %s", snapshots[0].GroupKey.Tags)
|
||||||
|
rtest.Assert(t, len(snapshots[0].Snapshots) == 1, "expected only one latest snapshot, got %d", len(snapshots[0].Snapshots))
|
||||||
|
rtest.Equals(t, snapshots[0].Snapshots[0].ID.String(), secondSnapshotID, "unexpected snapshot ID")
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user