vocker 0.1.0__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.
vocker/__init__.py ADDED
File without changes
vocker/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import Main
2
+
3
+ Main.main()
vocker/cli.py ADDED
@@ -0,0 +1,384 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ import re
6
+ import functools
7
+ import json
8
+ import sys
9
+ import typing as ty
10
+
11
+ import structlog
12
+
13
+ from .util import parse_pure_path, pprofile
14
+
15
+ logger = structlog.get_logger(__name__)
16
+
17
+
18
+ rx_probably_uri = re.compile(r"^[a-z-]+://")
19
+
20
+
21
+ def syst():
22
+ from . import system as syst
23
+
24
+ return syst
25
+
26
+
27
+ def _remote_repo_name(s):
28
+ if not s.startswith("@"):
29
+ raise ValueError("remote repository name must be start with a @")
30
+
31
+ return s[1:]
32
+
33
+
34
+ def _local_repo_name(s):
35
+ if s.startswith("@"):
36
+ raise ValueError("local repository name cannot start with @")
37
+
38
+ return s
39
+
40
+
41
+ def _abspath(s):
42
+ return Path(s).absolute()
43
+
44
+
45
+ class Main:
46
+ def __init__(self, argv, *, path_base=None):
47
+ self.argument_parser = self.init_argparser()
48
+ self.a = self.argument_parser.parse_args(argv)
49
+ self.path_base = path_base
50
+
51
+ def cmd_missing(self):
52
+ self.argument_parser.error("missing command")
53
+
54
+ @functools.cached_property
55
+ def system(self):
56
+ return syst().System(path_base=self.path_base)
57
+
58
+ def cmd_repo_remote_add(self):
59
+ a = self.a
60
+ name = _remote_repo_name(self.a.remote_name)
61
+ if not rx_probably_uri.search(uri := a.uri):
62
+ if (p := Path(uri)).exists():
63
+ uri = p.as_uri()
64
+ else:
65
+ raise ValueError(f"not a valid URI or filesystem path: {uri!r}")
66
+ self.system.remotes[name] = syst().RemoteRepository(uri=uri)
67
+
68
+ def cmd_repo_remote_list(self):
69
+ return dict(remotes={k: repr(v) for k, v in self.system.remotes.items()})
70
+
71
+ def cmd_repo_remote_remove(self):
72
+ name = _remote_repo_name(self.a.remote_name)
73
+ del self.system.remotes[name]
74
+
75
+ def cmd_repo_list(self):
76
+ has_invalid = False
77
+ d = {}
78
+ for r in self.system.repo_list():
79
+ if not (valid := r.check()):
80
+ has_invalid = True
81
+ d[r.name] = "ok" if valid else "damaged"
82
+ comment = ""
83
+ if has_invalid:
84
+ comment = """There are damaged/invalid local repositories. This can happen if a local \
85
+ repository operation was interrupted (via a crash, an unexpected power loss, or Ctrl-C)."""
86
+ return dict(repos=d, comment=comment)
87
+
88
+ def cmd_repo_init(self):
89
+ self.system.repo_init_new(self.a.local_name, self.a.hash_function)
90
+
91
+ def _get_local_and_remote_names(self):
92
+ local_name, remote_name = self.a.local_name, self.a.remote_name
93
+ if local_name.startswith("@"):
94
+ local_name, remote_name = remote_name, local_name
95
+ local_name = _local_repo_name(local_name)
96
+ remote_name = _remote_repo_name(remote_name)
97
+ return local_name, remote_name
98
+
99
+ def cmd_repo_upload(self):
100
+ local_name, remote_name = self._get_local_and_remote_names()
101
+ self.system.repo_upload(local_name, remote_name)
102
+
103
+ def cmd_repo_download(self):
104
+ local_name, remote_name = self._get_local_and_remote_names()
105
+ self.system.repo_download(remote_name, local_name)
106
+
107
+ def cmd_image_import(self):
108
+ if (t := self.a.type) == "auto":
109
+ t = None
110
+ return self.system.repo_add_image(
111
+ repo_name=self.a.repository,
112
+ image_path=self.a.input,
113
+ image_type=t,
114
+ mock_image_path=self.a.mock_image_path,
115
+ )
116
+
117
+ def cmd_image_export(self):
118
+ trusted = self.a.trusted
119
+ use_sys_python = self.a.mock_use_system_python
120
+ if (trusted + use_sys_python) != 1:
121
+ raise ValueError("provide exactly one of --trusted or --mock-use-system-python")
122
+
123
+ if (repo_name := self.a.repo_name).startswith("@"):
124
+ kw = dict(remote_name=_remote_repo_name(repo_name))
125
+ else:
126
+ kw = dict(repo_name=_local_repo_name(repo_name))
127
+ return self.system.export_image(
128
+ image_id=self.a.image_id,
129
+ target=self.a.target,
130
+ mock_target=self.a.mock_target,
131
+ mock_use_system_python=self.a.mock_use_system_python,
132
+ **kw,
133
+ )
134
+
135
+ def cmd_gc(self):
136
+ d = dict(main=self.system.dedup, repo=self.system.repo_dedup)
137
+ dedups_keys = set()
138
+ a = self.a
139
+ max_age = a.max_age
140
+ check_integrity = a.check_integrity
141
+ check_links = a.check_links
142
+ if a.main:
143
+ dedups_keys.add("main")
144
+ if a.repo:
145
+ dedups_keys.add("repo")
146
+ if a.full:
147
+ dedups_keys.update(("main", "repo"))
148
+ check_integrity = True
149
+ check_links = True
150
+ if max_age is None:
151
+ max_age = 3600
152
+ dedups = tuple(d[k] for k in dedups_keys)
153
+ check_links_under: ty.Sequence[Path] = a.check_links_under or ()
154
+
155
+ for k in dedups_keys:
156
+ dedup = d[k]
157
+ logger.info("Starting to gc.", data_dedup=k)
158
+ if check_links:
159
+ logger.info("Checking links.")
160
+ dedup.check_links()
161
+ elif check_links_under:
162
+ for p in check_links_under:
163
+ logger.info(f"Checking links under user path.", data_path=str(p))
164
+ dedup.check_links(p)
165
+
166
+ if check_integrity:
167
+ logger.info("Checking integrity.")
168
+ dedup.integrity_check(skip_same_mtime=True)
169
+ dedup.garbage_collect_extra_files()
170
+
171
+ if max_age is not None:
172
+ logger.info("Deleting unused files.", data_max_age_seconds=max_age)
173
+ dedup.update_all_orphaned()
174
+ dedup.garbage_collect_dedup_files(max_age)
175
+
176
+ def _info(dedup):
177
+ return {
178
+ "corrupted": [c.to_json() for c in dedup.corrupted_list()],
179
+ "#comments": [f"Corrupted base path is at {dedup.path_corrupted!s}"],
180
+ }
181
+
182
+ result = {"dedup": {k: _info(v) for k, v in d.items()}, "comments": []}
183
+
184
+ if not dedups:
185
+ result["comments"].append(
186
+ """No actions taken. You must use --main and/or --repo to specify which \
187
+ files to act on, or use --full to perform all cleanup actions against all of the files."""
188
+ )
189
+
190
+ return result
191
+
192
+ def cmd_stats(self):
193
+ def f(dedup):
194
+ stats = dedup.compute_stats()
195
+ if stats.dedup_total_bytes:
196
+ ratio = stats.link_total_bytes / stats.dedup_total_bytes
197
+ else:
198
+ ratio = 1.0
199
+ return stats.to_json() | {"#space_savings_ratio": ratio}
200
+
201
+ d = dict(main=self.system.dedup, repo=self.system.repo_dedup)
202
+ return {k: f(v) for k, v in d.items()}
203
+
204
+ def init_argparser(self):
205
+ parser = argparse.ArgumentParser()
206
+ parser.set_defaults(callback=self.cmd_missing)
207
+ subparsers = parser.add_subparsers()
208
+ sub_repo = subparsers.add_parser("repo").add_subparsers()
209
+ sub_remote = sub_repo.add_parser("remote").add_subparsers()
210
+ sub_img = subparsers.add_parser("image").add_subparsers()
211
+
212
+ p = {}
213
+
214
+ def _add_parser(___parent, ___name):
215
+ def f(*args, **kwargs):
216
+ a = p[___name] = ___parent.add_parser(*args, **kwargs)
217
+ a.set_defaults(callback=getattr(self, "cmd_" + ___name))
218
+ return a
219
+
220
+ return f
221
+
222
+ def _Path(x):
223
+ return Path(x).resolve()
224
+
225
+ _add_parser(sub_repo, "repo_upload")(
226
+ "upload",
227
+ description="""\
228
+ Upload local repository copy to remote.""",
229
+ )
230
+ _add_parser(sub_repo, "repo_download")(
231
+ "download",
232
+ description="""\
233
+ Download remote repository to local.""",
234
+ )
235
+
236
+ def _arg_local(k):
237
+ p[k].add_argument(
238
+ "local_name", metavar="LOCAL-NAME", help="Name of the local repository."
239
+ )
240
+
241
+ def _arg_remote(k):
242
+ p[k].add_argument(
243
+ "remote_name",
244
+ metavar="@REMOTE-NAME",
245
+ help="Name of the remote repository, with a '@' character as a prefix.",
246
+ )
247
+
248
+ _arg_local("repo_upload")
249
+ _arg_remote("repo_upload")
250
+
251
+ _arg_remote("repo_download")
252
+ _arg_local("repo_download")
253
+
254
+ _add_parser(sub_remote, "repo_remote_add")(
255
+ "add",
256
+ description="Add a new remote repository location.",
257
+ )
258
+ _add_parser(sub_remote, "repo_remote_remove")(
259
+ "remove",
260
+ description="Remove remote repository.",
261
+ )
262
+ _add_parser(sub_remote, "repo_remote_list")(
263
+ "list",
264
+ aliases=["ls"],
265
+ description="List remote repository locations.",
266
+ )
267
+ _arg_remote("repo_remote_add")
268
+ _arg_remote("repo_remote_remove")
269
+ p["repo_remote_add"].add_argument("uri", help="URI of repository location")
270
+
271
+ _add_parser(sub_repo, "repo_list")(
272
+ "list", aliases=["ls"], description="List local repositories."
273
+ )
274
+ _add_parser(sub_repo, "repo_init")("init", description="Create new empty local repository.")
275
+ _arg_local("repo_init")
276
+ p["repo_init"].add_argument("--hash-function", default="sha3-512")
277
+
278
+ _add_parser(sub_img, "image_import")("import", description="Add image to local repository.")
279
+ p["image_import"].add_argument(
280
+ "--repository",
281
+ "-R",
282
+ metavar="LOCAL-REPO",
283
+ help="Name of the local repository.",
284
+ required=True,
285
+ )
286
+ p["image_import"].add_argument("--type", "-t", help="Image type. Autodetected by default.")
287
+ p["image_import"].add_argument(
288
+ "--mock-image-path",
289
+ help="(For testing only) Pretend that this is the original location of the image.",
290
+ type=parse_pure_path,
291
+ )
292
+ p["image_import"].add_argument(
293
+ "input", metavar="INPUT-DIRECTORY", help="Input directory path.", type=_abspath
294
+ )
295
+
296
+ _add_parser(sub_img, "image_export")(
297
+ "export",
298
+ description="Download an image (if needed) and unpack its contents into a directory.",
299
+ )
300
+ p["image_export"].add_argument(
301
+ "repo_name",
302
+ metavar="LOCAL-REPO-NAME|@REMOTE-REPO-NAME",
303
+ help="Name of the local or remote repository.",
304
+ )
305
+ p["image_export"].add_argument(
306
+ "image_id",
307
+ metavar="IMAGE-ID",
308
+ help="Image ID. Must be a multihash in base64url format.",
309
+ )
310
+ p["image_export"].add_argument(
311
+ "target", metavar="TARGET-DIR", help="Target directory to export to.", type=_abspath
312
+ )
313
+ p["image_export"].add_argument(
314
+ "--trusted",
315
+ help="Allow the execution of arbitrary code inside the image. This is necessary, for"
316
+ "example, to generate the pyc files. The only other alternative is the "
317
+ "--mock-use-system-python flag.",
318
+ action="store_true",
319
+ )
320
+ p["image_export"].add_argument(
321
+ "--mock-target",
322
+ help="(For testing only) Pretend that this is the output path.",
323
+ type=parse_pure_path,
324
+ )
325
+ p["image_export"].add_argument(
326
+ "--mock-use-system-python",
327
+ help="(For testing only) Do not attempt to use the Python.",
328
+ action="store_true",
329
+ )
330
+ _add_parser(subparsers, "gc")("gc", description="Clean old and unused files.")
331
+ p["gc"].add_argument(
332
+ "--full",
333
+ help="""Do everything. Equivalent to `--main --repo --check-links --check-integrity \
334
+ --max-age=3600`.""",
335
+ action="store_true",
336
+ )
337
+ p["gc"].add_argument(
338
+ "--main",
339
+ help="Act on the files inside exported image directories.",
340
+ action="store_true",
341
+ )
342
+ p["gc"].add_argument("--repo", help="Act on local repository files.", action="store_true")
343
+ p["gc"].add_argument(
344
+ "--check-links",
345
+ help="Check the links created by the file deduplication system.",
346
+ action="store_true",
347
+ )
348
+ p["gc"].add_argument(
349
+ "--max-age", help="Delete unused files older than MAX-AGE seconds.", type=int
350
+ )
351
+ p["gc"].add_argument(
352
+ "--check-links-under",
353
+ "-L",
354
+ help="Check the links under the following (potentially non-existing) path.",
355
+ type=_abspath,
356
+ action="append",
357
+ )
358
+ p["gc"].add_argument(
359
+ "--check-integrity",
360
+ help="Read every file contents and check the hash.",
361
+ action="store_true",
362
+ )
363
+ _add_parser(subparsers, "stats")("stats", description="Show storage statistics.")
364
+
365
+ return parser
366
+
367
+ def setup_logging(self):
368
+ structlog.configure(logger_factory=structlog.PrintLoggerFactory(sys.stderr))
369
+
370
+ def run(self):
371
+ with pprofile():
372
+ value = self.a.callback()
373
+ if isinstance(value, dict):
374
+ print(json.dumps(value, indent=2))
375
+
376
+ def run_debug(self):
377
+ return self.a.callback()
378
+
379
+ @classmethod
380
+ def main(cls, argv=None, setup_logging=True):
381
+ self = cls(argv=argv)
382
+ if setup_logging:
383
+ self.setup_logging()
384
+ self.run()