- "backend/routers/posts.py" - "backend/routers/files.py" - "backend/minio_client.py" - "backend/auth.py" - "backend/main.py" GSD-Task: S01/T02
116 lines
3.2 KiB
Python
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)
|