offspot-config 2.0.0__tar.gz → 2.2.0__tar.gz
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.
- {offspot_config-2.0.0 → offspot_config-2.2.0}/CHANGELOG.md +35 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/PKG-INFO +1 -1
- offspot_config-2.2.0/src/offspot_config/branding/horizontal-logo-light.png +0 -0
- offspot_config-2.2.0/src/offspot_config/branding/square-logo-light.png +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/builder.py +131 -31
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/constants.py +1 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/file.py +19 -9
- offspot_config-2.2.0/src/offspot_config/inputs/base.py +51 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/checksum.py +4 -1
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/mainconfig.py +1 -1
- offspot_config-2.2.0/src/offspot_config/inputs/ways.py +1 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/dashboard.py +12 -2
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/misc.py +11 -0
- offspot_config-2.2.0/src/offspot_runtime/__about__.py +1 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/conftest.py +1 -1
- offspot_config-2.2.0/tests/test_inputs.py +92 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_reader.py +79 -18
- offspot_config-2.0.0/src/offspot_config/inputs/base.py +0 -30
- offspot_config-2.0.0/src/offspot_config/inputs/ways.py +0 -1
- offspot_config-2.0.0/src/offspot_runtime/__about__.py +0 -1
- offspot_config-2.0.0/tests/test_inputs.py +0 -8
- {offspot_config-2.0.0 → offspot_config-2.2.0}/.gitignore +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/.pre-commit-config.yaml +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/LICENSE +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/README.md +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/pyproject.toml +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/__init__.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/catalog.json +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/catalog.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/__init__.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/file.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/oci.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/output.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/inputs/str.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/oci_images.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/packages.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/__init__.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/download.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/sizes.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/utils/yaml.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_config/zim.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/__init__.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/ap.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/checks.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/configlib.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/containers.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/dnsmasqspoof.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/ethernet.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/firmware.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/fromfile.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/hostname.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/src/offspot_runtime/timezone.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/tasks.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_catalog.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_checks.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_humanid.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_link.py +0 -0
- {offspot_config-2.0.0 → offspot_config-2.2.0}/tests/test_zim.py +0 -0
|
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.2.0] - 2024-04-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- [inputs.file] `File.is_base64_encoded` if `via` is `base64`
|
|
13
|
+
- [utils.misc] `b64_encode()` and `b64_decode()` for reproducible base64 transport
|
|
14
|
+
- [branding] `branding` folder in `offspot_config` containing official/original offspot branding files
|
|
15
|
+
- [constants] `INTERNAL_BRANDING_PATH` pointing to code-reachable folder of branding files
|
|
16
|
+
- [builder] `ORIGINAL_BRANDING_PATH` constant for '/data/branding`
|
|
17
|
+
- [builder] `BRANDING_PATH` constant for `/data/contents/branding`
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- [builder] Using kiwix-serve 3.7.0-1
|
|
22
|
+
- [builder] Using reverse-proxy 1.8
|
|
23
|
+
- [builder] Original branding files copied to `ORIGINAL_BRANDING_PATH`
|
|
24
|
+
- [builder] Creating empty `BRANDING_PATH` for hotspot-specific branding
|
|
25
|
+
- [builder] Added mounts for `BRANDING_PATH` (and `ORIGINAL_BRANDING_PATH`) on all internal apps
|
|
26
|
+
- [builder] Catalog apps can mount `${BRANDING_PATH}` or `${ORIGINAL_BRANDING_PATH}`
|
|
27
|
+
|
|
28
|
+
## [2.1.0] - 2024-04-18
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- [inputs.checksum] `Checksum` gets a `to_dict()` method
|
|
33
|
+
- [dashboard] `Reader` gets an optional `checksum`
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- [builder] Removed useless download.kiwix.org special behavior for reader checksum. Checksum now on Reader
|
|
38
|
+
- [builder] Using captive-portal 1.4.1
|
|
39
|
+
- [inputs.base] Version-only base image def now targets uncompressed URL
|
|
40
|
+
- [inputs.base] BaseConfig gets an optional `checksum` and populates base_file accordingly
|
|
41
|
+
- [inputs.base] Version-only base image def now also retrieves Checksum via expected .md5 endpoint
|
|
42
|
+
|
|
8
43
|
## [2.0.0] - 2024-04-16
|
|
9
44
|
|
|
10
45
|
### Added
|
|
Binary file
|
|
@@ -3,10 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import PurePath as Path
|
|
5
5
|
from typing import Any
|
|
6
|
-
from urllib.parse import urlsplit
|
|
7
6
|
|
|
8
7
|
from offspot_config.catalog import app_catalog, get_app_path
|
|
9
|
-
from offspot_config.constants import
|
|
8
|
+
from offspot_config.constants import (
|
|
9
|
+
CONTENT_TARGET_PATH,
|
|
10
|
+
DATA_PART_PATH,
|
|
11
|
+
INTERNAL_BRANDING_PATH,
|
|
12
|
+
)
|
|
10
13
|
from offspot_config.inputs.base import BaseConfig
|
|
11
14
|
from offspot_config.inputs.checksum import Checksum
|
|
12
15
|
from offspot_config.inputs.file import FileConfig
|
|
@@ -14,7 +17,7 @@ from offspot_config.inputs.str import BlockStr
|
|
|
14
17
|
from offspot_config.oci_images import OCIImage
|
|
15
18
|
from offspot_config.packages import AppPackage, FilesPackage, ZimPackage
|
|
16
19
|
from offspot_config.utils.dashboard import Link, Reader
|
|
17
|
-
from offspot_config.utils.
|
|
20
|
+
from offspot_config.utils.misc import b64_encode
|
|
18
21
|
from offspot_config.utils.sizes import (
|
|
19
22
|
get_margin_for,
|
|
20
23
|
get_min_image_size_for,
|
|
@@ -36,14 +39,19 @@ METRICS_DATA_PATH = CONTENT_TARGET_PATH / "metrics"
|
|
|
36
39
|
KIWIXSERVE_DATA_PATH = CONTENT_TARGET_PATH / "zims"
|
|
37
40
|
METRICS_VAR_LOG_PATH_HOST = Path("/var/log/metrics")
|
|
38
41
|
METRICS_VAR_LOG_PATH_CONT = Path("/var/log/host/metrics")
|
|
42
|
+
# hotspot original branding material (usually kept as untouched)
|
|
43
|
+
ORIGINAL_BRANDING_PATH = DATA_PART_PATH / "branding"
|
|
44
|
+
# actual hotspot branding. default from orig, replaced in builder
|
|
45
|
+
BRANDING_PATH = CONTENT_TARGET_PATH / "branding"
|
|
46
|
+
|
|
39
47
|
KIWIX_ZIM_LOAD_BALANCER_URL = "https://download.kiwix.org/zim/"
|
|
40
48
|
|
|
41
49
|
# data source for “internal images” (out of catalog)
|
|
42
50
|
INTERNAL_IMAGES = {
|
|
43
51
|
"captive-portal": {
|
|
44
|
-
"source": "ghcr.io/offspot/captive-portal:1.4",
|
|
45
|
-
"filesize":
|
|
46
|
-
"fullsize":
|
|
52
|
+
"source": "ghcr.io/offspot/captive-portal:1.4.1",
|
|
53
|
+
"filesize": 189911040,
|
|
54
|
+
"fullsize": 189841976,
|
|
47
55
|
},
|
|
48
56
|
"dashboard": {
|
|
49
57
|
"source": "ghcr.io/offspot/dashboard:1.3.1",
|
|
@@ -61,9 +69,9 @@ INTERNAL_IMAGES = {
|
|
|
61
69
|
"fullsize": 58922600,
|
|
62
70
|
},
|
|
63
71
|
"kiwix-serve": {
|
|
64
|
-
"source": "ghcr.io/offspot/kiwix-serve:3.
|
|
65
|
-
"filesize":
|
|
66
|
-
"fullsize":
|
|
72
|
+
"source": "ghcr.io/offspot/kiwix-serve:3.7.0-1",
|
|
73
|
+
"filesize": 58480640,
|
|
74
|
+
"fullsize": 58443713,
|
|
67
75
|
},
|
|
68
76
|
"metrics": {
|
|
69
77
|
"source": "ghcr.io/offspot/metrics:0.3.0",
|
|
@@ -71,9 +79,9 @@ INTERNAL_IMAGES = {
|
|
|
71
79
|
"fullsize": 167202612,
|
|
72
80
|
},
|
|
73
81
|
"reverse-proxy": {
|
|
74
|
-
"source": "ghcr.io/offspot/reverse-proxy:1.
|
|
75
|
-
"filesize":
|
|
76
|
-
"fullsize":
|
|
82
|
+
"source": "ghcr.io/offspot/reverse-proxy:1.8",
|
|
83
|
+
"filesize": 120432640,
|
|
84
|
+
"fullsize": 120368490,
|
|
77
85
|
},
|
|
78
86
|
}
|
|
79
87
|
|
|
@@ -209,23 +217,12 @@ class ConfigBuilder:
|
|
|
209
217
|
|
|
210
218
|
# Add files for requested readers
|
|
211
219
|
for reader in self.dashboard_readers:
|
|
212
|
-
checksum = None
|
|
213
|
-
# download.kiwix.org is known to provide digests via mirrorbrain
|
|
214
|
-
if urlsplit(reader.download_url).netloc == "download.kiwix.org":
|
|
215
|
-
try:
|
|
216
|
-
checksum = Checksum(
|
|
217
|
-
algo="md5",
|
|
218
|
-
value=read_checksum_from(f"{reader.download_url}.md5"),
|
|
219
|
-
)
|
|
220
|
-
# we cant assume this this work forever
|
|
221
|
-
except Exception:
|
|
222
|
-
...
|
|
223
220
|
self.add_file(
|
|
224
221
|
url_or_content=reader.download_url,
|
|
225
222
|
to=str(KIWIXSERVE_DATA_PATH / reader.filename),
|
|
226
223
|
via="direct",
|
|
227
224
|
size=reader.size,
|
|
228
|
-
checksum=checksum,
|
|
225
|
+
checksum=reader.checksum,
|
|
229
226
|
is_url=True,
|
|
230
227
|
)
|
|
231
228
|
|
|
@@ -261,6 +258,18 @@ class ConfigBuilder:
|
|
|
261
258
|
"target": "/data/zims",
|
|
262
259
|
"read_only": True,
|
|
263
260
|
},
|
|
261
|
+
{
|
|
262
|
+
"type": "bind",
|
|
263
|
+
"source": BRANDING_PATH,
|
|
264
|
+
"target": "/var/www/branding",
|
|
265
|
+
"read_only": True,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"type": "bind",
|
|
269
|
+
"source": ORIGINAL_BRANDING_PATH,
|
|
270
|
+
"target": "/var/www/offspot.branding",
|
|
271
|
+
"read_only": True,
|
|
272
|
+
},
|
|
264
273
|
],
|
|
265
274
|
}
|
|
266
275
|
|
|
@@ -339,7 +348,19 @@ class ConfigBuilder:
|
|
|
339
348
|
"source": str(METRICS_VAR_LOG_PATH_HOST.parent),
|
|
340
349
|
"target": str(METRICS_VAR_LOG_PATH_CONT),
|
|
341
350
|
"read_only": False,
|
|
342
|
-
}
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"type": "bind",
|
|
354
|
+
"source": BRANDING_PATH,
|
|
355
|
+
"target": "/var/www/branding",
|
|
356
|
+
"read_only": True,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"type": "bind",
|
|
360
|
+
"source": ORIGINAL_BRANDING_PATH,
|
|
361
|
+
"target": "/var/www/offspot.branding",
|
|
362
|
+
"read_only": True,
|
|
363
|
+
},
|
|
343
364
|
],
|
|
344
365
|
}
|
|
345
366
|
|
|
@@ -396,7 +417,19 @@ class ConfigBuilder:
|
|
|
396
417
|
"source": "/var/run/internet",
|
|
397
418
|
"target": "/var/run/internet",
|
|
398
419
|
"read_only": True,
|
|
399
|
-
}
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"type": "bind",
|
|
423
|
+
"source": BRANDING_PATH,
|
|
424
|
+
"target": "/src/portal/branding",
|
|
425
|
+
"read_only": True,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"type": "bind",
|
|
429
|
+
"source": ORIGINAL_BRANDING_PATH,
|
|
430
|
+
"target": "/src/portal/offspot.branding",
|
|
431
|
+
"read_only": True,
|
|
432
|
+
},
|
|
400
433
|
],
|
|
401
434
|
}
|
|
402
435
|
|
|
@@ -454,6 +487,18 @@ class ConfigBuilder:
|
|
|
454
487
|
"target": in_container_packages_path,
|
|
455
488
|
"read_only": True,
|
|
456
489
|
},
|
|
490
|
+
{
|
|
491
|
+
"type": "bind",
|
|
492
|
+
"source": BRANDING_PATH,
|
|
493
|
+
"target": "/src/ui/branding",
|
|
494
|
+
"read_only": True,
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
"type": "bind",
|
|
498
|
+
"source": ORIGINAL_BRANDING_PATH,
|
|
499
|
+
"target": "/src/ui/offspot.branding",
|
|
500
|
+
"read_only": True,
|
|
501
|
+
},
|
|
457
502
|
],
|
|
458
503
|
}
|
|
459
504
|
|
|
@@ -481,6 +526,20 @@ class ConfigBuilder:
|
|
|
481
526
|
"restart": "unless-stopped",
|
|
482
527
|
"expose": ["80"],
|
|
483
528
|
"privileged": True,
|
|
529
|
+
"volumes": [
|
|
530
|
+
{
|
|
531
|
+
"type": "bind",
|
|
532
|
+
"source": BRANDING_PATH,
|
|
533
|
+
"target": "/src/branding",
|
|
534
|
+
"read_only": True,
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"type": "bind",
|
|
538
|
+
"source": ORIGINAL_BRANDING_PATH,
|
|
539
|
+
"target": "/src/offspot.branding",
|
|
540
|
+
"read_only": True,
|
|
541
|
+
},
|
|
542
|
+
],
|
|
484
543
|
}
|
|
485
544
|
|
|
486
545
|
self.protected_services.update(
|
|
@@ -536,7 +595,19 @@ class ConfigBuilder:
|
|
|
536
595
|
"source": f"{CONTENT_TARGET_PATH}/zims",
|
|
537
596
|
"target": "/data",
|
|
538
597
|
"read_only": True,
|
|
539
|
-
}
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"type": "bind",
|
|
601
|
+
"source": BRANDING_PATH,
|
|
602
|
+
"target": "/data/branding",
|
|
603
|
+
"read_only": True,
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
"type": "bind",
|
|
607
|
+
"source": ORIGINAL_BRANDING_PATH,
|
|
608
|
+
"target": "/data/offspot.branding",
|
|
609
|
+
"read_only": True,
|
|
610
|
+
},
|
|
540
611
|
],
|
|
541
612
|
"command": '/bin/sh -c "kiwix-serve --blockexternal '
|
|
542
613
|
'--port 80 --nodatealiases /data/*.zim"',
|
|
@@ -727,7 +798,19 @@ class ConfigBuilder:
|
|
|
727
798
|
"source": f"{CONTENT_TARGET_PATH}",
|
|
728
799
|
"target": "/data",
|
|
729
800
|
"read_only": True,
|
|
730
|
-
}
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
"type": "bind",
|
|
804
|
+
"source": BRANDING_PATH,
|
|
805
|
+
"target": "/branding",
|
|
806
|
+
"read_only": True,
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
"type": "bind",
|
|
810
|
+
"source": ORIGINAL_BRANDING_PATH,
|
|
811
|
+
"target": "/offspot.branding",
|
|
812
|
+
"read_only": True,
|
|
813
|
+
},
|
|
731
814
|
],
|
|
732
815
|
}
|
|
733
816
|
|
|
@@ -774,8 +857,11 @@ class ConfigBuilder:
|
|
|
774
857
|
if match:
|
|
775
858
|
repl = self.environ[match.groupdict()["var"]]
|
|
776
859
|
text = RE_ENVIRON_VAR.sub(repl, text)
|
|
777
|
-
resolved =
|
|
778
|
-
"${
|
|
860
|
+
resolved = (
|
|
861
|
+
text.replace("${FQDN}", self.fqdn)
|
|
862
|
+
.replace("${REVERSE_NAME}", "reverse-proxy")
|
|
863
|
+
.replace("${BRANDING_PATH}", str(BRANDING_PATH))
|
|
864
|
+
.replace("${ORIGINAL_BRANDING_PATH}", str(ORIGINAL_BRANDING_PATH))
|
|
779
865
|
)
|
|
780
866
|
if package:
|
|
781
867
|
app_dir = get_app_path(package=package)
|
|
@@ -809,7 +895,21 @@ class ConfigBuilder:
|
|
|
809
895
|
"""compute config based on requests"""
|
|
810
896
|
...
|
|
811
897
|
|
|
812
|
-
# add
|
|
898
|
+
# add original branding so apps can rely on it
|
|
899
|
+
self.ensure_host_path(ORIGINAL_BRANDING_PATH)
|
|
900
|
+
data = b""
|
|
901
|
+
for fpath in INTERNAL_BRANDING_PATH.iterdir():
|
|
902
|
+
data = fpath.read_bytes()
|
|
903
|
+
self.add_file(
|
|
904
|
+
url_or_content=b64_encode(data),
|
|
905
|
+
to=str(ORIGINAL_BRANDING_PATH.joinpath(fpath.name)),
|
|
906
|
+
via="base64",
|
|
907
|
+
size=len(data),
|
|
908
|
+
)
|
|
909
|
+
del data
|
|
910
|
+
|
|
911
|
+
# make sure branding folder exists for mounts
|
|
912
|
+
self.ensure_host_path(BRANDING_PATH)
|
|
813
913
|
|
|
814
914
|
# gen dashboard.yaml
|
|
815
915
|
if self.with_dashboard:
|
|
@@ -18,11 +18,14 @@ class File:
|
|
|
18
18
|
- size: optional[int]
|
|
19
19
|
size of (expanded) content. If specified, must be >= source file
|
|
20
20
|
- via: optional[str]
|
|
21
|
-
method to process source file
|
|
21
|
+
method to process source file. Values in File.unpack_formats
|
|
22
|
+
`base64` value only applies to `content`
|
|
23
|
+
other values not applicable if there's a `content` value
|
|
22
24
|
- url: optional[str]
|
|
23
25
|
URL to download file from
|
|
24
26
|
- content: optional[str]
|
|
25
27
|
plain text content to write to destination
|
|
28
|
+
can be base64 encoded (std) in which case via must be `base64`
|
|
26
29
|
|
|
27
30
|
one of content or url must be supplied. content has priority"""
|
|
28
31
|
|
|
@@ -31,9 +34,14 @@ class File:
|
|
|
31
34
|
unpack_formats: list[str]
|
|
32
35
|
|
|
33
36
|
def __init__(self, payload: dict[str, str | int | dict[str, str]]):
|
|
34
|
-
self.unpack_formats = ["direct", *SUPPORTED_UNPACKING_FORMATS]
|
|
37
|
+
self.unpack_formats = ["direct", "base64", *SUPPORTED_UNPACKING_FORMATS]
|
|
35
38
|
self.url: urllib.parse.ParseResult | None = None
|
|
39
|
+
self.to: pathlib.Path = pathlib.Path(str(payload["to"])).resolve()
|
|
40
|
+
self.via: str = str(payload.get("via", "direct"))
|
|
36
41
|
self.content: str = str(payload.get("content", "") or "").strip()
|
|
42
|
+
self.checksum: Checksum | None = None
|
|
43
|
+
self._size = -1
|
|
44
|
+
self._fullsize: int | None = None
|
|
37
45
|
|
|
38
46
|
if not self.content:
|
|
39
47
|
try:
|
|
@@ -41,24 +49,19 @@ class File:
|
|
|
41
49
|
except Exception as exc:
|
|
42
50
|
raise ValueError(f"URL “{payload.get('url')}” is incorrect") from exc
|
|
43
51
|
|
|
44
|
-
self.to: pathlib.Path = pathlib.Path(str(payload["to"])).resolve()
|
|
45
52
|
if not self.to.is_relative_to(DATA_PART_PATH):
|
|
46
53
|
raise ValueError(f"{self.to} not a descendent of {DATA_PART_PATH}")
|
|
47
54
|
|
|
48
|
-
self.via: str = str(payload.get("via", "direct"))
|
|
49
55
|
if self.via not in self.unpack_formats:
|
|
50
56
|
raise NotImplementedError(f"Unsupported handler `{self.via}`")
|
|
51
57
|
|
|
52
58
|
# optional checksum
|
|
53
|
-
self.checksum = None
|
|
54
59
|
if "checksum" in payload and isinstance(payload["checksum"], dict):
|
|
55
60
|
self.checksum = Checksum(**payload["checksum"])
|
|
56
61
|
|
|
57
|
-
# initialized
|
|
58
|
-
self._size = -1
|
|
62
|
+
# initialized as unknown
|
|
59
63
|
if "size" in payload and isinstance(payload["size"], (int, str)):
|
|
60
64
|
self._size: int = int(payload["size"])
|
|
61
|
-
self._fullsize: int | None = None
|
|
62
65
|
if "fullsize" in payload and isinstance(payload["fullsize"], (int, str)):
|
|
63
66
|
self._fullsize = int(payload["fullsize"])
|
|
64
67
|
|
|
@@ -104,6 +107,11 @@ class File:
|
|
|
104
107
|
"""whether a plain text content to be written"""
|
|
105
108
|
return bool(self.content)
|
|
106
109
|
|
|
110
|
+
@property
|
|
111
|
+
def is_base64_encoded(self) -> bool:
|
|
112
|
+
"""whether a plain text content to be written"""
|
|
113
|
+
return self.via == "base64"
|
|
114
|
+
|
|
107
115
|
@property
|
|
108
116
|
def is_local(self) -> bool:
|
|
109
117
|
"""whether referencing a local file"""
|
|
@@ -124,7 +132,9 @@ class File:
|
|
|
124
132
|
msg += f", url={self.geturl()}"
|
|
125
133
|
if self.content:
|
|
126
134
|
msg += f", content={self.content.splitlines()[0][:10]}"
|
|
127
|
-
msg += f", size={self.size}
|
|
135
|
+
msg += f", size={self.size}"
|
|
136
|
+
msg += f", checksum={self.checksum.as_aria if self.checksum else None}"
|
|
137
|
+
msg += ")"
|
|
128
138
|
return msg
|
|
129
139
|
|
|
130
140
|
def __str__(self) -> str:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from attrs import define
|
|
6
|
+
from typeguard import typechecked
|
|
7
|
+
|
|
8
|
+
from offspot_config.constants import DATA_PART_PATH
|
|
9
|
+
from offspot_config.file import File
|
|
10
|
+
from offspot_config.inputs.checksum import Checksum
|
|
11
|
+
from offspot_config.utils.download import read_checksum_from
|
|
12
|
+
from offspot_config.utils.misc import parse_size
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_base_from(base: BaseConfig) -> File:
|
|
16
|
+
"""Infer url from flexible `base` and return a File"""
|
|
17
|
+
payload: dict[str, str | int | dict[str, str]] = {
|
|
18
|
+
"url": str(base.source),
|
|
19
|
+
"to": str(DATA_PART_PATH / "-"),
|
|
20
|
+
}
|
|
21
|
+
match = re.match(
|
|
22
|
+
r"^(?P<version>\d\.\d\.\d)(?P<extra>[a-z0-9\-\.\_]*)", str(base.source)
|
|
23
|
+
)
|
|
24
|
+
if match:
|
|
25
|
+
version = "".join(match.groups())
|
|
26
|
+
payload["url"] = (
|
|
27
|
+
f"https://drive.offspot.it/base/offspot-base-arm64-{version}.img"
|
|
28
|
+
)
|
|
29
|
+
try:
|
|
30
|
+
payload["checksum"] = Checksum(
|
|
31
|
+
algo="md5", value=read_checksum_from(f"{payload['url']}.md5")
|
|
32
|
+
).to_dict()
|
|
33
|
+
except Exception:
|
|
34
|
+
...
|
|
35
|
+
elif isinstance(base.checksum, Checksum):
|
|
36
|
+
payload["checksum"] = base.checksum.to_dict()
|
|
37
|
+
return File(payload)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@typechecked
|
|
41
|
+
@define(kw_only=True)
|
|
42
|
+
class BaseConfig:
|
|
43
|
+
source: str | File
|
|
44
|
+
rootfs_size: str | int
|
|
45
|
+
checksum: dict[str, str] | Checksum | None = None
|
|
46
|
+
|
|
47
|
+
def __attrs_post_init__(self):
|
|
48
|
+
if self.rootfs_size and isinstance(self.rootfs_size, str):
|
|
49
|
+
self.rootfs_size = parse_size(self.rootfs_size)
|
|
50
|
+
if self.checksum and isinstance(self.checksum, dict):
|
|
51
|
+
self.checksum = Checksum(**self.checksum)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from attrs import define
|
|
3
|
+
from attrs import asdict, define
|
|
4
4
|
from typeguard import typechecked
|
|
5
5
|
|
|
6
6
|
from offspot_config.constants import SUPPORTED_CHECKSUM_ALGORITHMS
|
|
@@ -47,3 +47,6 @@ class Checksum:
|
|
|
47
47
|
def as_aria(self) -> str:
|
|
48
48
|
"""aria2-compatible checksum format: {algo}={digest}"""
|
|
49
49
|
return f"{self.algo}={self.digest}"
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, str]:
|
|
52
|
+
return asdict(self)
|
|
@@ -35,7 +35,7 @@ class MainConfig:
|
|
|
35
35
|
self.base = BaseConfig(**self.base)
|
|
36
36
|
|
|
37
37
|
if isinstance(self.base.source, str):
|
|
38
|
-
self.base_file = get_base_from(self.base
|
|
38
|
+
self.base_file = get_base_from(self.base)
|
|
39
39
|
|
|
40
40
|
if isinstance(self.output, dict):
|
|
41
41
|
self.output = OutputConfig(**self.output)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
WAYS = ("direct", "base64", "bztar", "gztar", "tar", "xztar", "zip")
|
|
@@ -4,6 +4,7 @@ import pathlib
|
|
|
4
4
|
import urllib.parse
|
|
5
5
|
from typing import NamedTuple, TypeVar
|
|
6
6
|
|
|
7
|
+
from offspot_config.inputs.checksum import Checksum
|
|
7
8
|
from offspot_config.utils.download import get_online_rsc_size
|
|
8
9
|
|
|
9
10
|
R = TypeVar("R", bound="Reader")
|
|
@@ -19,9 +20,15 @@ class Reader(NamedTuple):
|
|
|
19
20
|
download_url: str
|
|
20
21
|
filename: str
|
|
21
22
|
size: int
|
|
23
|
+
checksum: Checksum | None = None
|
|
22
24
|
|
|
23
25
|
def to_dict(self) -> dict[str, str | int]:
|
|
24
|
-
|
|
26
|
+
def value_or_dict(value):
|
|
27
|
+
if hasattr(value, "to_dict"):
|
|
28
|
+
return value.to_dict()
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
return {field: value_or_dict(getattr(self, field)) for field in self._fields}
|
|
25
32
|
|
|
26
33
|
@property
|
|
27
34
|
def order(self) -> int:
|
|
@@ -31,13 +38,16 @@ class Reader(NamedTuple):
|
|
|
31
38
|
)
|
|
32
39
|
|
|
33
40
|
@classmethod
|
|
34
|
-
def using(
|
|
41
|
+
def using(
|
|
42
|
+
cls: type[R], platform: str, download_url: str, checksum: Checksum | None = None
|
|
43
|
+
) -> R:
|
|
35
44
|
"""Reader from a platform name and download URL"""
|
|
36
45
|
return cls(
|
|
37
46
|
platform=platform,
|
|
38
47
|
download_url=download_url,
|
|
39
48
|
size=get_online_rsc_size(download_url),
|
|
40
49
|
filename=cls.filename_from_url(download_url),
|
|
50
|
+
checksum=checksum,
|
|
41
51
|
)
|
|
42
52
|
|
|
43
53
|
@staticmethod
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import datetime
|
|
4
5
|
import fnmatch
|
|
5
6
|
import inspect
|
|
@@ -142,6 +143,16 @@ def expand_file(src: pathlib.Path, method: str, dest: pathlib.Path):
|
|
|
142
143
|
fpath.unlink(missing_ok=True)
|
|
143
144
|
|
|
144
145
|
|
|
146
|
+
def b64_encode(data: bytes) -> str:
|
|
147
|
+
"""ASCII based64 repr of data"""
|
|
148
|
+
return base64.standard_b64encode(data).decode("ASCII")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def b64_decode(data: str) -> bytes:
|
|
152
|
+
"""ASCII based64 repr of data"""
|
|
153
|
+
return base64.standard_b64decode(data.encode("ASCII"))
|
|
154
|
+
|
|
155
|
+
|
|
145
156
|
class SimpleAttrs:
|
|
146
157
|
"""dict-like xattr wrapper to save specifying user. prefix"""
|
|
147
158
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.2.0"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest # pyright: ignore [reportMissingImports]
|
|
4
|
+
|
|
5
|
+
from offspot_config.inputs.checksum import Checksum
|
|
6
|
+
from offspot_config.inputs.mainconfig import MainConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_main_config(mini_config_yaml: str):
|
|
10
|
+
main_config = MainConfig.read_from(mini_config_yaml)
|
|
11
|
+
assert main_config
|
|
12
|
+
assert len(main_config.all_files) == 3
|
|
13
|
+
assert len(main_config.all_images) == 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.parametrize(
|
|
17
|
+
"config, md5sum",
|
|
18
|
+
[
|
|
19
|
+
(
|
|
20
|
+
"""
|
|
21
|
+
---
|
|
22
|
+
base:
|
|
23
|
+
source: http://yolo
|
|
24
|
+
rootfs_size: 2638217216
|
|
25
|
+
checksum:
|
|
26
|
+
algo: md5
|
|
27
|
+
value: 747fd0841b56d2d5158e0e65646b1be1
|
|
28
|
+
kind: digest
|
|
29
|
+
output:
|
|
30
|
+
size: auto
|
|
31
|
+
""",
|
|
32
|
+
"747fd0841b56d2d5158e0e65646b1be1",
|
|
33
|
+
),
|
|
34
|
+
(
|
|
35
|
+
"""
|
|
36
|
+
---
|
|
37
|
+
base:
|
|
38
|
+
source: http://yolo
|
|
39
|
+
rootfs_size: 2638217216
|
|
40
|
+
output:
|
|
41
|
+
size: auto
|
|
42
|
+
""",
|
|
43
|
+
None,
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
"""
|
|
47
|
+
---
|
|
48
|
+
base:
|
|
49
|
+
source: 1.2.1
|
|
50
|
+
rootfs_size: 2638217216
|
|
51
|
+
output:
|
|
52
|
+
size: auto
|
|
53
|
+
""",
|
|
54
|
+
"bb6fdcee36678ffcc88431dd502dfc11",
|
|
55
|
+
),
|
|
56
|
+
(
|
|
57
|
+
"""
|
|
58
|
+
---
|
|
59
|
+
base:
|
|
60
|
+
source: 1.2.1
|
|
61
|
+
rootfs_size: 2638217216
|
|
62
|
+
checksum:
|
|
63
|
+
algo: md5
|
|
64
|
+
value: YOLO
|
|
65
|
+
kind: digest
|
|
66
|
+
output:
|
|
67
|
+
size: auto
|
|
68
|
+
""",
|
|
69
|
+
"bb6fdcee36678ffcc88431dd502dfc11",
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"""
|
|
73
|
+
---
|
|
74
|
+
base:
|
|
75
|
+
source: 1.0.0
|
|
76
|
+
rootfs_size: 2638217216
|
|
77
|
+
output:
|
|
78
|
+
size: auto
|
|
79
|
+
""",
|
|
80
|
+
None,
|
|
81
|
+
),
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
def test_base_config(config: str, md5sum: str | None):
|
|
85
|
+
main_config = MainConfig.read_from(config)
|
|
86
|
+
assert main_config
|
|
87
|
+
assert len(main_config.all_files) == 0
|
|
88
|
+
assert len(main_config.all_images) == 0
|
|
89
|
+
assert main_config.base
|
|
90
|
+
assert main_config.base_file
|
|
91
|
+
checksum = Checksum(algo="md5", value=md5sum) if md5sum else None
|
|
92
|
+
assert main_config.base_file.checksum == checksum
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pytest # pyright: ignore [reportMissingImports]
|
|
2
2
|
|
|
3
|
+
from offspot_config.inputs.checksum import Checksum
|
|
3
4
|
from offspot_config.utils.dashboard import Reader
|
|
4
5
|
|
|
5
6
|
REALISTIC_VALUES = [
|
|
@@ -8,24 +9,28 @@ REALISTIC_VALUES = [
|
|
|
8
9
|
"https://download.kiwix.org/release/kiwix-desktop/kiwix-desktop_x86_64_2.3.1-4.appimage",
|
|
9
10
|
"kiwix-desktop_x86_64_2.3.1-4.appimage",
|
|
10
11
|
146629824,
|
|
12
|
+
"899279fb76e357afe33bbdd968750376",
|
|
11
13
|
),
|
|
12
14
|
(
|
|
13
15
|
"android",
|
|
14
16
|
"https://download.kiwix.org/release/kiwix-android/kiwix-3.9.1.apk",
|
|
15
17
|
"kiwix-3.9.1.apk",
|
|
16
18
|
79629012,
|
|
19
|
+
"d2ede8e23b4095718c508f44a341f687",
|
|
17
20
|
),
|
|
18
21
|
(
|
|
19
22
|
"windows",
|
|
20
23
|
"https://download.kiwix.org/release/kiwix-desktop/kiwix-desktop_windows_x64_2.3.1-2.zip",
|
|
21
24
|
"kiwix-desktop_windows_x64_2.3.1-2.zip",
|
|
22
25
|
126628211,
|
|
26
|
+
"adf9b64b5c6906427d7a1c8bdfc31546",
|
|
23
27
|
),
|
|
24
28
|
(
|
|
25
29
|
"macos",
|
|
26
30
|
"https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos_3.1.0.dmg",
|
|
27
31
|
"kiwix-desktop-macos_3.1.0.dmg",
|
|
28
32
|
16051402,
|
|
33
|
+
"747fd0841b56d2d5158e0e65646b1be1",
|
|
29
34
|
),
|
|
30
35
|
]
|
|
31
36
|
|
|
@@ -56,34 +61,73 @@ def test_filename_from_url(url: str, expected_filename: str):
|
|
|
56
61
|
assert Reader.filename_from_url(url) == expected_filename
|
|
57
62
|
|
|
58
63
|
|
|
64
|
+
def get_checksum_from(md5sum: str) -> Checksum:
|
|
65
|
+
return Checksum(algo="md5", value=md5sum)
|
|
66
|
+
|
|
67
|
+
|
|
59
68
|
def test_reader_is_tuple():
|
|
60
69
|
platform = "windows"
|
|
61
70
|
url = "http://some.tld/file"
|
|
62
71
|
filename = "one"
|
|
63
72
|
size = 4
|
|
64
|
-
|
|
73
|
+
checksum = None
|
|
74
|
+
assert Reader(platform, url, filename, size, checksum) == (
|
|
75
|
+
platform,
|
|
76
|
+
url,
|
|
77
|
+
filename,
|
|
78
|
+
size,
|
|
79
|
+
checksum,
|
|
80
|
+
)
|
|
65
81
|
assert Reader(
|
|
66
|
-
download_url=url,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
download_url=url,
|
|
83
|
+
size=size,
|
|
84
|
+
platform=platform,
|
|
85
|
+
filename=filename,
|
|
86
|
+
checksum=checksum,
|
|
87
|
+
) == (platform, url, filename, size, checksum)
|
|
88
|
+
assert isinstance(Reader(platform, url, filename, size, checksum), Reader)
|
|
89
|
+
assert isinstance(Reader(platform, url, filename, size, checksum), tuple)
|
|
90
|
+
tuple_ = (platform, url, filename, size, checksum)
|
|
71
91
|
casted = Reader._make(tuple_)
|
|
72
92
|
assert isinstance(casted, Reader)
|
|
73
93
|
|
|
74
94
|
|
|
75
|
-
@pytest.mark.parametrize(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
@pytest.mark.parametrize(
|
|
96
|
+
"platform, download_url, filename, size, md5sum", REALISTIC_VALUES
|
|
97
|
+
)
|
|
98
|
+
def test_reader_using(
|
|
99
|
+
platform: str, download_url: str, filename: str, size: int, md5sum: str
|
|
100
|
+
):
|
|
101
|
+
assert Reader.using(
|
|
102
|
+
platform, download_url, checksum=get_checksum_from(md5sum)
|
|
103
|
+
) == Reader(
|
|
104
|
+
platform,
|
|
105
|
+
download_url,
|
|
106
|
+
filename,
|
|
107
|
+
size,
|
|
108
|
+
get_checksum_from(md5sum),
|
|
109
|
+
)
|
|
110
|
+
assert Reader.using(platform=platform, download_url=download_url) == Reader(
|
|
111
|
+
platform,
|
|
112
|
+
download_url,
|
|
113
|
+
filename,
|
|
114
|
+
size,
|
|
115
|
+
None,
|
|
79
116
|
)
|
|
80
117
|
assert Reader.using(platform=platform, download_url=download_url) == Reader(
|
|
81
|
-
platform,
|
|
118
|
+
platform,
|
|
119
|
+
download_url,
|
|
120
|
+
filename,
|
|
121
|
+
size,
|
|
82
122
|
)
|
|
83
123
|
|
|
84
124
|
|
|
85
|
-
@pytest.mark.parametrize(
|
|
86
|
-
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
"platform, download_url, filename, size, md5sum", REALISTIC_VALUES
|
|
127
|
+
)
|
|
128
|
+
def test_invalid_data(
|
|
129
|
+
platform: str, download_url: str, filename: str, size: int, md5sum: str
|
|
130
|
+
):
|
|
87
131
|
with pytest.raises(TypeError):
|
|
88
132
|
Reader(platform, download_url, filename) # pyright: ignore [reportCallIssue]
|
|
89
133
|
with pytest.raises(TypeError):
|
|
@@ -92,6 +136,7 @@ def test_invalid_data(platform: str, download_url: str, filename: str, size: int
|
|
|
92
136
|
download_url,
|
|
93
137
|
filename,
|
|
94
138
|
size,
|
|
139
|
+
get_checksum_from(md5sum),
|
|
95
140
|
32, # pyright: ignore [reportCallIssue]
|
|
96
141
|
)
|
|
97
142
|
with pytest.raises(TypeError):
|
|
@@ -105,7 +150,10 @@ def test_invalid_data(platform: str, download_url: str, filename: str, size: int
|
|
|
105
150
|
|
|
106
151
|
|
|
107
152
|
def test_sort_order():
|
|
108
|
-
readers = [
|
|
153
|
+
readers = [
|
|
154
|
+
Reader(*values[:-1], checksum=get_checksum_from(md5sum=values[-1]))
|
|
155
|
+
for values in REALISTIC_VALUES
|
|
156
|
+
]
|
|
109
157
|
sorted_readers = sorted(readers, key=Reader.sort)
|
|
110
158
|
assert readers != sorted_readers
|
|
111
159
|
assert [r.platform for r in sorted_readers] == [
|
|
@@ -117,18 +165,20 @@ def test_sort_order():
|
|
|
117
165
|
|
|
118
166
|
|
|
119
167
|
@pytest.mark.parametrize(
|
|
120
|
-
"platform, download_url, filename, size, expected_dict",
|
|
168
|
+
"platform, download_url, filename, size, md5sum, expected_dict",
|
|
121
169
|
[
|
|
122
170
|
(
|
|
123
171
|
"android",
|
|
124
172
|
"https://download.kiwix.org/release/kiwix-android/kiwix-3.9.1.apk",
|
|
125
173
|
"kiwix-3.9.1.apk",
|
|
126
174
|
79629012,
|
|
175
|
+
None,
|
|
127
176
|
{
|
|
128
177
|
"platform": "android",
|
|
129
178
|
"download_url": "https://download.kiwix.org/release/kiwix-android/kiwix-3.9.1.apk",
|
|
130
179
|
"filename": "kiwix-3.9.1.apk",
|
|
131
180
|
"size": 79629012,
|
|
181
|
+
"checksum": None,
|
|
132
182
|
},
|
|
133
183
|
),
|
|
134
184
|
(
|
|
@@ -136,11 +186,17 @@ def test_sort_order():
|
|
|
136
186
|
"https://download.kiwix.org/release/kiwix-desktop/kiwix-desktop_windows_x64_2.3.1-2.zip",
|
|
137
187
|
"kiwix-desktop_windows_x64_2.3.1-2.zip",
|
|
138
188
|
126628211,
|
|
189
|
+
"adf9b64b5c6906427d7a1c8bdfc31546",
|
|
139
190
|
{
|
|
140
191
|
"platform": "windows",
|
|
141
192
|
"download_url": "https://download.kiwix.org/release/kiwix-desktop/kiwix-desktop_windows_x64_2.3.1-2.zip",
|
|
142
193
|
"filename": "kiwix-desktop_windows_x64_2.3.1-2.zip",
|
|
143
194
|
"size": 126628211,
|
|
195
|
+
"checksum": {
|
|
196
|
+
"algo": "md5",
|
|
197
|
+
"value": "adf9b64b5c6906427d7a1c8bdfc31546",
|
|
198
|
+
"kind": "digest",
|
|
199
|
+
},
|
|
144
200
|
},
|
|
145
201
|
),
|
|
146
202
|
(
|
|
@@ -148,18 +204,23 @@ def test_sort_order():
|
|
|
148
204
|
"https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos_3.1.0.dmg",
|
|
149
205
|
None,
|
|
150
206
|
None,
|
|
207
|
+
None,
|
|
151
208
|
{
|
|
152
209
|
"platform": "macos",
|
|
153
210
|
"download_url": "https://download.kiwix.org/release/kiwix-desktop-macos/kiwix-desktop-macos_3.1.0.dmg",
|
|
154
211
|
"filename": "kiwix-desktop-macos_3.1.0.dmg",
|
|
155
212
|
"size": 16051402,
|
|
213
|
+
"checksum": None,
|
|
156
214
|
},
|
|
157
215
|
),
|
|
158
216
|
],
|
|
159
217
|
)
|
|
160
|
-
def test_to_dict(platform, download_url, filename, size, expected_dict):
|
|
218
|
+
def test_to_dict(platform, download_url, filename, size, md5sum, expected_dict):
|
|
219
|
+
checksum = get_checksum_from(md5sum) if md5sum else None
|
|
161
220
|
if filename and size:
|
|
162
|
-
reader = Reader(platform, download_url, filename, size)
|
|
221
|
+
reader = Reader(platform, download_url, filename, size, checksum)
|
|
163
222
|
else:
|
|
164
|
-
reader = Reader.using(
|
|
223
|
+
reader = Reader.using(
|
|
224
|
+
platform=platform, download_url=download_url, checksum=checksum
|
|
225
|
+
)
|
|
165
226
|
assert reader.to_dict() == expected_dict
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
from attrs import define
|
|
6
|
-
from typeguard import typechecked
|
|
7
|
-
|
|
8
|
-
from offspot_config.constants import DATA_PART_PATH
|
|
9
|
-
from offspot_config.file import File
|
|
10
|
-
from offspot_config.utils.misc import parse_size
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_base_from(url: str) -> File:
|
|
14
|
-
"""Infer url from flexible `base` and return a File"""
|
|
15
|
-
match = re.match(r"^(?P<version>\d\.\d\.\d)(?P<extra>[a-z0-9\-\.\_]*)", url)
|
|
16
|
-
if match:
|
|
17
|
-
version = "".join(match.groups())
|
|
18
|
-
url = f"https://drive.offspot.it/base/offspot-base-arm64-{version}.img.xz"
|
|
19
|
-
return File({"url": url, "to": str(DATA_PART_PATH / "-")})
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@typechecked
|
|
23
|
-
@define(kw_only=True)
|
|
24
|
-
class BaseConfig:
|
|
25
|
-
source: str | File
|
|
26
|
-
rootfs_size: str | int
|
|
27
|
-
|
|
28
|
-
def __attrs_post_init__(self):
|
|
29
|
-
if self.rootfs_size and isinstance(self.rootfs_size, str):
|
|
30
|
-
self.rootfs_size = parse_size(self.rootfs_size)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
WAYS = ("direct", "bztar", "gztar", "tar", "xztar", "zip")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|