#!/usr/bin/env python3 """Static server for the post generator, plus a save endpoint. The browser download route turned out to be unreliable here: with a data: URL it opened a blank tab, with a blob: URL it opened the image in a tab. Both times the download attribute was ignored. Since this only ever runs locally, the generator now POSTs the rendered bytes to /save and the file is written straight to the Downloads folder. No browser download machinery involved. """ import http.server import os import re import socketserver from pathlib import Path PORT = 4321 ROOT = Path(__file__).resolve().parent TARGET = Path.home() / "Downloads" SAFE_NAME = re.compile(r"^[A-Za-z0-9._-]{1,120}$") class Handler(http.server.SimpleHTTPRequestHandler): def __init__(self, *a, **kw): super().__init__(*a, directory=str(ROOT), **kw) def do_POST(self): if self.path.split("?")[0] != "/save": self.send_error(404) return name = self.headers.get("X-Filename", "") # The filename comes from the page, so it is never trusted: no paths, no # traversal, plain basename only. name = os.path.basename(name) if not SAFE_NAME.match(name): self._json(400, '{"error":"ungueltiger Dateiname"}') return length = int(self.headers.get("Content-Length", 0)) if length <= 0 or length > 80 * 1024 * 1024: self._json(400, '{"error":"ungueltige Groesse"}') return data = self.rfile.read(length) TARGET.mkdir(parents=True, exist_ok=True) dest = TARGET / name stem, suffix = dest.stem, dest.suffix n = 1 while dest.exists(): dest = TARGET / f"{stem}-{n}{suffix}" n += 1 dest.write_bytes(data) print(f"gespeichert: {dest} ({len(data)/1024:.0f} KB)") self._json(200, '{"path":"%s"}' % dest) def _json(self, code, body): payload = body.encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) def end_headers(self): # Werkzeug, kein Auslieferungsserver: Assets werden hier laufend # ausgetauscht, und ein gecachter Screenshot unter gleichem Dateinamen # kostet mehr Zeit als jeder erneute Ladevorgang. self.send_header("Cache-Control", "no-store, must-revalidate") self.send_header("Pragma", "no-cache") self.send_header("Expires", "0") super().end_headers() def log_message(self, fmt, *args): if "POST" in (args[0] if args else ""): super().log_message(fmt, *args) class Server(socketserver.ThreadingTCPServer): allow_reuse_address = True if __name__ == "__main__": print(f"Generator: http://127.0.0.1:{PORT}/post1.html") print(f"Speichert nach: {TARGET}") with Server(("127.0.0.1", PORT), Handler) as httpd: httpd.serve_forever()