import boto3 from botocore.exceptions import ClientError from botocore.client import Config from typing import BinaryIO from app.storage.base import StorageBackend from app.config import settings import logging logger = logging.getLogger(__name__) class MinIOBackend(StorageBackend): """MinIO storage backend implementation (S3-compatible)""" def __init__(self): # MinIO uses S3-compatible API self.s3_client = boto3.client( 's3', endpoint_url=f"{'https' if settings.minio_secure else 'http'}://{settings.minio_endpoint}", aws_access_key_id=settings.minio_access_key, aws_secret_access_key=settings.minio_secret_key, config=Config(signature_version='s3v4'), region_name='us-east-1' ) self.bucket_name = settings.minio_bucket_name self._ensure_bucket_exists() def _ensure_bucket_exists(self): """Create bucket if it doesn't exist""" try: self.s3_client.head_bucket(Bucket=self.bucket_name) except ClientError as e: error_code = e.response['Error']['Code'] if error_code == '404': try: self.s3_client.create_bucket(Bucket=self.bucket_name) logger.info(f"Created MinIO bucket: {self.bucket_name}") except ClientError as create_error: logger.error(f"Failed to create bucket: {create_error}") raise async def upload_file(self, file_data: BinaryIO, object_name: str) -> str: """Upload file to MinIO""" try: self.s3_client.upload_fileobj(file_data, self.bucket_name, object_name) return f"minio://{self.bucket_name}/{object_name}" except ClientError as e: logger.error(f"Failed to upload file to MinIO: {e}") raise async def download_file(self, object_name: str) -> bytes: """Download file from MinIO""" try: response = self.s3_client.get_object(Bucket=self.bucket_name, Key=object_name) return response['Body'].read() except ClientError as e: logger.error(f"Failed to download file from MinIO: {e}") raise async def delete_file(self, object_name: str) -> bool: """Delete file from MinIO""" try: self.s3_client.delete_object(Bucket=self.bucket_name, Key=object_name) return True except ClientError as e: logger.error(f"Failed to delete file from MinIO: {e}") return False async def file_exists(self, object_name: str) -> bool: """Check if file exists in MinIO""" try: self.s3_client.head_object(Bucket=self.bucket_name, Key=object_name) return True except ClientError: return False async def get_file_url(self, object_name: str, expiration: int = 3600) -> str: """Generate presigned URL for MinIO object""" try: url = self.s3_client.generate_presigned_url( 'get_object', Params={'Bucket': self.bucket_name, 'Key': object_name}, ExpiresIn=expiration ) return url except ClientError as e: logger.error(f"Failed to generate presigned URL: {e}") raise