Files
posts/serve.py
T
raffael 124524482b HiveGrid Social: Design-Library und Post-Generator
Autarke Werkbank für Social-Grafiken: Slides als HTML, Bearbeitung im Canvas,
Export als Instagram-taugliche PNGs.

- Assets nach brand/partner/mockups/screens sortiert, Karteileichen entfernt
- G-Marke als eigene Datei statt nur als Polygon im Code
- Partner-Logos und Geerkens-Logo ergänzt
- Keine externen Referenzen: Schrift, Bibliotheken und Bilder liegen lokal
2026-08-01 18:36:18 +02:00

79 lines
2.6 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 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()