dycw-restic 0.2.22__py3-none-any.whl → 0.3.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dycw_restic-0.2.22.dist-info → dycw_restic-0.3.9.dist-info}/METADATA +4 -3
- dycw_restic-0.3.9.dist-info/RECORD +16 -0
- {dycw_restic-0.2.22.dist-info → dycw_restic-0.3.9.dist-info}/WHEEL +1 -1
- restic/__init__.py +1 -1
- restic/cli.py +21 -4
- restic/click.py +4 -16
- restic/lib.py +111 -27
- restic/models.py +43 -0
- restic/repo.py +150 -14
- restic/settings.py +35 -13
- restic/utilities.py +33 -7
- dycw_restic-0.2.22.dist-info/RECORD +0 -15
- {dycw_restic-0.2.22.dist-info → dycw_restic-0.3.9.dist-info}/entry_points.txt +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dycw-restic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: Library to operate `restic`
|
|
5
5
|
Author: Derek Wan
|
|
6
6
|
Author-email: Derek Wan <d.wan@icloud.com>
|
|
7
7
|
Requires-Dist: click>=8.3.1,<9
|
|
8
|
-
Requires-Dist: dycw-utilities>=0.
|
|
8
|
+
Requires-Dist: dycw-utilities>=0.176.3,<1
|
|
9
|
+
Requires-Dist: pydantic>=2.12.5,<3
|
|
9
10
|
Requires-Dist: typed-settings[attrs,click]>=25.3.0,<26
|
|
10
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.12
|
|
11
12
|
Description-Content-Type: text/markdown
|
|
12
13
|
|
|
13
14
|
# `restic`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
restic/__init__.py,sha256=1R3lI2sjBNAPTtK4HP7ZglqUphNcoj7YV8YvqS_AsVE,58
|
|
2
|
+
restic/cli.py,sha256=TK2SGlaxCusUrm0TEQ7TpOtgNeXo_18UmHhx4CcvoLQ,6035
|
|
3
|
+
restic/click.py,sha256=LsM3wEHablcV7vsdAHJAgde4l8mikOW7MHy67ICeJ90,953
|
|
4
|
+
restic/constants.py,sha256=uu2dXIlx3vyMNgJUs1lrCMOYBPGGUviVJ5qvxqOx6Tw,911
|
|
5
|
+
restic/lib.py,sha256=vGkoeSoqpV9q5RpS6EA4JxZ8v314aihH0kRzUFNX4fk,12875
|
|
6
|
+
restic/logging.py,sha256=tzoiz1F2T-AavxahNduXxfnFvhmcBh6JHskDgUhcNVU,119
|
|
7
|
+
restic/models.py,sha256=UEVhXzXTaoHxrOmXR9-RSaxuC0Q_AH4RWwFfCrWL3ek,805
|
|
8
|
+
restic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
restic/repo.py,sha256=wzSvqtxXvlYg_3Fjv7xc3-H_sGxwHcCPVXM_Z3cTK_E,6870
|
|
10
|
+
restic/settings.py,sha256=TJraIkjJVDzu3ReuQIXKVry1Nwn2YR1uWNT4RYzKvsU,13588
|
|
11
|
+
restic/types.py,sha256=lCchCT7Y7yo0PMBGHgYjpuTDf9z2orPjKiPuje1vfmE,229
|
|
12
|
+
restic/utilities.py,sha256=PROHOEqEm5KM9p9LoSggVMd5kxX2-nyuAMS_YBcU4QY,3690
|
|
13
|
+
dycw_restic-0.3.9.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
|
|
14
|
+
dycw_restic-0.3.9.dist-info/entry_points.txt,sha256=6byn-7KAIlNAdTjaNQ3DjylzzazsZB8SLFvvrqv641o,48
|
|
15
|
+
dycw_restic-0.3.9.dist-info/METADATA,sha256=kkeyYE1-GUITJGBJQX7oxXosW63OTXAPaRkSQfYx38M,421
|
|
16
|
+
dycw_restic-0.3.9.dist-info/RECORD,,
|
restic/__init__.py
CHANGED
restic/cli.py
CHANGED
|
@@ -12,7 +12,7 @@ from utilities.os import is_pytest
|
|
|
12
12
|
|
|
13
13
|
import restic.click
|
|
14
14
|
import restic.repo
|
|
15
|
-
from restic.lib import backup, copy, forget, init, restore, snapshots
|
|
15
|
+
from restic.lib import backup, copy, forget, init, restore, snapshots, unlock
|
|
16
16
|
from restic.logging import LOGGER
|
|
17
17
|
from restic.settings import (
|
|
18
18
|
LOADERS,
|
|
@@ -22,6 +22,7 @@ from restic.settings import (
|
|
|
22
22
|
InitSettings,
|
|
23
23
|
RestoreSettings,
|
|
24
24
|
SnapshotsSettings,
|
|
25
|
+
UnlockSettings,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
if TYPE_CHECKING:
|
|
@@ -38,6 +39,7 @@ def _main() -> None: ...
|
|
|
38
39
|
def init_sub_cmd(settings: InitSettings, /, *, repo: restic.repo.Repo) -> None:
|
|
39
40
|
if is_pytest():
|
|
40
41
|
return
|
|
42
|
+
basic_config(obj=LOGGER)
|
|
41
43
|
init(repo, password=settings.password)
|
|
42
44
|
|
|
43
45
|
|
|
@@ -50,11 +52,10 @@ def backup_sub_cmd(
|
|
|
50
52
|
) -> None:
|
|
51
53
|
if is_pytest():
|
|
52
54
|
return
|
|
55
|
+
basic_config(obj=LOGGER)
|
|
53
56
|
backup(
|
|
54
57
|
path,
|
|
55
58
|
repo,
|
|
56
|
-
chmod=settings.chmod,
|
|
57
|
-
chown=settings.chown,
|
|
58
59
|
password=settings.password,
|
|
59
60
|
dry_run=settings.dry_run,
|
|
60
61
|
exclude=settings.exclude,
|
|
@@ -74,6 +75,7 @@ def backup_sub_cmd(
|
|
|
74
75
|
keep_within_weekly=settings.keep_within_weekly,
|
|
75
76
|
keep_within_monthly=settings.keep_within_monthly,
|
|
76
77
|
keep_within_yearly=settings.keep_within_yearly,
|
|
78
|
+
group_by=settings.group_by,
|
|
77
79
|
prune=settings.prune,
|
|
78
80
|
repack_cacheable_only=settings.repack_cacheable_only,
|
|
79
81
|
repack_small=settings.repack_small,
|
|
@@ -92,12 +94,14 @@ def copy_sub_cmd(
|
|
|
92
94
|
) -> None:
|
|
93
95
|
if is_pytest():
|
|
94
96
|
return
|
|
97
|
+
basic_config(obj=LOGGER)
|
|
95
98
|
copy(
|
|
96
99
|
src,
|
|
97
100
|
dest,
|
|
98
101
|
src_password=settings.src_password,
|
|
99
102
|
dest_password=settings.dest_password,
|
|
100
103
|
tag=settings.tag,
|
|
104
|
+
sleep=settings.sleep,
|
|
101
105
|
)
|
|
102
106
|
|
|
103
107
|
|
|
@@ -107,6 +111,7 @@ def copy_sub_cmd(
|
|
|
107
111
|
def forget_sub_cmd(settings: ForgetSettings, /, *, repo: restic.repo.Repo) -> None:
|
|
108
112
|
if is_pytest():
|
|
109
113
|
return
|
|
114
|
+
basic_config(obj=LOGGER)
|
|
110
115
|
forget(
|
|
111
116
|
repo,
|
|
112
117
|
password=settings.password,
|
|
@@ -123,6 +128,7 @@ def forget_sub_cmd(settings: ForgetSettings, /, *, repo: restic.repo.Repo) -> No
|
|
|
123
128
|
keep_within_weekly=settings.keep_within_weekly,
|
|
124
129
|
keep_within_monthly=settings.keep_within_monthly,
|
|
125
130
|
keep_within_yearly=settings.keep_within_yearly,
|
|
131
|
+
group_by=settings.group_by,
|
|
126
132
|
prune=settings.prune,
|
|
127
133
|
repack_cacheable_only=settings.repack_cacheable_only,
|
|
128
134
|
repack_small=settings.repack_small,
|
|
@@ -131,6 +137,16 @@ def forget_sub_cmd(settings: ForgetSettings, /, *, repo: restic.repo.Repo) -> No
|
|
|
131
137
|
)
|
|
132
138
|
|
|
133
139
|
|
|
140
|
+
@_main.command(name="unlock", **CONTEXT_SETTINGS)
|
|
141
|
+
@argument("repo", type=restic.click.Repo())
|
|
142
|
+
@click_options(UnlockSettings, LOADERS, show_envvars_in_help=True)
|
|
143
|
+
def unlock_sub_cmd(settings: UnlockSettings, /, *, repo: restic.repo.Repo) -> None:
|
|
144
|
+
if is_pytest():
|
|
145
|
+
return
|
|
146
|
+
basic_config(obj=LOGGER)
|
|
147
|
+
unlock(repo, password=settings.password, remove_all=settings.remove_all)
|
|
148
|
+
|
|
149
|
+
|
|
134
150
|
@_main.command(name="restore", **CONTEXT_SETTINGS)
|
|
135
151
|
@argument("repo", type=restic.click.Repo())
|
|
136
152
|
@argument("target", type=click.Path(path_type=Path))
|
|
@@ -140,6 +156,7 @@ def restore_sub_cmd(
|
|
|
140
156
|
) -> None:
|
|
141
157
|
if is_pytest():
|
|
142
158
|
return
|
|
159
|
+
basic_config(obj=LOGGER)
|
|
143
160
|
restore(
|
|
144
161
|
repo,
|
|
145
162
|
target,
|
|
@@ -163,9 +180,9 @@ def snapshots_sub_cmd(
|
|
|
163
180
|
) -> None:
|
|
164
181
|
if is_pytest():
|
|
165
182
|
return
|
|
183
|
+
basic_config(obj=LOGGER)
|
|
166
184
|
snapshots(repo, password=settings.password)
|
|
167
185
|
|
|
168
186
|
|
|
169
187
|
if __name__ == "__main__":
|
|
170
|
-
basic_config(obj=LOGGER)
|
|
171
188
|
_main()
|
restic/click.py
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from contextlib import suppress
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from re import search
|
|
6
3
|
from typing import TYPE_CHECKING, assert_never, override
|
|
7
4
|
|
|
8
5
|
from click import Context, Parameter, ParamType
|
|
9
|
-
from utilities.re import ExtractGroupError, ExtractGroupsError
|
|
10
6
|
|
|
11
|
-
from restic.repo import SFTP, Backblaze, Local
|
|
7
|
+
from restic.repo import SFTP, Backblaze, Local, ParseRepoBackblazeError, parse_repo
|
|
12
8
|
|
|
13
9
|
if TYPE_CHECKING:
|
|
14
10
|
import restic.repo
|
|
@@ -33,17 +29,9 @@ class Repo(ParamType):
|
|
|
33
29
|
return value
|
|
34
30
|
case str():
|
|
35
31
|
try:
|
|
36
|
-
return
|
|
37
|
-
except
|
|
38
|
-
|
|
39
|
-
message = f"For a Backblaze repository {value!r}, the environment varaibles 'BACKBLAZE_KEY_ID' and 'BACKBLAZE_APPLICATION_KEY' must be defined"
|
|
40
|
-
return self.fail(message, param, ctx)
|
|
41
|
-
with suppress(ExtractGroupsError):
|
|
42
|
-
return SFTP.parse(value)
|
|
43
|
-
try:
|
|
44
|
-
return Local.parse(value)
|
|
45
|
-
except ExtractGroupError:
|
|
46
|
-
return Local(Path(value))
|
|
32
|
+
return parse_repo(value)
|
|
33
|
+
except ParseRepoBackblazeError as error:
|
|
34
|
+
return self.fail(str(error), param, ctx)
|
|
47
35
|
case never:
|
|
48
36
|
assert_never(never)
|
|
49
37
|
|
restic/lib.py
CHANGED
|
@@ -3,12 +3,16 @@ from __future__ import annotations
|
|
|
3
3
|
import time
|
|
4
4
|
from re import MULTILINE, search
|
|
5
5
|
from subprocess import CalledProcessError
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING, Literal, overload
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from pydantic import TypeAdapter
|
|
9
|
+
from utilities.iterables import one
|
|
10
|
+
from utilities.subprocess import cp, run
|
|
11
|
+
from utilities.tempfile import TemporaryDirectory
|
|
9
12
|
from whenever import TimeDelta
|
|
10
13
|
|
|
11
14
|
from restic.logging import LOGGER
|
|
15
|
+
from restic.models import Snapshot
|
|
12
16
|
from restic.repo import yield_repo_env
|
|
13
17
|
from restic.settings import SETTINGS
|
|
14
18
|
from restic.utilities import (
|
|
@@ -16,12 +20,14 @@ from restic.utilities import (
|
|
|
16
20
|
expand_dry_run,
|
|
17
21
|
expand_exclude,
|
|
18
22
|
expand_exclude_i,
|
|
23
|
+
expand_group_by,
|
|
19
24
|
expand_include,
|
|
20
25
|
expand_include_i,
|
|
21
26
|
expand_keep,
|
|
22
27
|
expand_keep_within,
|
|
28
|
+
expand_read_concurrency,
|
|
23
29
|
expand_tag,
|
|
24
|
-
|
|
30
|
+
expand_target,
|
|
25
31
|
yield_password,
|
|
26
32
|
)
|
|
27
33
|
|
|
@@ -37,8 +43,6 @@ def backup(
|
|
|
37
43
|
repo: Repo,
|
|
38
44
|
/,
|
|
39
45
|
*,
|
|
40
|
-
chmod: bool = SETTINGS.chmod,
|
|
41
|
-
chown: str | None = SETTINGS.chown,
|
|
42
46
|
password: PasswordLike = SETTINGS.password,
|
|
43
47
|
dry_run: bool = SETTINGS.dry_run,
|
|
44
48
|
exclude: list[str] | None = SETTINGS.exclude_backup,
|
|
@@ -58,6 +62,7 @@ def backup(
|
|
|
58
62
|
keep_within_weekly: str | None = SETTINGS.keep_within_weekly,
|
|
59
63
|
keep_within_monthly: str | None = SETTINGS.keep_within_monthly,
|
|
60
64
|
keep_within_yearly: str | None = SETTINGS.keep_within_yearly,
|
|
65
|
+
group_by: list[str] | None = SETTINGS.group_by,
|
|
61
66
|
prune: bool = SETTINGS.prune,
|
|
62
67
|
repack_cacheable_only: bool = SETTINGS.repack_cacheable_only,
|
|
63
68
|
repack_small: bool = SETTINGS.repack_small,
|
|
@@ -66,11 +71,6 @@ def backup(
|
|
|
66
71
|
sleep: int | None = SETTINGS.sleep,
|
|
67
72
|
) -> None:
|
|
68
73
|
LOGGER.info("Backing up '%s' to '%s'...", path, repo)
|
|
69
|
-
if chmod:
|
|
70
|
-
run_chmod(path, "d", "u=rwx,g=rx,o=rx")
|
|
71
|
-
run_chmod(path, "f", "u=rw,g=r,o=r")
|
|
72
|
-
if chown is not None:
|
|
73
|
-
run("sudo", "chown", "-R", f"{chown}:{chown}", str(path))
|
|
74
74
|
try:
|
|
75
75
|
_backup_core(
|
|
76
76
|
path,
|
|
@@ -102,7 +102,20 @@ def backup(
|
|
|
102
102
|
)
|
|
103
103
|
else:
|
|
104
104
|
raise
|
|
105
|
-
if run_forget
|
|
105
|
+
if run_forget and (
|
|
106
|
+
(keep_last is not None)
|
|
107
|
+
or (keep_hourly is not None)
|
|
108
|
+
or (keep_daily is not None)
|
|
109
|
+
or (keep_weekly is not None)
|
|
110
|
+
or (keep_monthly is not None)
|
|
111
|
+
or (keep_yearly is not None)
|
|
112
|
+
or (keep_within is not None)
|
|
113
|
+
or (keep_within_hourly is not None)
|
|
114
|
+
or (keep_within_daily is not None)
|
|
115
|
+
or (keep_within_weekly is not None)
|
|
116
|
+
or (keep_within_monthly is not None)
|
|
117
|
+
or (keep_within_yearly is not None)
|
|
118
|
+
):
|
|
106
119
|
forget(
|
|
107
120
|
repo,
|
|
108
121
|
password=password,
|
|
@@ -118,6 +131,7 @@ def backup(
|
|
|
118
131
|
keep_within_weekly=keep_within_weekly,
|
|
119
132
|
keep_within_monthly=keep_within_monthly,
|
|
120
133
|
keep_within_yearly=keep_within_yearly,
|
|
134
|
+
group_by=group_by,
|
|
121
135
|
prune=prune,
|
|
122
136
|
repack_cacheable_only=repack_cacheable_only,
|
|
123
137
|
repack_small=repack_small,
|
|
@@ -154,21 +168,13 @@ def _backup_core(
|
|
|
154
168
|
*expand_dry_run(dry_run=dry_run),
|
|
155
169
|
*expand_exclude(exclude=exclude),
|
|
156
170
|
*expand_exclude_i(exclude_i=exclude_i),
|
|
157
|
-
|
|
158
|
-
str(read_concurrency),
|
|
171
|
+
*expand_read_concurrency(read_concurrency),
|
|
159
172
|
*expand_tag(tag=tag),
|
|
160
173
|
str(path),
|
|
161
174
|
print=True,
|
|
162
175
|
)
|
|
163
176
|
|
|
164
177
|
|
|
165
|
-
def init(repo: Repo, /, *, password: PasswordLike = SETTINGS.password) -> None:
|
|
166
|
-
LOGGER.info("Initializing '%s'", repo)
|
|
167
|
-
with yield_repo_env(repo), yield_password(password=password):
|
|
168
|
-
run("restic", "init", print=True)
|
|
169
|
-
LOGGER.info("Finished initializing '%s'", repo)
|
|
170
|
-
|
|
171
|
-
|
|
172
178
|
def copy(
|
|
173
179
|
src: Repo,
|
|
174
180
|
dest: Repo,
|
|
@@ -177,6 +183,7 @@ def copy(
|
|
|
177
183
|
src_password: PasswordLike = SETTINGS.password,
|
|
178
184
|
dest_password: PasswordLike = SETTINGS.password,
|
|
179
185
|
tag: list[str] | None = SETTINGS.tag_copy,
|
|
186
|
+
sleep: int | None = SETTINGS.sleep,
|
|
180
187
|
) -> None:
|
|
181
188
|
LOGGER.info("Copying snapshots from '%s' to '%s'...", src, dest)
|
|
182
189
|
with (
|
|
@@ -186,7 +193,15 @@ def copy(
|
|
|
186
193
|
yield_password(password=dest_password),
|
|
187
194
|
):
|
|
188
195
|
run("restic", "copy", *expand_tag(tag=tag), print=True)
|
|
189
|
-
|
|
196
|
+
if sleep is None:
|
|
197
|
+
LOGGER.info("Finished copying snapshots from '%s' to '%s'", src, dest)
|
|
198
|
+
else:
|
|
199
|
+
delta = TimeDelta(seconds=sleep)
|
|
200
|
+
LOGGER.info(
|
|
201
|
+
"Finished copying snapshots from '%s' to '%s'; sleeping for %s...", delta
|
|
202
|
+
)
|
|
203
|
+
time.sleep(sleep)
|
|
204
|
+
LOGGER.info("Finishing sleeping for %s", delta)
|
|
190
205
|
|
|
191
206
|
|
|
192
207
|
def forget(
|
|
@@ -207,6 +222,7 @@ def forget(
|
|
|
207
222
|
keep_within_weekly: str | None = SETTINGS.keep_within_weekly,
|
|
208
223
|
keep_within_monthly: str | None = SETTINGS.keep_within_monthly,
|
|
209
224
|
keep_within_yearly: str | None = SETTINGS.keep_within_yearly,
|
|
225
|
+
group_by: list[str] | None = SETTINGS.group_by,
|
|
210
226
|
prune: bool = SETTINGS.prune,
|
|
211
227
|
repack_cacheable_only: bool = SETTINGS.repack_cacheable_only,
|
|
212
228
|
repack_small: bool = SETTINGS.repack_small,
|
|
@@ -231,6 +247,7 @@ def forget(
|
|
|
231
247
|
*expand_keep_within("within-weekly", duration=keep_within_weekly),
|
|
232
248
|
*expand_keep_within("within-monthly", duration=keep_within_monthly),
|
|
233
249
|
*expand_keep_within("within-yearly", duration=keep_within_yearly),
|
|
250
|
+
*expand_group_by(group_by=group_by),
|
|
234
251
|
*expand_bool("prune", bool_=prune),
|
|
235
252
|
*expand_bool("repack-cacheable-only", bool_=repack_cacheable_only),
|
|
236
253
|
*expand_bool("repack-small", bool_=repack_small),
|
|
@@ -241,6 +258,28 @@ def forget(
|
|
|
241
258
|
LOGGER.info("Finished forgetting snapshots in '%s'", repo)
|
|
242
259
|
|
|
243
260
|
|
|
261
|
+
def init(repo: Repo, /, *, password: PasswordLike = SETTINGS.password) -> None:
|
|
262
|
+
LOGGER.info("Initializing '%s'", repo)
|
|
263
|
+
with yield_repo_env(repo), yield_password(password=password):
|
|
264
|
+
run("restic", "init", print=True)
|
|
265
|
+
LOGGER.info("Finished initializing '%s'", repo)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def unlock(
|
|
269
|
+
repo: Repo,
|
|
270
|
+
/,
|
|
271
|
+
*,
|
|
272
|
+
password: PasswordLike = SETTINGS.password,
|
|
273
|
+
remove_all: bool = SETTINGS.remove_all,
|
|
274
|
+
) -> None:
|
|
275
|
+
LOGGER.info("Unlocking '%s'", repo)
|
|
276
|
+
with yield_repo_env(repo), yield_password(password=password):
|
|
277
|
+
run(
|
|
278
|
+
"restic", "unlock", *expand_bool("remove-all", bool_=remove_all), print=True
|
|
279
|
+
)
|
|
280
|
+
LOGGER.info("Finished unlocking '%s'", repo)
|
|
281
|
+
|
|
282
|
+
|
|
244
283
|
def restore(
|
|
245
284
|
repo: Repo,
|
|
246
285
|
target: PathLike,
|
|
@@ -257,7 +296,11 @@ def restore(
|
|
|
257
296
|
snapshot: str = SETTINGS.snapshot,
|
|
258
297
|
) -> None:
|
|
259
298
|
LOGGER.info("Restoring snapshot '%s' of '%s' to '%s'...", snapshot, repo, target)
|
|
260
|
-
with
|
|
299
|
+
with (
|
|
300
|
+
yield_repo_env(repo),
|
|
301
|
+
yield_password(password=password),
|
|
302
|
+
TemporaryDirectory() as temp,
|
|
303
|
+
):
|
|
261
304
|
run(
|
|
262
305
|
"restic",
|
|
263
306
|
"restore",
|
|
@@ -268,22 +311,63 @@ def restore(
|
|
|
268
311
|
*expand_include(include=include),
|
|
269
312
|
*expand_include_i(include_i=include_i),
|
|
270
313
|
*expand_tag(tag=tag),
|
|
271
|
-
|
|
272
|
-
str(target),
|
|
314
|
+
*expand_target(temp),
|
|
273
315
|
"--verify",
|
|
274
316
|
snapshot,
|
|
275
317
|
print=True,
|
|
276
318
|
)
|
|
319
|
+
snaps = snapshots(repo, password=password, return_=True)
|
|
320
|
+
path = one(snaps[-1].paths)
|
|
321
|
+
src = temp / path.relative_to(path.anchor)
|
|
322
|
+
cp(src, target)
|
|
277
323
|
LOGGER.info(
|
|
278
324
|
"Finished restoring snapshot '%s' of '%s' to '%s'", snapshot, repo, target
|
|
279
325
|
)
|
|
280
326
|
|
|
281
327
|
|
|
282
|
-
|
|
328
|
+
@overload
|
|
329
|
+
def snapshots(
|
|
330
|
+
repo: Repo,
|
|
331
|
+
/,
|
|
332
|
+
*,
|
|
333
|
+
password: PasswordLike = SETTINGS.password,
|
|
334
|
+
return_: Literal[True],
|
|
335
|
+
print: bool = False,
|
|
336
|
+
) -> list[Snapshot]: ...
|
|
337
|
+
@overload
|
|
338
|
+
def snapshots(
|
|
339
|
+
repo: Repo,
|
|
340
|
+
/,
|
|
341
|
+
*,
|
|
342
|
+
password: PasswordLike = SETTINGS.password,
|
|
343
|
+
return_: Literal[False] = False,
|
|
344
|
+
print: bool = False,
|
|
345
|
+
) -> None: ...
|
|
346
|
+
@overload
|
|
347
|
+
def snapshots(
|
|
348
|
+
repo: Repo,
|
|
349
|
+
/,
|
|
350
|
+
*,
|
|
351
|
+
password: PasswordLike = SETTINGS.password,
|
|
352
|
+
return_: bool = False,
|
|
353
|
+
print: bool = False,
|
|
354
|
+
) -> list[Snapshot] | None: ...
|
|
355
|
+
def snapshots(
|
|
356
|
+
repo: Repo,
|
|
357
|
+
/,
|
|
358
|
+
*,
|
|
359
|
+
password: PasswordLike = SETTINGS.password,
|
|
360
|
+
return_: bool = False,
|
|
361
|
+
print: bool = False, # noqa: A002
|
|
362
|
+
) -> list[Snapshot] | None:
|
|
283
363
|
LOGGER.info("Listing snapshots in '%s'...", repo)
|
|
364
|
+
args: list[str] = ["restic", "snapshots"]
|
|
365
|
+
if return_:
|
|
366
|
+
args.append("--json")
|
|
284
367
|
with yield_repo_env(repo), yield_password(password=password):
|
|
285
|
-
run(
|
|
368
|
+
result = run(*args, print=print, return_=return_)
|
|
286
369
|
LOGGER.info("Finished listing snapshots in '%s'", repo)
|
|
370
|
+
return None if result is None else TypeAdapter(list[Snapshot]).validate_json(result)
|
|
287
371
|
|
|
288
372
|
|
|
289
|
-
__all__ = ["backup", "copy", "forget", "init", "restore", "snapshots"]
|
|
373
|
+
__all__ = ["backup", "copy", "forget", "init", "restore", "snapshots", "unlock"]
|
restic/models.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt # noqa: TC003
|
|
4
|
+
from pathlib import Path # noqa: TC003
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Snapshot(BaseModel):
|
|
10
|
+
time: dt.datetime
|
|
11
|
+
tree: str
|
|
12
|
+
paths: list[Path]
|
|
13
|
+
hostname: str
|
|
14
|
+
username: str
|
|
15
|
+
uid: int
|
|
16
|
+
gid: int
|
|
17
|
+
program_version: str
|
|
18
|
+
summary: Summary
|
|
19
|
+
id: str
|
|
20
|
+
short_id: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Summary(BaseModel):
|
|
24
|
+
backup_start: dt.datetime
|
|
25
|
+
backup_end: dt.datetime
|
|
26
|
+
files_new: int
|
|
27
|
+
files_changed: int
|
|
28
|
+
files_unmodified: int
|
|
29
|
+
dirs_new: int
|
|
30
|
+
dirs_changed: int
|
|
31
|
+
dirs_unmodified: int
|
|
32
|
+
data_blobs: int
|
|
33
|
+
tree_blobs: int
|
|
34
|
+
data_added: int
|
|
35
|
+
data_added_packed: int
|
|
36
|
+
total_files_processed: int
|
|
37
|
+
total_bytes_processed: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_ = Snapshot.model_rebuild()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["Snapshot", "Summary"]
|
restic/repo.py
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from contextlib import contextmanager
|
|
3
|
+
from contextlib import contextmanager, suppress
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from
|
|
6
|
+
from re import search
|
|
7
|
+
from typing import TYPE_CHECKING, Self, assert_never, override
|
|
7
8
|
|
|
8
9
|
from typed_settings import Secret, load_settings
|
|
9
10
|
from utilities.os import temp_environ
|
|
10
|
-
from utilities.re import
|
|
11
|
+
from utilities.re import (
|
|
12
|
+
ExtractGroupError,
|
|
13
|
+
ExtractGroupsError,
|
|
14
|
+
extract_group,
|
|
15
|
+
extract_groups,
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
from restic.settings import LOADERS, SETTINGS, Settings
|
|
13
19
|
|
|
@@ -20,13 +26,38 @@ if TYPE_CHECKING:
|
|
|
20
26
|
type Repo = Backblaze | Local | SFTP
|
|
21
27
|
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
##
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(order=True, slots=True)
|
|
24
33
|
class Backblaze:
|
|
25
34
|
key_id: Secret[str]
|
|
26
35
|
application_key: Secret[str]
|
|
27
36
|
bucket: str
|
|
28
37
|
path: Path
|
|
29
38
|
|
|
39
|
+
@override
|
|
40
|
+
def __eq__(self, other: object, /) -> bool:
|
|
41
|
+
return (
|
|
42
|
+
isinstance(other, type(self))
|
|
43
|
+
and (self.key_id.get_secret_value() == other.key_id.get_secret_value())
|
|
44
|
+
and (
|
|
45
|
+
self.application_key.get_secret_value()
|
|
46
|
+
== other.application_key.get_secret_value()
|
|
47
|
+
)
|
|
48
|
+
and (self.bucket == other.bucket)
|
|
49
|
+
and (self.path == other.path)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
def __hash__(self) -> int:
|
|
54
|
+
return hash((
|
|
55
|
+
self.key_id.get_secret_value(),
|
|
56
|
+
self.application_key.get_secret_value(),
|
|
57
|
+
self.bucket,
|
|
58
|
+
self.path,
|
|
59
|
+
))
|
|
60
|
+
|
|
30
61
|
@classmethod
|
|
31
62
|
def parse(
|
|
32
63
|
cls,
|
|
@@ -45,8 +76,7 @@ class Backblaze:
|
|
|
45
76
|
case None, Secret() as key_id_use:
|
|
46
77
|
...
|
|
47
78
|
case None, None:
|
|
48
|
-
|
|
49
|
-
raise ValueError(msg)
|
|
79
|
+
raise BackblazeKeyIdMissingError
|
|
50
80
|
case never:
|
|
51
81
|
assert_never(never)
|
|
52
82
|
match application_key, settings.backblaze_application_key:
|
|
@@ -57,11 +87,14 @@ class Backblaze:
|
|
|
57
87
|
case None, Secret() as application_key_use:
|
|
58
88
|
...
|
|
59
89
|
case None, None:
|
|
60
|
-
|
|
61
|
-
raise ValueError(msg)
|
|
90
|
+
raise BackblazeApplicationIdMissingError
|
|
62
91
|
case never:
|
|
63
92
|
assert_never(never)
|
|
64
|
-
|
|
93
|
+
pattern = r"^b2:([^@:]+):([^@+]+)$"
|
|
94
|
+
try:
|
|
95
|
+
bucket, path = extract_groups(pattern, text)
|
|
96
|
+
except ExtractGroupsError:
|
|
97
|
+
raise BackblazeInvalidStrError(pattern=pattern, text=text) from None
|
|
65
98
|
return cls(key_id_use, application_key_use, bucket, Path(path))
|
|
66
99
|
|
|
67
100
|
@property
|
|
@@ -69,13 +102,48 @@ class Backblaze:
|
|
|
69
102
|
return f"b2:{self.bucket}:{self.path}"
|
|
70
103
|
|
|
71
104
|
|
|
105
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
106
|
+
class BackblazeParseError(Exception): ...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
110
|
+
class BackblazeKeyIdMissingError(BackblazeParseError):
|
|
111
|
+
@override
|
|
112
|
+
def __str__(self) -> str:
|
|
113
|
+
return "'BACKBLAZE_KEY_ID' is missing"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
117
|
+
class BackblazeApplicationIdMissingError(BackblazeParseError):
|
|
118
|
+
@override
|
|
119
|
+
def __str__(self) -> str:
|
|
120
|
+
return "'BACKBLAZE_APPLICATION_ID' is missing"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
124
|
+
class BackblazeInvalidStrError(BackblazeParseError):
|
|
125
|
+
pattern: str
|
|
126
|
+
text: str
|
|
127
|
+
|
|
128
|
+
@override
|
|
129
|
+
def __str__(self) -> str:
|
|
130
|
+
return f"Text must be of the form {self.pattern!r}; got {self.text!r}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
##
|
|
134
|
+
|
|
135
|
+
|
|
72
136
|
@dataclass(order=True, unsafe_hash=True, slots=True)
|
|
73
137
|
class Local:
|
|
74
138
|
path: Path
|
|
75
139
|
|
|
76
140
|
@classmethod
|
|
77
141
|
def parse(cls, text: str, /) -> Self:
|
|
78
|
-
|
|
142
|
+
pattern = r"^local:([^@:]+)$"
|
|
143
|
+
try:
|
|
144
|
+
path = extract_group(pattern, text)
|
|
145
|
+
except ExtractGroupError:
|
|
146
|
+
raise LocalParseError(pattern=pattern, text=text) from None
|
|
79
147
|
return cls(Path(path))
|
|
80
148
|
|
|
81
149
|
@property
|
|
@@ -83,6 +151,19 @@ class Local:
|
|
|
83
151
|
return f"local:{self.path}"
|
|
84
152
|
|
|
85
153
|
|
|
154
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
155
|
+
class LocalParseError(Exception):
|
|
156
|
+
pattern: str
|
|
157
|
+
text: str
|
|
158
|
+
|
|
159
|
+
@override
|
|
160
|
+
def __str__(self) -> str:
|
|
161
|
+
return f"Text must be of the form {self.pattern!r}; got {self.text!r}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
##
|
|
165
|
+
|
|
166
|
+
|
|
86
167
|
@dataclass(order=True, unsafe_hash=True, slots=True)
|
|
87
168
|
class SFTP:
|
|
88
169
|
user: str
|
|
@@ -91,9 +172,11 @@ class SFTP:
|
|
|
91
172
|
|
|
92
173
|
@classmethod
|
|
93
174
|
def parse(cls, text: str, /) -> Self:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
175
|
+
pattern = r"^sftp:([^@:]+)@([^@:]+):([^@:]+)$"
|
|
176
|
+
try:
|
|
177
|
+
user, hostname, path = extract_groups(pattern, text)
|
|
178
|
+
except ExtractGroupsError:
|
|
179
|
+
raise SFTPParseError(pattern=pattern, text=text) from None
|
|
97
180
|
return cls(user, hostname, Path(path))
|
|
98
181
|
|
|
99
182
|
@property
|
|
@@ -101,6 +184,45 @@ class SFTP:
|
|
|
101
184
|
return f"sftp:{self.user}@{self.hostname}:{self.path}"
|
|
102
185
|
|
|
103
186
|
|
|
187
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
188
|
+
class SFTPParseError(Exception):
|
|
189
|
+
pattern: str
|
|
190
|
+
text: str
|
|
191
|
+
|
|
192
|
+
@override
|
|
193
|
+
def __str__(self) -> str:
|
|
194
|
+
return f"Text must be of the form {self.pattern!r}; got {self.text!r}"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
##
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def parse_repo(text: str, /) -> Repo:
|
|
201
|
+
try:
|
|
202
|
+
return Backblaze.parse(text)
|
|
203
|
+
except BackblazeParseError as error:
|
|
204
|
+
if search("b2", text):
|
|
205
|
+
raise ParseRepoBackblazeError(text=str(error)) from None
|
|
206
|
+
with suppress(SFTPParseError):
|
|
207
|
+
return SFTP.parse(text)
|
|
208
|
+
try:
|
|
209
|
+
return Local.parse(text)
|
|
210
|
+
except LocalParseError:
|
|
211
|
+
return Local(Path(text))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
215
|
+
class ParseRepoBackblazeError(Exception):
|
|
216
|
+
text: str
|
|
217
|
+
|
|
218
|
+
@override
|
|
219
|
+
def __str__(self) -> str:
|
|
220
|
+
return self.text
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
##
|
|
224
|
+
|
|
225
|
+
|
|
104
226
|
@contextmanager
|
|
105
227
|
def yield_repo_env(
|
|
106
228
|
repo: Repo, /, *, env_var: str = "RESTIC_REPOSITORY"
|
|
@@ -120,4 +242,18 @@ def yield_repo_env(
|
|
|
120
242
|
assert_never(never)
|
|
121
243
|
|
|
122
244
|
|
|
123
|
-
__all__ = [
|
|
245
|
+
__all__ = [
|
|
246
|
+
"SFTP",
|
|
247
|
+
"Backblaze",
|
|
248
|
+
"BackblazeApplicationIdMissingError",
|
|
249
|
+
"BackblazeInvalidStrError",
|
|
250
|
+
"BackblazeKeyIdMissingError",
|
|
251
|
+
"BackblazeParseError",
|
|
252
|
+
"Local",
|
|
253
|
+
"LocalParseError",
|
|
254
|
+
"ParseRepoBackblazeError",
|
|
255
|
+
"Repo",
|
|
256
|
+
"SFTPParseError",
|
|
257
|
+
"parse_repo",
|
|
258
|
+
"yield_repo_env",
|
|
259
|
+
]
|
restic/settings.py
CHANGED
|
@@ -17,6 +17,8 @@ from typed_settings import (
|
|
|
17
17
|
)
|
|
18
18
|
from utilities.os import CPU_COUNT
|
|
19
19
|
|
|
20
|
+
from restic.logging import LOGGER
|
|
21
|
+
|
|
20
22
|
CONFIG_FILE = getenv("RESTIC_CONFIG_FILE", "config.toml")
|
|
21
23
|
SECRETS_FILE = getenv("RESTIC_SECRETS_FILE", "secrets.toml")
|
|
22
24
|
LOADERS = [
|
|
@@ -38,10 +40,6 @@ class Settings:
|
|
|
38
40
|
default=None, help="Backblaze application key"
|
|
39
41
|
)
|
|
40
42
|
# backup
|
|
41
|
-
chmod: bool = option(default=False, help="Change permissions of the directory/file")
|
|
42
|
-
chown: str | None = option(
|
|
43
|
-
default=None, help="Change ownership of the directory/file"
|
|
44
|
-
)
|
|
45
43
|
exclude_backup: list[str] | None = option(default=None, help="Exclude a pattern")
|
|
46
44
|
exclude_i_backup: list[str] | None = option(
|
|
47
45
|
default=None, help="Exclude a pattern but ignores the casing of filenames"
|
|
@@ -101,6 +99,9 @@ class Settings:
|
|
|
101
99
|
default=None,
|
|
102
100
|
help="Keep yearly snapshots that are newer than duration relative to the latest snapshot",
|
|
103
101
|
)
|
|
102
|
+
group_by: list[str] | None = option(
|
|
103
|
+
default=None, help="Group snapshots by host, paths and/or tags"
|
|
104
|
+
)
|
|
104
105
|
prune: bool = option(
|
|
105
106
|
default=True,
|
|
106
107
|
help="Automatically run the 'prune' command if snapshots have been removed",
|
|
@@ -117,6 +118,10 @@ class Settings:
|
|
|
117
118
|
tag_forget: list[str] | None = option(
|
|
118
119
|
default=None, help="Only consider snapshots including tag[,tag,...]"
|
|
119
120
|
)
|
|
121
|
+
# unlock
|
|
122
|
+
remove_all: bool = option(
|
|
123
|
+
default=False, help="Remove all locks, even non-stale ones"
|
|
124
|
+
)
|
|
120
125
|
# restore
|
|
121
126
|
delete: bool = option(
|
|
122
127
|
default=False,
|
|
@@ -137,6 +142,7 @@ class Settings:
|
|
|
137
142
|
snapshot: str = option(default="latest", help="Snapshot ID to restore")
|
|
138
143
|
|
|
139
144
|
|
|
145
|
+
LOGGER.info("Loading settings from '%s' and '%s'...", CONFIG_FILE, SECRETS_FILE)
|
|
140
146
|
SETTINGS = load_settings(Settings, LOADERS)
|
|
141
147
|
|
|
142
148
|
|
|
@@ -146,17 +152,8 @@ def _get_help(member_descriptor: Any, /) -> None:
|
|
|
146
152
|
]
|
|
147
153
|
|
|
148
154
|
|
|
149
|
-
@settings(kw_only=True)
|
|
150
|
-
class InitSettings:
|
|
151
|
-
password: Secret[str] = secret(
|
|
152
|
-
default=SETTINGS.password, help=_get_help(Settings.password)
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
155
|
@settings(kw_only=True)
|
|
157
156
|
class BackupSettings:
|
|
158
|
-
chmod: bool = option(default=SETTINGS.chmod, help=_get_help(Settings.chmod))
|
|
159
|
-
chown: str | None = option(default=SETTINGS.chown, help=_get_help(Settings.chown))
|
|
160
157
|
password: Secret[str] = secret(
|
|
161
158
|
default=SETTINGS.password, help=_get_help(Settings.password)
|
|
162
159
|
)
|
|
@@ -213,6 +210,9 @@ class BackupSettings:
|
|
|
213
210
|
keep_within_yearly: str | None = option(
|
|
214
211
|
default=SETTINGS.keep_within_yearly, help=_get_help(Settings.keep_within_yearly)
|
|
215
212
|
)
|
|
213
|
+
group_by: list[str] | None = option(
|
|
214
|
+
default=SETTINGS.group_by, help=_get_help(Settings.group_by)
|
|
215
|
+
)
|
|
216
216
|
prune: bool = option(default=SETTINGS.prune, help=_get_help(Settings.prune))
|
|
217
217
|
repack_cacheable_only: bool = option(
|
|
218
218
|
default=SETTINGS.repack_cacheable_only,
|
|
@@ -242,6 +242,7 @@ class CopySettings:
|
|
|
242
242
|
tag: list[str] | None = option(
|
|
243
243
|
default=SETTINGS.tag_copy, help=_get_help(Settings.tag_copy)
|
|
244
244
|
)
|
|
245
|
+
sleep: int | None = option(default=SETTINGS.sleep, help=_get_help(Settings.sleep))
|
|
245
246
|
|
|
246
247
|
|
|
247
248
|
@settings(kw_only=True)
|
|
@@ -287,6 +288,9 @@ class ForgetSettings:
|
|
|
287
288
|
keep_within_yearly: str | None = option(
|
|
288
289
|
default=SETTINGS.keep_within_yearly, help=_get_help(Settings.keep_within_yearly)
|
|
289
290
|
)
|
|
291
|
+
group_by: list[str] | None = option(
|
|
292
|
+
default=SETTINGS.group_by, help=_get_help(Settings.group_by)
|
|
293
|
+
)
|
|
290
294
|
prune: bool = option(default=SETTINGS.prune, help=_get_help(Settings.prune))
|
|
291
295
|
repack_cacheable_only: bool = option(
|
|
292
296
|
default=SETTINGS.repack_cacheable_only,
|
|
@@ -304,6 +308,23 @@ class ForgetSettings:
|
|
|
304
308
|
)
|
|
305
309
|
|
|
306
310
|
|
|
311
|
+
@settings(kw_only=True)
|
|
312
|
+
class InitSettings:
|
|
313
|
+
password: Secret[str] = secret(
|
|
314
|
+
default=SETTINGS.password, help=_get_help(Settings.password)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@settings(kw_only=True)
|
|
319
|
+
class UnlockSettings:
|
|
320
|
+
password: Secret[str] = secret(
|
|
321
|
+
default=SETTINGS.password, help=_get_help(Settings.password)
|
|
322
|
+
)
|
|
323
|
+
remove_all: bool = option(
|
|
324
|
+
default=SETTINGS.remove_all, help=_get_help(Settings.remove_all)
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
307
328
|
@settings(kw_only=True)
|
|
308
329
|
class RestoreSettings:
|
|
309
330
|
password: Secret[str] = secret(
|
|
@@ -346,4 +367,5 @@ __all__ = [
|
|
|
346
367
|
"RestoreSettings",
|
|
347
368
|
"Settings",
|
|
348
369
|
"SnapshotsSettings",
|
|
370
|
+
"UnlockSettings",
|
|
349
371
|
]
|
restic/utilities.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from itertools import chain
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING,
|
|
7
|
+
from typing import TYPE_CHECKING, assert_never, override
|
|
7
8
|
|
|
8
9
|
from typed_settings import Secret
|
|
9
10
|
from utilities.os import temp_environ
|
|
10
|
-
from utilities.subprocess import run
|
|
11
11
|
from utilities.tempfile import TemporaryFile
|
|
12
|
+
from utilities.text import repr_str
|
|
12
13
|
|
|
13
14
|
from restic.settings import SETTINGS
|
|
14
15
|
|
|
@@ -36,6 +37,10 @@ def expand_exclude_i(*, exclude_i: list[str] | None = None) -> list[str]:
|
|
|
36
37
|
return _expand_list("iexclude", arg=exclude_i)
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
def expand_group_by(*, group_by: list[str] | None = None) -> list[str]:
|
|
41
|
+
return [] if group_by is None else ["--group-by", ",".join(group_by)]
|
|
42
|
+
|
|
43
|
+
|
|
39
44
|
def expand_include(*, include: list[str] | None = None) -> list[str]:
|
|
40
45
|
return _expand_list("include", arg=include)
|
|
41
46
|
|
|
@@ -52,12 +57,19 @@ def expand_keep_within(freq: str, /, *, duration: str | None = None) -> list[str
|
|
|
52
57
|
return [] if duration is None else [f"--keep-{freq}", duration]
|
|
53
58
|
|
|
54
59
|
|
|
60
|
+
def expand_read_concurrency(n: int, /) -> list[str]:
|
|
61
|
+
return ["--read-concurrency", str(n)]
|
|
62
|
+
|
|
63
|
+
|
|
55
64
|
def expand_tag(*, tag: list[str] | None = None) -> list[str]:
|
|
56
65
|
return _expand_list("tag", arg=tag)
|
|
57
66
|
|
|
58
67
|
|
|
59
|
-
def
|
|
60
|
-
|
|
68
|
+
def expand_target(target: PathLike, /) -> list[str]:
|
|
69
|
+
return ["--target", str(target)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
##
|
|
61
73
|
|
|
62
74
|
|
|
63
75
|
@contextmanager
|
|
@@ -77,8 +89,7 @@ def yield_password(
|
|
|
77
89
|
with temp_environ({env_var: str(value)}):
|
|
78
90
|
yield
|
|
79
91
|
else:
|
|
80
|
-
|
|
81
|
-
raise FileNotFoundError(msg)
|
|
92
|
+
raise YieldPasswordError(path=value)
|
|
82
93
|
case str():
|
|
83
94
|
if Path(value).is_file():
|
|
84
95
|
with temp_environ({env_var: value}):
|
|
@@ -91,6 +102,18 @@ def yield_password(
|
|
|
91
102
|
assert_never(never)
|
|
92
103
|
|
|
93
104
|
|
|
105
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
106
|
+
class YieldPasswordError(Exception):
|
|
107
|
+
path: Path
|
|
108
|
+
|
|
109
|
+
@override
|
|
110
|
+
def __str__(self) -> str:
|
|
111
|
+
return f"Password file not found: {repr_str(self.path)}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
|
|
116
|
+
|
|
94
117
|
def _expand_list(flag: str, /, *, arg: list[str] | None = None) -> list[str]:
|
|
95
118
|
return (
|
|
96
119
|
[] if arg is None else list(chain.from_iterable([f"--{flag}", a] for a in arg))
|
|
@@ -98,15 +121,18 @@ def _expand_list(flag: str, /, *, arg: list[str] | None = None) -> list[str]:
|
|
|
98
121
|
|
|
99
122
|
|
|
100
123
|
__all__ = [
|
|
124
|
+
"YieldPasswordError",
|
|
101
125
|
"expand_bool",
|
|
102
126
|
"expand_dry_run",
|
|
103
127
|
"expand_exclude",
|
|
104
128
|
"expand_exclude_i",
|
|
129
|
+
"expand_group_by",
|
|
105
130
|
"expand_include",
|
|
106
131
|
"expand_include_i",
|
|
107
132
|
"expand_keep",
|
|
108
133
|
"expand_keep_within",
|
|
134
|
+
"expand_read_concurrency",
|
|
109
135
|
"expand_tag",
|
|
110
|
-
"
|
|
136
|
+
"expand_target",
|
|
111
137
|
"yield_password",
|
|
112
138
|
]
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
restic/__init__.py,sha256=yhMJ-0LPYMf--j6qVDnbLfF1nV_AHSSJISPpIr9pFm8,59
|
|
2
|
-
restic/cli.py,sha256=YVrgIwCkAMI9BmbjAC1M4aFZ2WjNk1f8YqfauwfLwG8,5432
|
|
3
|
-
restic/click.py,sha256=cuqKKXlErXsb_vNkWHKyo_xnrTgo8qhscaEWqsT7Nl8,1525
|
|
4
|
-
restic/constants.py,sha256=uu2dXIlx3vyMNgJUs1lrCMOYBPGGUviVJ5qvxqOx6Tw,911
|
|
5
|
-
restic/lib.py,sha256=-d3dPReAJikNdLWHNC7QjJiAHYG6vj435KTCc0A09Xc,10454
|
|
6
|
-
restic/logging.py,sha256=tzoiz1F2T-AavxahNduXxfnFvhmcBh6JHskDgUhcNVU,119
|
|
7
|
-
restic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
restic/repo.py,sha256=uDOEzkSWz0vJR-SFFraYtVSD-bl7C6FWlPtfAdDy9KI,3544
|
|
9
|
-
restic/settings.py,sha256=a2WPOeB_VwvbZJ-uxuJpJB1RE5TH1S-BD0IeIIf1v_8,12988
|
|
10
|
-
restic/types.py,sha256=lCchCT7Y7yo0PMBGHgYjpuTDf9z2orPjKiPuje1vfmE,229
|
|
11
|
-
restic/utilities.py,sha256=uXq5Jl_FFjVNCLYG9si4KNiiCJWG2nk0Yr49c4TgA1Q,3215
|
|
12
|
-
dycw_restic-0.2.22.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
13
|
-
dycw_restic-0.2.22.dist-info/entry_points.txt,sha256=6byn-7KAIlNAdTjaNQ3DjylzzazsZB8SLFvvrqv641o,48
|
|
14
|
-
dycw_restic-0.2.22.dist-info/METADATA,sha256=PHpIDO2qcLXFznHv-ujYWYzW6byripPVLxTqOVyjzfA,387
|
|
15
|
-
dycw_restic-0.2.22.dist-info/RECORD,,
|
|
File without changes
|