79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Fetch pinned upstream assets ONCE; browsers only load local copies.
|
|
|
|
Supply --from-directory for air-gapped deployment. Integrity checks are identical.
|
|
No npm, Node, CDN requests from the browser, or mutable 'latest' URLs.
|
|
"""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
from urllib.request import urlopen
|
|
|
|
ASSETS = {
|
|
'bootstrap.min.css': (
|
|
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css',
|
|
'sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB'),
|
|
'htmx.min.js': (
|
|
'https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js',
|
|
'H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V'),
|
|
}
|
|
|
|
|
|
def valid(data: bytes, expected: str) -> bool:
|
|
return base64.b64encode(hashlib.sha384(data).digest()).decode('ascii') == expected
|
|
|
|
|
|
def prepare(destination: Path, offline: Path | None = None, *, reuse: Path | None = None) -> None:
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
for name, (url, integrity) in ASSETS.items():
|
|
target = destination / name
|
|
if target.is_file() and valid(target.read_bytes(), integrity):
|
|
print('Verified local asset:', name)
|
|
continue
|
|
if offline:
|
|
data = (offline / name).read_bytes()
|
|
elif reuse and (reuse / name).is_file() and not (reuse / name).is_symlink():
|
|
candidate = (reuse / name).read_bytes()
|
|
if valid(candidate, integrity):
|
|
data = candidate
|
|
print('Reusing verified installed asset:', name)
|
|
else:
|
|
with urlopen(url, timeout=30) as response:
|
|
data = response.read(2_000_001)
|
|
else:
|
|
with urlopen(url, timeout=30) as response:
|
|
data = response.read(2_000_001)
|
|
if len(data) > 2_000_000 or not valid(data, integrity):
|
|
raise ValueError(f'Upstream integrity mismatch: {name}. No asset was installed.')
|
|
fd, filename = tempfile.mkstemp(prefix='.asset-', dir=destination)
|
|
temporary = Path(filename)
|
|
try:
|
|
with os.fdopen(fd, 'wb') as stream:
|
|
stream.write(data)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
temporary.chmod(0o644)
|
|
os.replace(temporary, target)
|
|
finally:
|
|
temporary.unlink(missing_ok=True)
|
|
print('Installed verified local asset:', name)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--from-directory', type=Path)
|
|
parser.add_argument('--destination', type=Path, default=Path(__file__).resolve().parents[1] / 'src/aim_webgui/static/vendor')
|
|
args = parser.parse_args()
|
|
try:
|
|
prepare(args.destination, args.from_directory)
|
|
except Exception as exc:
|
|
parser.exit(1, f'Assets not prepared: {exc}\nUse --from-directory with the exact pinned upstream files for offline installation.\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|