""" Map Editor HTTP Server Serves editor/client/public/ as static files with smart routing: / -> index.html /page -> page.html or page/index.html /index -> index.html API endpoints (all paths relative to project root): GET /api/ls?path= directory listing GET /api/read?path= file contents POST /api/write?path= write file (JSON body: {"content":"..."}) POST /api/mkdir?path= create directory """ import os import json import mimetypes from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs, unquote from pathlib import Path PORT = 3000 SCRIPT_DIR = Path(__file__).parent.resolve() PUBLIC_DIR = SCRIPT_DIR.parent / "client" / "public" PROJECT_ROOT = SCRIPT_DIR.parent.parent.resolve() class Handler(BaseHTTPRequestHandler): def do_GET(self): parsed = urlparse(self.path) path = unquote(parsed.path) if path.startswith("/api/"): params = parse_qs(parsed.query) if path == "/api/ls": self.api_ls(params) elif path == "/api/read": self.api_read(params) else: self.send_error(404) else: self.serve_static(path) def do_POST(self): parsed = urlparse(self.path) params = parse_qs(parsed.query) if parsed.path == "/api/write": self.api_write(params) elif parsed.path == "/api/mkdir": self.api_mkdir(params) else: self.send_error(404) # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ def resolve_project_path(self, rel): rel = rel.lstrip("/") target = (PROJECT_ROOT / rel).resolve() if not str(target).startswith(str(PROJECT_ROOT)): return None return target def send_json(self, data, code=200): body = json.dumps(data, indent=2).encode("utf-8") self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", len(body)) self.end_headers() self.wfile.write(body) def read_body(self): length = int(self.headers.get("Content-Length", 0)) return self.rfile.read(length) # ------------------------------------------------------------------ # API handlers # ------------------------------------------------------------------ def api_ls(self, params): rel = params.get("path", [""])[0] target = self.resolve_project_path(rel) if target is None: self.send_json({"error": "invalid path"}, 400) return if not target.exists(): self.send_json({"error": "path not found"}, 404) return if not target.is_dir(): self.send_json({"error": "not a directory"}, 400) return entries = [] for entry in sorted(target.iterdir(), key=lambda e: (e.is_file(), e.name)): entries.append({ "name": entry.name, "type": "dir" if entry.is_dir() else "file", "size": entry.stat().st_size if entry.is_file() else None, }) self.send_json({ "path": str(target.relative_to(PROJECT_ROOT)), "entries": entries, }) def api_read(self, params): rel = params.get("path", [""])[0] target = self.resolve_project_path(rel) if target is None: self.send_json({"error": "invalid path"}, 400) return if not target.exists(): self.send_json({"error": "file not found"}, 404) return if not target.is_file(): self.send_json({"error": "not a file"}, 400) return try: content = target.read_text(encoding="utf-8") self.send_json({ "path": str(target.relative_to(PROJECT_ROOT)), "content": content, }) except UnicodeDecodeError: import base64 content = base64.b64encode(target.read_bytes()).decode("ascii") self.send_json({ "path": str(target.relative_to(PROJECT_ROOT)), "content": content, "encoding": "base64", }) def api_write(self, params): rel = params.get("path", [""])[0] target = self.resolve_project_path(rel) if target is None: self.send_json({"error": "invalid path"}, 400) return body = self.read_body() try: data = json.loads(body) content = data.get("content", "") except (json.JSONDecodeError, UnicodeDecodeError): content = body.decode("utf-8") target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content, encoding="utf-8") self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))}) def api_mkdir(self, params): rel = params.get("path", [""])[0] target = self.resolve_project_path(rel) if target is None: self.send_json({"error": "invalid path"}, 400) return target.mkdir(parents=True, exist_ok=True) self.send_json({"ok": True, "path": str(target.relative_to(PROJECT_ROOT))}) # ------------------------------------------------------------------ # Static file serving # ------------------------------------------------------------------ def serve_static(self, path): clean = path.lstrip("/") candidates = [] if clean == "" or clean == "index" or clean == "index.html": candidates = ["index.html"] else: candidates.append(clean) if "." not in Path(clean).name: candidates.append(clean + ".html") candidates.append(clean + "/index.html") for candidate in candidates: file_path = PUBLIC_DIR / candidate if file_path.is_file(): self.serve_file(file_path) return self.send_error(404) def serve_file(self, file_path): mime, _ = mimetypes.guess_type(str(file_path)) mime = mime or "application/octet-stream" template = PUBLIC_DIR / "template.html" if ( mime == "text/html" and file_path.name != "template.html" and template.is_file() ): fragment = file_path.read_text(encoding="utf-8") shell = template.read_text(encoding="utf-8") content = shell.replace( '
', f'
{fragment}
', 1, ).encode("utf-8") else: content = file_path.read_bytes() self.send_response(200) self.send_header("Content-Type", mime) self.send_header("Content-Length", len(content)) self.end_headers() self.wfile.write(content) def log_message(self, fmt, *args): print(f" {self.address_string()} {fmt % args}") if __name__ == "__main__": print(f"Map Editor") print(f" http://localhost:{PORT}") print(f" public : {PUBLIC_DIR}") print(f" project: {PROJECT_ROOT}") server = HTTPServer(("", PORT), Handler) try: server.serve_forever() except KeyboardInterrupt: print("\nStopped.")