Spaces:
Sleeping
Sleeping
| """ | |
| MinIO storage client for task attachments. | |
| """ | |
| import asyncio | |
| import io | |
| from datetime import timedelta | |
| from typing import Optional | |
| from minio import Minio | |
| from minio.error import S3Error | |
| from app.core.config import settings | |
| from app.core.logging import get_logger | |
| logger = get_logger(__name__) | |
| # Global MinIO client instance | |
| _minio_client: Optional[Minio] = None | |
| def get_minio_client() -> Minio: | |
| """Get or create MinIO client instance.""" | |
| global _minio_client | |
| if _minio_client is None: | |
| if not settings.MINIO_ACCESS_KEY or not settings.MINIO_SECRET_KEY: | |
| raise RuntimeError("MinIO credentials are not configured") | |
| _minio_client = Minio( | |
| endpoint=settings.MINIO_ENDPOINT, | |
| access_key=settings.MINIO_ACCESS_KEY, | |
| secret_key=settings.MINIO_SECRET_KEY, | |
| secure=settings.MINIO_SECURE, | |
| region=settings.MINIO_REGION | |
| ) | |
| logger.info("MinIO client initialized", extra={ | |
| "endpoint": settings.MINIO_ENDPOINT, | |
| "secure": settings.MINIO_SECURE | |
| }) | |
| return _minio_client | |
| class MinioStorageAdapter: | |
| """Async-friendly wrapper around MinIO client for file uploads.""" | |
| def __init__(self, client: Minio, bucket: str): | |
| self.client = client | |
| self.bucket = bucket | |
| self._ensure_bucket() | |
| def _ensure_bucket(self) -> None: | |
| """Ensure bucket exists, create if not.""" | |
| try: | |
| if not self.client.bucket_exists(self.bucket): | |
| self.client.make_bucket(self.bucket) | |
| logger.info(f"Created MinIO bucket: {self.bucket}") | |
| except S3Error as e: | |
| logger.error(f"Failed to ensure bucket exists: {e}", exc_info=e) | |
| raise | |
| async def upload_file( | |
| self, | |
| object_key: str, | |
| file_data: bytes, | |
| mime_type: str | |
| ) -> str: | |
| """ | |
| Upload file data directly to MinIO storage. | |
| Args: | |
| object_key: Object path/key in bucket | |
| file_data: File content as bytes | |
| mime_type: MIME type of the file | |
| Returns: | |
| Object key of uploaded file | |
| """ | |
| loop = asyncio.get_running_loop() | |
| file_stream = io.BytesIO(file_data) | |
| try: | |
| await loop.run_in_executor( | |
| None, | |
| lambda: self.client.put_object( | |
| bucket_name=self.bucket, | |
| object_name=object_key, | |
| data=file_stream, | |
| length=len(file_data), | |
| content_type=mime_type | |
| ) | |
| ) | |
| logger.info(f"File uploaded to MinIO", extra={ | |
| "bucket": self.bucket, | |
| "object_key": object_key, | |
| "size": len(file_data), | |
| "mime_type": mime_type | |
| }) | |
| return object_key | |
| except S3Error as e: | |
| logger.error(f"Failed to upload file to MinIO: {e}", exc_info=e) | |
| raise | |
| async def get_public_url(self, object_key: str) -> str: | |
| """ | |
| Generate public URL for uploaded file. | |
| Args: | |
| object_key: Object path/key in bucket | |
| Returns: | |
| Public URL to access the file | |
| """ | |
| protocol = "https" if settings.MINIO_SECURE else "http" | |
| endpoint = f"{protocol}://{settings.MINIO_ENDPOINT}".rstrip('/') | |
| return f"{endpoint}/{self.bucket}/{object_key}" | |
| async def get_presigned_url( | |
| self, | |
| object_key: str, | |
| expiry_seconds: int = 3600 | |
| ) -> str: | |
| """ | |
| Generate presigned URL for secure file access. | |
| Args: | |
| object_key: Object path/key in bucket | |
| expiry_seconds: URL expiry time in seconds | |
| Returns: | |
| Presigned URL to access the file | |
| """ | |
| loop = asyncio.get_running_loop() | |
| try: | |
| url = await loop.run_in_executor( | |
| None, | |
| lambda: self.client.presigned_get_object( | |
| bucket_name=self.bucket, | |
| object_name=object_key, | |
| expires=timedelta(seconds=expiry_seconds) | |
| ) | |
| ) | |
| return url | |
| except S3Error as e: | |
| logger.error(f"Failed to generate presigned URL: {e}", exc_info=e) | |
| raise | |
| async def delete_file(self, object_key: str) -> None: | |
| """ | |
| Delete file from MinIO storage. | |
| Args: | |
| object_key: Object path/key in bucket | |
| """ | |
| loop = asyncio.get_running_loop() | |
| try: | |
| await loop.run_in_executor( | |
| None, | |
| lambda: self.client.remove_object( | |
| bucket_name=self.bucket, | |
| object_name=object_key | |
| ) | |
| ) | |
| logger.info(f"File deleted from MinIO", extra={ | |
| "bucket": self.bucket, | |
| "object_key": object_key | |
| }) | |
| except S3Error as e: | |
| logger.error(f"Failed to delete file from MinIO: {e}", exc_info=e) | |
| raise | |
| def get_storage_adapter(bucket: Optional[str] = None) -> MinioStorageAdapter: | |
| """ | |
| Factory function to create MinioStorageAdapter instance. | |
| Args: | |
| bucket: Bucket name (defaults to task attachments bucket) | |
| Returns: | |
| MinioStorageAdapter instance | |
| """ | |
| client = get_minio_client() | |
| bucket_name = bucket or settings.STORAGE_BUCKET_TASK_ATTACHMENTS | |
| return MinioStorageAdapter(client, bucket_name) | |