#!/usr/bin/env python3 """Serve the local VoxelForge web viewer and a folder of converted .glb meshes.""" import argparse import json from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlsplit APP_DIR = Path(__file__).resolve().parent def make_handler(mesh_dir): root = mesh_dir.resolve() class Handler(SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=str(APP_DIR), **kwargs) def do_GET(self): route = urlsplit(self.path).path if route == "/api/meshes": meshes = [] for path in sorted(root.glob("*.glb"), key=lambda item: item.name.casefold()): try: resolved = path.resolve(strict=True) if resolved.parent == root and resolved.is_file(): meshes.append({"name": path.name, "bytes": resolved.stat().st_size}) except OSError: continue body = json.dumps(meshes, separators=(",", ":")).encode("utf-8") self.send_response(200) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.send_header("Cache-Control", "no-store") self.end_headers() self.wfile.write(body) return if route.startswith("/meshes/"): name = unquote(route[len("/meshes/"):]) if not name or Path(name).name != name or not name.endswith(".glb"): self.send_error(404) return path = root / name try: resolved = path.resolve(strict=True) if resolved.parent != root or not resolved.is_file(): raise OSError("not a regular mesh file") size = resolved.stat().st_size self.send_response(200) self.send_header("Content-Type", "model/gltf-binary") self.send_header("Content-Length", str(size)) self.send_header("Cache-Control", "public, max-age=300") self.end_headers() with resolved.open("rb") as mesh: self.copyfile(mesh, self.wfile) except OSError: self.send_error(404) return super().do_GET() def log_message(self, fmt, *args): print(f"[{self.log_date_time_string()}] {self.address_string()} {fmt % args}") return Handler def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--mesh-dir", type=Path, default=APP_DIR / "meshes", help="folder containing converted .glb files") parser.add_argument("--port", type=int, default=8000) args = parser.parse_args() if not 0 < args.port < 65536: parser.error("--port must be between 1 and 65535") args.mesh_dir.mkdir(parents=True, exist_ok=True) server = ThreadingHTTPServer(("127.0.0.1", args.port), make_handler(args.mesh_dir)) server.daemon_threads = True print(f"VoxelForge viewer: http://127.0.0.1:{args.port}/") print(f"Mesh folder: {args.mesh_dir.resolve()}") try: server.serve_forever() except KeyboardInterrupt: print("Stopping viewer server") finally: server.server_close() if __name__ == "__main__": main()