Files
posts/serve.py
T
dejanandClaude Opus 5 f672635035 Server liefert ohne Cache aus
Screenshots werden hier laufend unter gleichem Dateinamen ausgetauscht. Der
Browser hat dann die alte Fassung gezeigt, obwohl die Datei auf der Platte
schon die neue war. Das ist ein Werkzeug, kein Auslieferungsserver: ein
erneuter Ladevorgang kostet weniger als die Suche nach dem Grund.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:35:49 +02:00

88 lines
3.0 KiB
Python
Executable File

#!/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()