refactor: split serve.py and index.html into single-responsibility modules
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
2026-05-03 14:40:44 +02:00
|
|
|
#!/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)
|
|
|
|
|
|
2026-05-03 20:09:36 +02:00
|
|
|
def send_response(self, code, message=None):
|
|
|
|
|
if self.request_version != 'HTTP/0.9':
|
|
|
|
|
if message is None:
|
|
|
|
|
if code in self.responses:
|
|
|
|
|
message = self.responses[code][0]
|
|
|
|
|
else:
|
|
|
|
|
message = ''
|
|
|
|
|
if not hasattr(self, '_headers_buffer'):
|
|
|
|
|
self._headers_buffer = []
|
|
|
|
|
self._headers_buffer.append(("%s %d %s\r\n" %
|
|
|
|
|
(self.protocol_version, code, message)).encode('latin-1', 'strict'))
|
|
|
|
|
# Write Server and Date headers (mimicking base class behavior)
|
|
|
|
|
self.send_header('Server', self.version_string())
|
|
|
|
|
self.send_header('Date', self.date_time_string())
|
|
|
|
|
# Security headers
|
|
|
|
|
self.send_header('X-Content-Type-Options', 'nosniff')
|
|
|
|
|
self.send_header('X-Frame-Options', 'DENY')
|
|
|
|
|
self.send_header('Content-Security-Policy',
|
|
|
|
|
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 'none'")
|
|
|
|
|
|
refactor: split serve.py and index.html into single-responsibility modules
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
2026-05-03 14:40:44 +02:00
|
|
|
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)
|
2026-05-03 20:09:36 +02:00
|
|
|
self.send_error(500, "Failed to save file")
|
refactor: split serve.py and index.html into single-responsibility modules
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
2026-05-03 14:40:44 +02:00
|
|
|
|
|
|
|
|
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()
|