Spaces:
Sleeping
Sleeping
| """Router des rapports HTML générés.""" | |
| from __future__ import annotations | |
| import asyncio | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from fastapi import APIRouter, HTTPException, Query | |
| from fastapi.responses import HTMLResponse | |
| router = APIRouter() | |
| async def api_reports( | |
| reports_dir: str = Query(default=".", description="Dossier rapports"), | |
| ) -> dict: | |
| """Liste les rapports HTML disponibles dans ``reports_dir``, ``.`` et ``./rapports``. | |
| Le scan disque (``glob``, ``stat``) est exécuté dans un thread | |
| pour ne pas bloquer l'event loop si le dossier contient des | |
| centaines de rapports. | |
| """ | |
| return await asyncio.to_thread(_list_reports_sync, reports_dir) | |
| def _list_reports_sync(reports_dir: str) -> dict: | |
| target = Path(reports_dir).resolve() | |
| reports: list[dict] = [] | |
| search_dirs = [target, Path(".").resolve(), Path("./rapports").resolve()] | |
| seen: set[str] = set() | |
| for d in search_dirs: | |
| if not d.exists(): | |
| continue | |
| for f in sorted(d.glob("*.html"), key=lambda x: x.stat().st_mtime, reverse=True): | |
| if str(f) not in seen: | |
| seen.add(str(f)) | |
| stat = f.stat() | |
| reports.append({ | |
| "filename": f.name, | |
| "path": str(f), | |
| "size_kb": round(stat.st_size / 1024, 1), | |
| "modified": datetime.fromtimestamp( | |
| stat.st_mtime, tz=timezone.utc, | |
| ).isoformat(), | |
| "url": f"/reports/{f.name}", | |
| }) | |
| return {"reports": reports} | |
| async def serve_report(filename: str) -> HTMLResponse: | |
| """Sert un rapport HTML par son nom de fichier. | |
| Lit le contenu et le renvoie en ``text/html`` plutôt que de | |
| rediriger vers un fichier statique : indispensable pour que ça | |
| fonctionne depuis un Codespace ou tout reverse-proxy distant. | |
| La lecture est déléguée à un thread car les rapports peuvent | |
| atteindre plusieurs Mo (Chart.js inline, données denses). | |
| """ | |
| # Sécurité : interdire les path traversal | |
| if "/" in filename or "\\" in filename or ".." in filename: | |
| raise HTTPException(status_code=400, detail="Nom de fichier invalide") | |
| content = await asyncio.to_thread(_read_report_sync, filename) | |
| return HTMLResponse(content=content) | |
| def _read_report_sync(filename: str) -> str: | |
| for d in [Path("."), Path("./rapports")]: | |
| f = d / filename | |
| if f.exists() and f.suffix == ".html": | |
| return f.read_text(encoding="utf-8") | |
| raise HTTPException(status_code=404, detail=f"Rapport non trouvé : {filename}") | |