#!/usr/bin/env python3 """Install the initial, stateless prototype. Refuse upgrades and any existing data.""" import fcntl import hashlib import json import os from pathlib import Path import re import shutil import subprocess import sys import tarfile import tempfile import time import urllib.request import urllib.error home = Path.home() app = home / 'opt/werkjournal' site = home / 'doms/werkjournal.javagil.de/htdocs-ssl' http_site = home / 'doms/werkjournal.javagil.de/htdocs' jdk = home / 'opt/jdk25' unit = home / '.config/systemd/user/werkjournal-backend.service' service = 'werkjournal-backend.service' def run(*args): return subprocess.run(args, check=True, text=True, capture_output=True).stdout def atomic(path, content, mode=0o644): temporary = path.with_name(path.name + '.new') temporary.write_text(content) temporary.chmod(mode) temporary.replace(path) def wait_ready(): deadline = time.monotonic() + 90 while time.monotonic() < deadline: try: with urllib.request.urlopen('http://127.0.0.1:18090/api/v1/info', timeout=3) as response: info = json.load(response) if info.get('application') == 'werkjournal' and info.get('version') == manifest['version']: return except (OSError, ValueError): pass time.sleep(2) raise RuntimeError('Prototype readiness check failed') if not site.is_dir() or not http_site.is_dir() or not (jdk / 'bin/java').is_file(): raise SystemExit('Expected Hostsharing domain directories and managed JDK must exist') app.mkdir(parents=True, exist_ok=True, mode=0o700) with (app / '.deploy.lock').open('a') as lock: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) if (app / 'current').exists() or (app / 'current').is_symlink() or unit.exists() or list(app.rglob('*.mv.db')): raise SystemExit('Initial deployment only: existing service, release or data requires the full upgrade procedure') # Refuse unrelated listeners before touching the website. import socket with socket.socket() as probe: probe.bind(('127.0.0.1', 18090)) stage = Path(tempfile.mkdtemp(prefix='candidate-', dir=app)) try: seen = set() with tarfile.open(fileobj=sys.stdin.buffer, mode='r|') as archive: for entry in archive: if entry.name not in {'manifest.json', 'werkjournal.jar'} or entry.name in seen or not entry.isfile(): raise ValueError('Unexpected archive member') if entry.size > 300 * 1024 * 1024: raise ValueError('Artifact too large') seen.add(entry.name) with archive.extractfile(entry) as source, (stage / entry.name).open('wb') as destination: shutil.copyfileobj(source, destination) if seen != {'manifest.json', 'werkjournal.jar'}: raise ValueError('Incomplete artifact') manifest = json.loads((stage / 'manifest.json').read_text()) with (stage / 'werkjournal.jar').open('rb') as source: checksum = hashlib.file_digest(source, 'sha256').hexdigest() if (manifest.get('schemaVersion') != 1 or manifest.get('runtime') != 'java-25' or not re.fullmatch(r'[0-9a-f]{40}', manifest.get('commit', '')) or checksum != manifest.get('sha256') or manifest.get('files') != {'werkjournal.jar': checksum}): raise ValueError('Unverified artifact') release = app / 'releases' / manifest['commit'] release.parent.mkdir(exist_ok=True) if release.exists(): raise ValueError('Release path already exists') stage.rename(release) finally: if stage.exists(): shutil.rmtree(stage) backup = app / 'initial-site-backup' backup.mkdir(mode=0o700) for root, label in [(site, 'https'), (http_site, 'http')]: destination = backup / label destination.mkdir() for name in ['.htaccess', 'index.html']: path = root / name if path.exists(): shutil.copy2(path, destination / name) atomic(site / 'maintenance.html', '
Wartungsmodus – bitte in wenigen Minuten erneut versuchen.
Maintenance — please try again in a few minutes.
') atomic(site / 'maintenance.flag', 'Initial deployment\n') atomic(site / '.htaccess', '''DirectoryIndex disabled RewriteEngine On ErrorDocument 503 /maintenance.html RewriteRule ^maintenance\\.html$ - [L] RewriteRule ^\\.well-known/acme-challenge/ - [L] RewriteCond %{DOCUMENT_ROOT}/maintenance.flag -f RewriteRule ^ - [R=503,L] RequestHeader set X-Forwarded-Proto "https" RequestHeader set X-Forwarded-Port "443" RequestHeader set X-Forwarded-Host "werkjournal.javagil.de" RewriteRule ^(.*)$ http://127.0.0.1:18090/$1?%{QUERY_STRING} [P,L]