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
This commit is contained in:
commit
124524482b
34 files changed
+6129
No files matched your search
@@ -0,0 +1,78 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user