From d0c29f3c5097d4d5e1816857510af4d72e3b2807 Mon Sep 17 00:00:00 2001 From: Thierry Date: Wed, 1 Apr 2026 21:33:24 +0200 Subject: [PATCH] Upload files to "frontend" --- frontend/api_client.py | 57 ++++++++++++++++++++++++++++++++++++++++++ frontend/config.py | 31 +++++++++++++++++++++++ frontend/main.py | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 frontend/api_client.py create mode 100644 frontend/config.py create mode 100644 frontend/main.py diff --git a/frontend/api_client.py b/frontend/api_client.py new file mode 100644 index 0000000..03c1592 --- /dev/null +++ b/frontend/api_client.py @@ -0,0 +1,57 @@ +# api_client.py - httpx async + +import httpx +from config import BACKEND_URL + +TIMEOUT_HEALTH = 5 +TIMEOUT_UPLOAD = 3600 +TIMEOUT_DEFAULT = 15 + + +async def check_health() -> dict: + async with httpx.AsyncClient() as client: + r = await client.get(f"{BACKEND_URL}/health", timeout=TIMEOUT_HEALTH) + r.raise_for_status() + return r.json() + + +async def upload_file(filename: str, data: bytes) -> dict: + async with httpx.AsyncClient() as client: + r = await client.post( + f"{BACKEND_URL}/upload", + files={"file": (filename, data)}, + timeout=TIMEOUT_UPLOAD, + ) + if r.status_code != 200: + raise RuntimeError(f"Erreur backend ({r.status_code}) : {r.text}") + return r.json() + + +async def get_debug(pc_id: str) -> dict: + async with httpx.AsyncClient() as client: + r = await client.get( + f"{BACKEND_URL}/debug/{pc_id}", timeout=TIMEOUT_DEFAULT + ) + r.raise_for_status() + return r.json() + + +async def delete_pointcloud(pc_id: str) -> dict: + async with httpx.AsyncClient() as client: + r = await client.delete( + f"{BACKEND_URL}/delete/{pc_id}", timeout=TIMEOUT_DEFAULT + ) + r.raise_for_status() + return r.json() + + +async def crop_pointcloud(pc_id: str, payload: dict) -> dict: + async with httpx.AsyncClient() as client: + r = await client.post( + f"{BACKEND_URL}/crop/{pc_id}", + json=payload, + timeout=TIMEOUT_UPLOAD, + ) + if r.status_code != 200: + raise RuntimeError(f"Erreur crop ({r.status_code}) : {r.text}") + return r.json() diff --git a/frontend/config.py b/frontend/config.py new file mode 100644 index 0000000..daafc5b --- /dev/null +++ b/frontend/config.py @@ -0,0 +1,31 @@ +import os +from pathlib import Path + +# URL du backend - configurée uniquement via variable d'environnement BACKEND_URL +# Pour le développement local : BACKEND_URL=http://localhost:8091 +# Pour la production : BACKEND_URL=http://backend_entwine:8000 + +# URL Potree - configurée via variable d'environnement POTREE_URL +# Pour le développement local : POTREE_URL=http://localhost:8090 +# Pour la production : POTREE_URL=http://potree_server:8090 + +def load_backend_config(): + """Charge la configuration du backend depuis les variables d'environnement.""" + return os.getenv( + "BACKEND_URL", "http://localhost:8091" + ).strip().rstrip("/") + +def load_potree_config(): + """Charge la configuration Potree depuis les variables d'environnement.""" + return os.getenv( + "POTREE_URL", "http://localhost:8090" + ).strip().rstrip("/") + +BACKEND_URL = load_backend_config() +POTREE_URL = load_potree_config() + +SUPPORTED_FORMATS = [".las", ".laz", ".ply", ".xyz", ".pts"] +SUPPORTED_EXTENSIONS = SUPPORTED_FORMATS # Alias pour compatibilité + +# Chemin du dossier EPT (nuages de points convertis) +EPT_DIR = Path("/app/backend/data/ept") diff --git a/frontend/main.py b/frontend/main.py new file mode 100644 index 0000000..523957c --- /dev/null +++ b/frontend/main.py @@ -0,0 +1,54 @@ +# main.py - FastAPI + Jinja2 + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from pathlib import Path +from datetime import datetime + +from routes import upload, viewer, admin, crop + +# -- Middleware ------------------------------------------------ +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response + + +app = FastAPI(title="PointCloud Frontend") + +MAX_UPLOAD_BYTES = 10 * 1024 * 1024 * 1024 # 10 GB à ajuster + +class LimitUploadSize(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + if request.method == "POST": + content_length = request.headers.get("content-length") + if content_length and int(content_length) > MAX_UPLOAD_BYTES: + return Response( + content=f"Fichier trop volumineux. Maximum : {MAX_UPLOAD_BYTES // (1024**3)} GB", + status_code=413, + ) + return await call_next(request) + +app.add_middleware(LimitUploadSize) + +# ── Fichiers statiques ──────────────────────────────────────────────────────── +app.mount( + "/static", + StaticFiles(directory=Path(__file__).parent / "static"), + name="static", +) + +# ── Jinja2 pour les pages et partials ──────────────────────────────────────── +templates = Jinja2Templates(directory=Path(__file__).parent / "templates") +templates.env.filters["datetimeformat"] = lambda ts: ( + datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M") if ts else "—" +) + +# Rend les templates accessibles aux routes via app.state +app.state.templates = templates + +# ── Routes ──────────────────────────────────────────────────────────────────── +app.include_router(upload.router) +app.include_router(viewer.router) +app.include_router(admin.router) +app.include_router(crop.router) \ No newline at end of file