chrysopedia/backend/minio_client.py
jlightner cc60852ac9 feat: Built post CRUD and file upload/download API routers with auth, o…
- "backend/routers/posts.py"
- "backend/routers/files.py"
- "backend/minio_client.py"
- "backend/auth.py"
- "backend/main.py"

GSD-Task: S01/T02
2026-04-04 09:07:35 +00:00

116 lines
3.2 KiB
Python

"""MinIO client singleton with lazy initialization.
Provides file upload, presigned download URL generation, and automatic
bucket creation for the Chrysopedia post attachment storage.
"""
from __future__ import annotations
import io
import logging
from datetime import timedelta
from minio import Minio
from minio.error import S3Error
from config import get_settings
logger = logging.getLogger(__name__)
_client: Minio | None = None
_bucket_ensured: bool = False
def get_minio_client() -> Minio:
"""Return the singleton MinIO client, creating it on first call."""
global _client
if _client is None:
settings = get_settings()
_client = Minio(
settings.minio_url,
access_key=settings.minio_access_key,
secret_key=settings.minio_secret_key,
secure=settings.minio_secure,
)
logger.info("MinIO client initialized (endpoint=%s)", settings.minio_url)
return _client
def ensure_bucket() -> None:
"""Create the configured bucket if it doesn't already exist."""
global _bucket_ensured
if _bucket_ensured:
return
settings = get_settings()
client = get_minio_client()
bucket = settings.minio_bucket
try:
if not client.bucket_exists(bucket):
client.make_bucket(bucket)
logger.info("Created MinIO bucket: %s", bucket)
else:
logger.debug("MinIO bucket already exists: %s", bucket)
_bucket_ensured = True
except S3Error as exc:
logger.error("MinIO bucket check/create failed: %s", exc)
raise
def upload_file(
object_key: str,
data: bytes | io.BytesIO,
length: int,
content_type: str = "application/octet-stream",
) -> None:
"""Upload a file to MinIO.
Args:
object_key: The storage path within the bucket.
data: File content as bytes or BytesIO stream.
length: Size in bytes.
content_type: MIME type for the object.
"""
ensure_bucket()
settings = get_settings()
client = get_minio_client()
stream = io.BytesIO(data) if isinstance(data, bytes) else data
client.put_object(
settings.minio_bucket,
object_key,
stream,
length,
content_type=content_type,
)
logger.info("Uploaded %s (%d bytes, %s)", object_key, length, content_type)
def generate_download_url(object_key: str, expires: int = 3600) -> str:
"""Generate a presigned GET URL for downloading a file.
Args:
object_key: The storage path within the bucket.
expires: URL validity in seconds (default 1 hour).
Returns:
Presigned URL string.
"""
settings = get_settings()
client = get_minio_client()
url: str = client.presigned_get_object(
settings.minio_bucket,
object_key,
expires=timedelta(seconds=expires),
)
return url
def delete_file(object_key: str) -> None:
"""Delete a file from MinIO.
Args:
object_key: The storage path within the bucket.
"""
settings = get_settings()
client = get_minio_client()
client.remove_object(settings.minio_bucket, object_key)
logger.info("Deleted %s from MinIO", object_key)