etape laptop
This commit is contained in:
141
backend/app/routes/images.py
Normal file
141
backend/app/routes/images.py
Normal file
@@ -0,0 +1,141 @@
|
||||
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
|
||||
Reference in New Issue
Block a user