# backend/utils/minio_client.py from datetime import timedelta import os from minio import Minio from minio.error import S3Error from django.conf import settings from typing import Optional, BinaryIO import logging logger = logging.getLogger(__name__) class MinIOClient: """Cliente singleton para MinIO con operaciones avanzadas""" _instance = None _client = None _bucket_name = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self): if self._client is None and settings.STORAGE_BACKEND == 'minio': self._initialize_client() def _initialize_client(self): """Inicializa el cliente de MinIO""" try: endpoint = os.getenv('MINIO_ENDPOINT', 'minio:9000') access_key = os.getenv('MINIO_ACCESS_KEY') secret_key = os.getenv('MINIO_SECRET_KEY') secure = os.getenv('MINIO_SECURE', 'false').lower() == 'true' self._client = Minio( endpoint=endpoint, access_key=access_key, secret_key=secret_key, secure=secure ) self._bucket_name = os.environ.get('MINIO_BUCKET_NAME', 'efc-backend-dev') # Asegurar que el bucket existe if not self._client.bucket_exists(self._bucket_name): self._client.make_bucket(self._bucket_name) except Exception as e: raise def upload_file( self, object_name: str, file_path: str = None, file_data: BinaryIO = None, content_type: str = None, metadata: dict = None ) -> bool: """ Sube un archivo a MinIO Args: object_name: Ruta del objeto en el bucket (ej: 'documents/archivo.xml') file_path: Ruta local del archivo (opcional) file_data: Datos del archivo en memoria (opcional) content_type: MIME type del archivo metadata: Metadatos adicionales Returns: bool: True si se subió correctamente """ try: if file_path: self._client.fput_object( bucket_name=self._bucket_name, object_name=object_name, file_path=file_path, content_type=content_type, metadata=metadata ) elif file_data: self._client.put_object( bucket_name=self._bucket_name, object_name=object_name, data=file_data, length=-1, part_size=10*1024*1024, # 10MB content_type=content_type, metadata=metadata ) else: raise ValueError("You must provide file_path or file_data") return True except S3Error as e: return False def get_file_url(self, object_name: str, expires: int = 3600) -> Optional[str]: """Genera una URL firmada para acceder al archivo""" try: url = self._client.presigned_get_object( bucket_name=self._bucket_name, object_name=object_name, expires=timedelta(seconds=expires) ) # Reemplazar endpoint interno por público si está configurado public_endpoint = os.getenv('MINIO_PUBLIC_ENDPOINT') if public_endpoint and url: internal_endpoint = os.getenv('MINIO_ENDPOINT', 'minio:9000') url = url.replace(internal_endpoint, public_endpoint) return url except S3Error as e: return None def delete_file(self, object_name: str) -> bool: """Elimina un archivo del bucket""" try: self._client.remove_object( bucket_name=self._bucket_name, object_name=object_name ) return True except S3Error as e: return False def file_exists(self, object_name: str) -> bool: """Verifica si un archivo existe en el bucket""" try: self._client.stat_object( bucket_name=self._bucket_name, object_name=object_name ) return True except S3Error: return False # Singleton para uso global minio_client = MinIOClient()