from __future__ import annotations import os from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles SANDBOX_DIR = Path(__file__).resolve().parent PROJECT_ROOT = SANDBOX_DIR.parents[2] STATIC_DIR = SANDBOX_DIR / "static" SHARED_STATIC_DIR = PROJECT_ROOT / "apps" / "web" / "vue3" / "static" DATA_ROOT = Path( os.environ.get( "AF3_SANDBOX_DATA_ROOT", "/home/xzq/5090/00test/af3/af3_outputs/gcn4_dimer_demo", ) ).resolve() ALLOWED_SUFFIXES = {".cif", ".mmcif"} MOLSTAR_VERSION = ( STATIC_DIR / "vendor" / "molstar" / "VERSION" ).read_text(encoding="utf-8").strip() app = FastAPI(title="AF3 Molstar Sandbox") app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") app.mount( "/shared-static", StaticFiles(directory=SHARED_STATIC_DIR), name="shared-static", ) def resolve_structure(filename: str) -> Path: requested = Path(filename) if requested.name != filename or requested.suffix.lower() not in ALLOWED_SUFFIXES: raise HTTPException(status_code=404, detail="structure not found") candidate = (DATA_ROOT / filename).resolve() if candidate.parent != DATA_ROOT or not candidate.is_file(): raise HTTPException(status_code=404, detail="structure not found") return candidate @app.get("/") def index() -> FileResponse: return FileResponse(SANDBOX_DIR / "index.html") @app.get("/api/health") def health() -> dict[str, object]: return { "status": "ok", "data_root": str(DATA_ROOT), "data_root_available": DATA_ROOT.is_dir(), "molstar_version": MOLSTAR_VERSION, } @app.get("/api/structures") def list_structures() -> dict[str, list[dict[str, object]]]: if not DATA_ROOT.is_dir(): return {"structures": []} structures = [ {"name": path.name, "size": path.stat().st_size} for path in DATA_ROOT.iterdir() if path.is_file() and path.suffix.lower() in ALLOWED_SUFFIXES ] structures.sort(key=lambda item: str(item["name"])) return {"structures": structures} @app.get("/api/structures/{filename}") def get_structure(filename: str) -> FileResponse: path = resolve_structure(filename) return FileResponse( path, media_type="chemical/x-mmcif", filename=path.name, )