import json import base64 import re import uuid from pathlib import Path from fastapi import APIRouter, Depends, UploadFile, File, HTTPException from fastapi.responses import FileResponse from sqlmodel import Session from ..database import get_session from ..models import Feature from ..config import DATA_DIR router = APIRouter(prefix="/images", tags=["images"]) IMAGES_DIR = DATA_DIR / "images" @router.get("/{dataset_id}/{filename}") def get_image(dataset_id: int, filename: str): """Servir une image stockée.""" path = IMAGES_DIR / str(dataset_id) / filename if not path.exists() or not path.is_file(): raise HTTPException(404, "Image non trouvée") # Sécurité : vérifier que le chemin résolu est bien dans IMAGES_DIR if not path.resolve().is_relative_to(IMAGES_DIR.resolve()): raise HTTPException(403, "Accès interdit") media_type = "image/jpeg" if filename.endswith(".png"): media_type = "image/png" elif filename.endswith(".webp"): media_type = "image/webp" return FileResponse(path, media_type=media_type) @router.post("/features/{feature_id}") async def upload_image( feature_id: int, file: UploadFile = File(...), session: Session = Depends(get_session), ): """Uploader une nouvelle image pour une feature.""" feature = session.get(Feature, feature_id) if not feature: raise HTTPException(404, "Feature non trouvée") props = json.loads(feature.properties_json) images = props.get("_images", []) # Sauvegarder le fichier img_dir = IMAGES_DIR / str(feature.dataset_id) img_dir.mkdir(parents=True, exist_ok=True) ext = Path(file.filename).suffix or ".jpg" filename = f"{feature_id}_{uuid.uuid4().hex[:8]}{ext}" filepath = img_dir / filename content = await file.read() filepath.write_bytes(content) # Ajouter l'URL dans les propriétés url = f"/api/images/{feature.dataset_id}/{filename}" images.append(url) props["_images"] = images feature.properties_json = json.dumps(props) session.add(feature) session.commit() return {"url": url, "images": images} @router.delete("/features/{feature_id}/{filename}") def delete_image( feature_id: int, filename: str, session: Session = Depends(get_session), ): """Supprimer une image d'une feature.""" feature = session.get(Feature, feature_id) if not feature: raise HTTPException(404, "Feature non trouvée") props = json.loads(feature.properties_json) images = props.get("_images", []) # Trouver et supprimer l'URL correspondante url = f"/api/images/{feature.dataset_id}/{filename}" if url not in images: raise HTTPException(404, "Image non trouvée dans cette feature") images.remove(url) props["_images"] = images feature.properties_json = json.dumps(props) session.add(feature) session.commit() # Supprimer le fichier filepath = IMAGES_DIR / str(feature.dataset_id) / filename if filepath.exists() and filepath.resolve().is_relative_to(IMAGES_DIR.resolve()): filepath.unlink() return {"images": images} def extract_and_save_images(properties: dict, dataset_id: int, feature_index: int) -> dict: """Extraire les images base64 des propriétés et les sauvegarder en fichiers. Les data URIs dans _images sont remplacées par des URLs /api/images/... """ images = properties.get("_images", []) if not images: return properties img_dir = IMAGES_DIR / str(dataset_id) img_dir.mkdir(parents=True, exist_ok=True) saved_urls = [] for i, img in enumerate(images): if isinstance(img, str) and img.startswith("data:image"): # Extraire le base64 match = re.match(r"data:image/(\w+);base64,(.+)", img, re.DOTALL) if match: ext = match.group(1) if ext == "jpeg": ext = "jpg" b64_data = match.group(2) try: raw = base64.b64decode(b64_data) filename = f"{feature_index}_{i}.{ext}" filepath = img_dir / filename filepath.write_bytes(raw) saved_urls.append(f"/api/images/{dataset_id}/{filename}") except Exception: continue elif isinstance(img, str) and img.startswith("/api/images/"): # Déjà une URL serveur saved_urls.append(img) elif isinstance(img, str) and img.startswith("http"): # URL externe, garder telle quelle saved_urls.append(img) properties["_images"] = saved_urls return properties