Upgrade configuration and Docker stack to v2.0.0

This commit is contained in:
2026-06-22 14:49:56 -04:00
parent 2f2a5e9b4a
commit 0d2ecbd218
19 changed files with 779 additions and 495 deletions
+35 -26
View File
@@ -2,6 +2,7 @@
Service de stockage — sauvegarde fichiers, génération thumbnails
Multi-tenant : les fichiers sont isolés par client_id.
"""
import asyncio
import uuid
import logging
import io
@@ -33,6 +34,9 @@ async def save_upload(file: UploadFile, client_id: str) -> dict:
"""
Valide, sauvegarde le fichier uploadé et génère un thumbnail.
Utilise le backend de stockage configuré (Local ou S3).
L'ordre est : validation → upload fichier → génération thumbnail → retourne les métadonnées.
L'appelant est responsable de l'insertion BDD et du rollback en cas d'échec.
"""
# ── Validation MIME ───────────────────────────────────────
if file.content_type not in ALLOWED_MIME_TYPES:
@@ -53,7 +57,7 @@ async def save_upload(file: UploadFile, client_id: str) -> dict:
# ── Nommage ───────────────────────────────────────────────
filename, file_uuid = _generate_filename(file.filename or "image")
# 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}"
@@ -67,26 +71,28 @@ async def save_upload(file: UploadFile, client_id: str) -> dict:
width, height = None, None
thumb_saved = False
try:
# 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
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
# 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:
logger.warning("Erreur génération thumbnail : %s", e)
logger.warning("thumbnail.generation_error", extra={
"file": rel_file_path,
"error": str(e),
})
return {
"uuid": file_uuid,
@@ -103,24 +109,27 @@ 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 via le backend."""
import asyncio
async def delete_files(file_path: str, thumbnail_path: str | None = None) -> None:
"""
Supprime le fichier original et son thumbnail via le backend de stockage.
Entièrement asynchrone — les fichiers sont supprimés de façon déterministe.
"""
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.
errors: list[str] = []
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
await backend.delete(file_path)
except Exception as e:
errors.append(f"original: {e}")
if thumbnail_path:
try:
await backend.delete(thumbnail_path)
except Exception as e:
errors.append(f"thumbnail: {e}")
if errors:
logger.warning("storage.delete_partial", extra={
"file_path": file_path,
"errors": errors,
})