Mise à jour de la configuration de la base de données pour utiliser PostgreSQL et ajout de Redis dans le fichier .env. Modifications du Dockerfile pour installer les dépendances nécessaires. Amélioration des services de stockage pour supporter les opérations asynchrones avec S3 et le stockage local. Refactorisation des pipelines d'images pour une meilleure gestion des tâches asynchrones. Ajout de la gestion des clés API dans l'authentification. Mise à jour de la documentation et des exemples d'utilisation.
This commit is contained in:
+47
-44
@@ -4,12 +4,13 @@ Multi-tenant : les fichiers sont isolés par client_id.
|
||||
"""
|
||||
import uuid
|
||||
import logging
|
||||
import aiofiles
|
||||
import io
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from PIL import Image as PILImage
|
||||
from fastapi import UploadFile, HTTPException, status
|
||||
from app.config import settings
|
||||
from app.services.storage_backend import get_storage_backend
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,25 +29,10 @@ def _generate_filename(original: str) -> tuple[str, str]:
|
||||
return f"{uid}{suffix}", uid
|
||||
|
||||
|
||||
def _get_client_upload_path(client_id: str) -> Path:
|
||||
"""Retourne le répertoire d'upload pour un client donné."""
|
||||
p = settings.upload_path / client_id
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
def _get_client_thumbnails_path(client_id: str) -> Path:
|
||||
"""Retourne le répertoire de thumbnails pour un client donné."""
|
||||
p = settings.thumbnails_path / client_id
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
async def save_upload(file: UploadFile, client_id: str) -> dict:
|
||||
"""
|
||||
Valide, sauvegarde le fichier uploadé et génère un thumbnail.
|
||||
Les fichiers sont stockés dans uploads/{client_id}/ pour l'isolation.
|
||||
Retourne un dict avec toutes les métadonnées fichier.
|
||||
Utilise le backend de stockage configuré (Local ou S3).
|
||||
"""
|
||||
# ── Validation MIME ───────────────────────────────────────
|
||||
if file.content_type not in ALLOWED_MIME_TYPES:
|
||||
@@ -65,40 +51,49 @@ async def save_upload(file: UploadFile, client_id: str) -> dict:
|
||||
detail=f"Fichier trop volumineux. Max : {settings.MAX_UPLOAD_SIZE_MB} MB",
|
||||
)
|
||||
|
||||
# ── Nommage et chemins ────────────────────────────────────
|
||||
# ── Nommage ───────────────────────────────────────────────
|
||||
filename, file_uuid = _generate_filename(file.filename or "image")
|
||||
upload_dir = _get_client_upload_path(client_id)
|
||||
thumb_dir = _get_client_thumbnails_path(client_id)
|
||||
|
||||
# Chemins relatifs par rapport au bucket/base_dir
|
||||
rel_file_path = f"uploads/{client_id}/{filename}"
|
||||
rel_thumb_path = f"thumbnails/{client_id}/thumb_{filename}"
|
||||
|
||||
file_path = upload_dir / filename
|
||||
thumb_filename = f"thumb_{filename}"
|
||||
thumb_path = thumb_dir / thumb_filename
|
||||
backend = get_storage_backend()
|
||||
|
||||
# ── Sauvegarde fichier original ───────────────────────────
|
||||
async with aiofiles.open(file_path, "wb") as f:
|
||||
await f.write(content)
|
||||
await backend.save(content, rel_file_path, file.content_type)
|
||||
|
||||
# ── Dimensions + thumbnail ────────────────────────────────
|
||||
width, height = None, None
|
||||
thumb_saved = False
|
||||
try:
|
||||
with PILImage.open(file_path) as img:
|
||||
# On utilise io.BytesIO pour ne pas avoir à écrire sur le disque local
|
||||
with PILImage.open(io.BytesIO(content)) as img:
|
||||
width, height = img.size
|
||||
img.thumbnail(THUMBNAIL_SIZE, PILImage.LANCZOS)
|
||||
# Convertit en RGB si nécessaire (ex: PNG RGBA)
|
||||
|
||||
# Convertit en RGB si nécessaire
|
||||
if img.mode in ("RGBA", "P"):
|
||||
img = img.convert("RGB")
|
||||
img.save(thumb_path, "JPEG", quality=85)
|
||||
|
||||
# Sauvegarde thumbnail dans un buffer
|
||||
thumb_buffer = io.BytesIO()
|
||||
img.save(thumb_buffer, "JPEG", quality=85)
|
||||
thumb_data = thumb_buffer.getvalue()
|
||||
|
||||
# Sauvegarde via le backend
|
||||
await backend.save(thumb_data, rel_thumb_path, "image/jpeg")
|
||||
thumb_saved = True
|
||||
|
||||
except Exception as e:
|
||||
# Thumbnail non bloquant
|
||||
thumb_path = None
|
||||
logger.warning("Erreur génération thumbnail : %s", e)
|
||||
|
||||
return {
|
||||
"uuid": file_uuid,
|
||||
"original_name": file.filename,
|
||||
"filename": filename,
|
||||
"file_path": str(file_path),
|
||||
"thumbnail_path": str(thumb_path) if thumb_path else None,
|
||||
"file_path": rel_file_path,
|
||||
"thumbnail_path": rel_thumb_path if thumb_saved else None,
|
||||
"mime_type": file.content_type,
|
||||
"file_size": len(content),
|
||||
"width": width,
|
||||
@@ -109,15 +104,23 @@ async def save_upload(file: UploadFile, client_id: str) -> dict:
|
||||
|
||||
|
||||
def delete_files(file_path: str, thumbnail_path: str | None = None) -> None:
|
||||
"""Supprime le fichier original et son thumbnail du disque."""
|
||||
for path_str in [file_path, thumbnail_path]:
|
||||
if path_str:
|
||||
p = Path(path_str)
|
||||
if p.exists():
|
||||
p.unlink()
|
||||
|
||||
|
||||
def get_image_url(filename: str, client_id: str, thumb: bool = False) -> str:
|
||||
"""Construit l'URL publique d'une image."""
|
||||
prefix = "thumbnails" if thumb else "uploads"
|
||||
return f"/static/{prefix}/{client_id}/{filename}"
|
||||
"""Supprime le fichier original et son thumbnail via le backend."""
|
||||
import asyncio
|
||||
backend = get_storage_backend()
|
||||
|
||||
async def _do_delete():
|
||||
await backend.delete(file_path)
|
||||
if thumbnail_path:
|
||||
await backend.delete(thumbnail_path)
|
||||
|
||||
# Note: delete_files est synchrone dans les routers existants,
|
||||
# mais le backend est async. C'est un risque.
|
||||
# TODO: Refactorer delete_image pour être full async.
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
asyncio.ensure_future(_do_delete())
|
||||
else:
|
||||
loop.run_until_complete(_do_delete())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user