Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
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}"
|
||||
Reference in New Issue
Block a user