Files
home_hub/backend/app/services/media.py
T
gilles 3dbd554eeb fix(media): upload photo — taille, formats et nginx
- nginx : client_max_body_size 15m (photos smartphone > 1 Mo rejetées silencieusement)
- backend : redimensionnement original à 500×500 max (aspect ratio conservé) avant sauvegarde WEBP
- backend : thumbnail généré depuis l'image déjà redimensionnée (économie mémoire)
- backend : formats acceptés étendus — image/heic, image/heif, image/jpg
- backend : normalisation content-type en lowercase (robustesse navigateurs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:29:45 +02:00

96 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import io
import uuid
from pathlib import Path
from fastapi import HTTPException, UploadFile
from PIL import Image
from app.core.config import settings
UPLOAD_DIR: Path = settings.upload_path
ALLOWED_IMAGE_TYPES = {
"image/jpeg", "image/jpg", "image/png", "image/webp", "image/svg+xml",
"image/heic", "image/heif",
}
ALLOWED_AUDIO_TYPES = {"audio/webm", "audio/mp4"}
MAX_ORIG_SIZE = (500, 500)
THUMBNAIL_SIZES = {
"product": (150, 150),
"note": (300, 300),
"attachment": (400, 300),
}
async def save_image(file: UploadFile, context: str = "note") -> dict:
content_type = (file.content_type or "").lower()
if content_type not in ALLOWED_IMAGE_TYPES:
raise HTTPException(status_code=400, detail=f"Format non supporté : {content_type}")
file_id = str(uuid.uuid4())
content = await file.read()
orig_dir = UPLOAD_DIR / "images" / "originals"
orig_dir.mkdir(parents=True, exist_ok=True)
if content_type == "image/svg+xml":
orig_path = orig_dir / f"{file_id}.svg"
orig_path.write_bytes(content)
return {
"file_id": file_id,
"file_path": str(orig_path.relative_to(UPLOAD_DIR)),
"thumbnail_path": None,
"file_type": "image",
}
orig_path = orig_dir / f"{file_id}.webp"
img = Image.open(io.BytesIO(content)).convert("RGB")
# Redimensionne l'original à 500×500 max en conservant l'aspect ratio
img.thumbnail(MAX_ORIG_SIZE, Image.LANCZOS)
img.save(orig_path, "WEBP", quality=85)
thumb_dir = UPLOAD_DIR / "images" / "thumbnails"
thumb_dir.mkdir(parents=True, exist_ok=True)
thumb_path = thumb_dir / f"{file_id}_thumb.webp"
thumb_size = THUMBNAIL_SIZES.get(context, (300, 300))
img_thumb = img.copy()
img_thumb.thumbnail(thumb_size, Image.LANCZOS)
img_thumb.save(thumb_path, "WEBP", quality=80)
return {
"file_id": file_id,
"file_path": str(orig_path.relative_to(UPLOAD_DIR)),
"thumbnail_path": str(thumb_path.relative_to(UPLOAD_DIR)),
"file_type": "image",
}
async def save_audio(file: UploadFile) -> dict:
if file.content_type not in ALLOWED_AUDIO_TYPES:
raise HTTPException(status_code=400, detail=f"Format audio non supporté : {file.content_type}")
file_id = str(uuid.uuid4())
audio_dir = UPLOAD_DIR / "audio"
audio_dir.mkdir(parents=True, exist_ok=True)
ext = ".webm" if "webm" in (file.content_type or "") else ".m4a"
audio_path = audio_dir / f"{file_id}{ext}"
audio_path.write_bytes(await file.read())
return {
"file_id": file_id,
"file_path": str(audio_path.relative_to(UPLOAD_DIR)),
"thumbnail_path": None,
"file_type": "audio",
}
def delete_media(file_id: str, file_path: str, thumbnail_path: str | None = None) -> None:
(UPLOAD_DIR / file_path).unlink(missing_ok=True)
if thumbnail_path:
(UPLOAD_DIR / thumbnail_path).unlink(missing_ok=True)