63 lines
1.8 KiB
Python
63 lines
1.8 KiB
Python
from __future__ import annotations
|
|
|
|
from io import BytesIO
|
|
from uuid import uuid4
|
|
|
|
from PIL import Image
|
|
|
|
from app.core.config import settings
|
|
from app.core.storage import upload_object
|
|
|
|
ALLOWED_MIME = {
|
|
"image/jpeg": ".jpg",
|
|
"image/png": ".png",
|
|
"image/webp": ".webp",
|
|
}
|
|
|
|
FORMAT_TO_MIME = {
|
|
"JPEG": "image/jpeg",
|
|
"PNG": "image/png",
|
|
"WEBP": "image/webp",
|
|
}
|
|
|
|
|
|
class AvatarValidationError(ValueError):
|
|
pass
|
|
|
|
|
|
def validate_and_process_avatar(content: bytes) -> tuple[str, bytes]:
|
|
if len(content) > settings.avatar_max_bytes:
|
|
raise AvatarValidationError("FILE_TOO_LARGE")
|
|
if not content:
|
|
raise AvatarValidationError("INVALID_IMAGE")
|
|
|
|
try:
|
|
with Image.open(BytesIO(content)) as image:
|
|
image.verify()
|
|
with Image.open(BytesIO(content)) as image:
|
|
mime = FORMAT_TO_MIME.get(image.format or "")
|
|
if mime not in ALLOWED_MIME:
|
|
raise AvatarValidationError("INVALID_MIME")
|
|
|
|
buffer = BytesIO()
|
|
if mime == "image/jpeg":
|
|
rgb = image.convert("RGB")
|
|
rgb.save(buffer, format="JPEG", quality=85, optimize=True)
|
|
elif mime == "image/png":
|
|
image.save(buffer, format="PNG", optimize=True)
|
|
else:
|
|
image.save(buffer, format="WEBP", quality=85, method=6)
|
|
return mime, buffer.getvalue()
|
|
except AvatarValidationError:
|
|
raise
|
|
except Exception as exc:
|
|
raise AvatarValidationError("INVALID_IMAGE") from exc
|
|
|
|
|
|
def upload_user_avatar(user_id: str, content: bytes) -> str:
|
|
mime, processed = validate_and_process_avatar(content)
|
|
extension = ALLOWED_MIME[mime]
|
|
key = f"avatars/{user_id}/{uuid4()}{extension}"
|
|
upload_object(key, processed, mime)
|
|
return f"/api/v1/media/files/{key}"
|