Chunks, models, and more
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
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=<dir> directory listing
|
||||
GET /api/read?path=<file> file contents
|
||||
POST /api/write?path=<file> write file (JSON body: {"content":"..."})
|
||||
POST /api/mkdir?path=<dir> 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(
|
||||
'<div id="editor"></div>',
|
||||
f'<div id="editor">{fragment}</div>',
|
||||
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.")
|
||||
Reference in New Issue
Block a user