Backend: - path_validator.py: PathValidator-Klasse für Pfad-Validierung - file_ops.py: read_file, write_file, directory_exists, file_exists - content_types.py: get_content_type mit EXTENSION_MAP - handler.py: Handler-Klasse mit do_GET/do_PUT, nutzt above modules - serve.py: Entry-Point (main, find_free_port), setzt Handler.validator/directory Frontend: - css/variables.css: CSS-Variablen (--bg-*, --text-*, --accent, etc.) - css/styles.css: Alle CSS-Regeln (modal, card, template-grid, etc.) - js/utils.js: esc, showToast, copyContentToClipboard - js/modal.js: showModal, closeModal, closeEditModal, wasViewModalOpen - js/editor.js: editModalContent, createJsonEditUI, extractJsonFromForm - js/api.js: viewTemplate, copyContent, loadTemplates, saveEditedContent - js/templates.js: renderTemplates, applyFilters, parseTypeFromHash - js/main.js: Event-Listener, Hash-Filter, Initialisierung - index.html: Inline-CSS/JS entfernt, <link>/<script src>-Tags hinzugefügt Smoke test: SO_REUSEADDR für schnelle Port-Wiederverwendung
107 lines
3.6 KiB
Python
107 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
HTTP-Handler für den Template-Server.
|
|
|
|
Behandelt GET- und PUT-Anfragen für Templates und das Frontend.
|
|
"""
|
|
|
|
import http.server
|
|
import logging
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
# Support für direkte Ausführung und Package-Import
|
|
try:
|
|
from .path_validator import PathValidator
|
|
from .file_ops import read_file_binary, write_file, directory_exists
|
|
from .content_types import get_content_type
|
|
except ImportError:
|
|
from path_validator import PathValidator
|
|
from file_ops import read_file_binary, write_file, directory_exists
|
|
from content_types import get_content_type
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MAX_BODY_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
|
|
|
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
"""HTTP-Handler für Template-Anfragen."""
|
|
|
|
validator = None # Wird von serve.py gesetzt
|
|
directory = None # Wird von serve.py gesetzt
|
|
|
|
def __init__(self, *args, directory=None, **kwargs):
|
|
super().__init__(*args, directory=directory or self.directory, **kwargs)
|
|
|
|
def do_PUT(self):
|
|
"""Speichert eine Template-Datei."""
|
|
file_path = self.validator.resolve_template_path(urlparse(self.path).path)
|
|
if file_path is None:
|
|
self.send_error(403, "Forbidden: Invalid path")
|
|
return
|
|
|
|
# Verzeichnis prüfen - muss existieren
|
|
file_dir = Path(file_path).parent
|
|
if not directory_exists(str(file_dir)):
|
|
self.send_error(404, "Directory not found")
|
|
return
|
|
|
|
# Content-Type prüfen
|
|
content_type = self.headers.get("Content-Type", "")
|
|
if "text/plain" not in content_type and not content_type.startswith("text/"):
|
|
self.send_error(400, "Unsupported content type")
|
|
return
|
|
|
|
# Inhalt lesen und speichern
|
|
content_length = int(self.headers.get("Content-Length", 0))
|
|
if content_length > MAX_BODY_SIZE:
|
|
self.send_error(413, "Request body too large")
|
|
return
|
|
if content_length <= 0:
|
|
self.send_error(400, "No content provided")
|
|
return
|
|
|
|
try:
|
|
file_content = self.rfile.read(content_length)
|
|
|
|
if not write_file(file_path, file_content):
|
|
raise Exception("write_file returned False")
|
|
|
|
response_content_type = get_content_type(file_path)
|
|
|
|
self.send_response(200)
|
|
self.send_header("Content-type", response_content_type)
|
|
self.end_headers()
|
|
self.wfile.write(b"File saved successfully")
|
|
except Exception as e:
|
|
logger.error("Failed to save file: %s", e)
|
|
self.send_error(500, f"Failed to save file: {e}")
|
|
|
|
def do_GET(self):
|
|
"""Liefert Dateien aus."""
|
|
parsed_path = urlparse(self.path).path
|
|
|
|
# Für Root-Pfad: index.html servieren
|
|
if parsed_path == "/" or parsed_path == "/index.html":
|
|
self.path = "/index.html"
|
|
return super().do_GET()
|
|
|
|
# Anfragen für /templates.json oder /templates/* umleiten
|
|
file_path = self.validator.resolve_template_path(urlparse(self.path).path)
|
|
if file_path is not None:
|
|
content_type = get_content_type(file_path)
|
|
|
|
file_content = read_file_binary(file_path)
|
|
if file_content is None:
|
|
self.send_error(404, "File not found")
|
|
return
|
|
|
|
self.send_response(200)
|
|
self.send_header("Content-type", content_type)
|
|
self.end_headers()
|
|
self.wfile.write(file_content)
|
|
return
|
|
|
|
return super().do_GET()
|