"""MinIO client — S3-compatible object storage for invoice / proforma attachments. The `minio` SDK is synchronous; every call is wrapped with ``asyncio.to_thread`` so the event loop is never blocked. """ import asyncio import io import logging from functools import lru_cache from minio import Minio from app.config import settings logger = logging.getLogger(__name__) @lru_cache(maxsize=1) def get_client() -> Minio: """Lazily built Minio client from the PROCUREMENT_MINIO_* settings.""" return Minio( settings.MINIO_ENDPOINT, access_key=settings.MINIO_ACCESS_KEY, secret_key=settings.MINIO_SECRET_KEY, secure=settings.MINIO_SECURE, ) async def ensure_bucket() -> None: """Create the attachments bucket if it does not exist (idempotent).""" client = get_client() try: exists = await asyncio.to_thread(client.bucket_exists, settings.MINIO_BUCKET) if not exists: await asyncio.to_thread(client.make_bucket, settings.MINIO_BUCKET) logger.info("Created MinIO bucket %r", settings.MINIO_BUCKET) except Exception: logger.exception("MinIO bucket check failed — is the minio service reachable?") async def put_object(key: str, content: bytes, mime_type: str) -> None: """Upload a byte payload to the bucket (overwrites any existing object).""" await asyncio.to_thread( get_client().put_object, settings.MINIO_BUCKET, key, io.BytesIO(content), length=len(content), content_type=mime_type, ) async def get_object_bytes(key: str) -> bytes: """Download an object's bytes from the bucket.""" response = await asyncio.to_thread(get_client().get_object, settings.MINIO_BUCKET, key) try: return response.read() finally: response.close() response.release_conn() async def remove_object(key: str) -> None: """Delete an object; missing objects are ignored (best-effort).""" try: await asyncio.to_thread(get_client().remove_object, settings.MINIO_BUCKET, key) except Exception: logger.warning("Could not remove MinIO object %r — ignoring", key)