Files

69 lines
2.1 KiB
Python

"""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)