import uuid from mimetypes import guess_extension from fastapi import UploadFile from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from starlette.concurrency import run_in_threadpool from app.core.config import get_settings from app.models.object_photo import ObjectPhoto from app.schemas.photo import PhotoRead ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/webp", "image/heic"} def build_photo_read(photo: ObjectPhoto) -> PhotoRead: settings = get_settings() url = f"{settings.minio_public_url}/{settings.minio_bucket}/{photo.storage_key}" return PhotoRead(id=photo.id, url=url, content_type=photo.content_type, position=photo.position) async def upload_photo( db: AsyncSession, minio_client, object_id: uuid.UUID, upload_file: UploadFile ) -> ObjectPhoto: if upload_file.content_type not in ALLOWED_CONTENT_TYPES: raise ValueError(f"Unsupported content type: {upload_file.content_type}") settings = get_settings() extension = guess_extension(upload_file.content_type) or "" photo_id = uuid.uuid4() storage_key = f"objects/{object_id}/{photo_id}{extension}" body = await upload_file.read() await run_in_threadpool( minio_client.put_object, Bucket=settings.minio_bucket, Key=storage_key, Body=body, ContentType=upload_file.content_type, ) max_position = await db.scalar( select(func.coalesce(func.max(ObjectPhoto.position), -1)).where(ObjectPhoto.object_id == object_id) ) photo = ObjectPhoto( id=photo_id, object_id=object_id, storage_key=storage_key, content_type=upload_file.content_type, position=max_position + 1, ) db.add(photo) await db.commit() await db.refresh(photo) return photo async def delete_photo(db: AsyncSession, minio_client, photo: ObjectPhoto) -> None: settings = get_settings() await run_in_threadpool(minio_client.delete_object, Bucket=settings.minio_bucket, Key=photo.storage_key) await db.delete(photo) await db.commit()