import io import uuid from datetime import timedelta from urllib.parse import urlparse import structlog from minio import Minio from minio.error import S3Error from fastapi import UploadFile, HTTPException, status from app.core.config import settings logger = structlog.get_logger() _client: Minio | None = None _public_client: Minio | None = None def get_client() -> Minio: global _client if _client is None: _client = Minio( settings.minio_endpoint, access_key=settings.minio_access_key, secret_key=settings.minio_secret_key, secure=settings.minio_secure, ) return _client def _get_public_client() -> Minio: global _public_client if _public_client is None: parsed = urlparse(settings.minio_public_url) endpoint = parsed.netloc secure = parsed.scheme == "https" _public_client = Minio( endpoint, access_key=settings.minio_access_key, secret_key=settings.minio_secret_key, secure=secure, region="us-east-1", ) return _public_client def _ensure_bucket(bucket: str) -> None: client = get_client() try: if not client.bucket_exists(bucket): client.make_bucket(bucket) logger.info("minio_bucket_created", bucket=bucket) except S3Error as exc: logger.error("minio_bucket_error", bucket=bucket, error=str(exc)) raise async def upload_photo(file: UploadFile, valuation_id: int) -> str: bucket = settings.minio_bucket_photos try: _ensure_bucket(bucket) except S3Error as exc: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Storage non disponibile", ) from exc ext = "" if file.filename and "." in file.filename: ext = "." + file.filename.rsplit(".", 1)[-1].lower() object_name = f"valuations/{valuation_id}/{uuid.uuid4().hex}{ext}" content = await file.read() content_length = len(content) content_type = file.content_type or "application/octet-stream" client = get_client() try: client.put_object( bucket, object_name, io.BytesIO(content), length=content_length, content_type=content_type, ) logger.info("minio_upload_ok", object_name=object_name) return object_name except S3Error as exc: logger.error("minio_upload_error", object_name=object_name, error=str(exc)) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Errore durante il caricamento del file", ) from exc def get_presigned_url(storage_path: str, expires_hours: int = 4) -> str | None: bucket = settings.minio_bucket_photos client = _get_public_client() try: url = client.presigned_get_object( bucket, storage_path, expires=timedelta(hours=expires_hours), ) return url except S3Error as exc: logger.warning("minio_presigned_error", path=storage_path, error=str(exc)) return None