""" BreakPilot MinIO Storage Helper Provides file upload/download operations for MinIO object storage. """ import os import io import structlog from typing import Optional, BinaryIO log = structlog.get_logger(__name__) class MinIOStorage: """ MinIO storage client for recordings and transcriptions. Provides methods to upload, download, and manage files in MinIO object storage (S3-compatible). """ def __init__( self, endpoint: str = "minio:9000", access_key: str = "breakpilot", secret_key: str = "breakpilot123", bucket: str = "breakpilot-recordings", secure: bool = False ): """ Initialize MinIO client. Args: endpoint: MinIO server endpoint (host:port) access_key: Access key (username) secret_key: Secret key (password) bucket: Default bucket name secure: Use HTTPS """ self.endpoint = endpoint self.access_key = access_key self.secret_key = secret_key self.bucket = bucket self.secure = secure self._client = None def _get_client(self): """Lazy initialize MinIO client.""" if self._client is not None: return self._client try: from minio import Minio self._client = Minio( self.endpoint, access_key=self.access_key, secret_key=self.secret_key, secure=self.secure ) log.info( "minio_client_initialized", endpoint=self.endpoint, bucket=self.bucket ) return self._client except ImportError: log.error("minio_not_installed") raise ImportError( "minio is not installed. " "Install with: pip install minio" ) def ensure_bucket(self) -> bool: """ Ensure the bucket exists, create if needed. Returns: True if bucket exists or was created """ client = self._get_client() if not client.bucket_exists(self.bucket): client.make_bucket(self.bucket) log.info("bucket_created", bucket=self.bucket) return True return True def download_file( self, object_name: str, local_path: str, bucket: Optional[str] = None ) -> str: """ Download a file from MinIO. Args: object_name: Path in MinIO bucket local_path: Local destination path bucket: Optional bucket override Returns: Local file path """ client = self._get_client() bucket = bucket or self.bucket log.info( "downloading_file", bucket=bucket, object_name=object_name, local_path=local_path ) # Ensure directory exists os.makedirs(os.path.dirname(local_path), exist_ok=True) # Download client.fget_object(bucket, object_name, local_path) log.info( "file_downloaded", object_name=object_name, local_path=local_path, size=os.path.getsize(local_path) ) return local_path def upload_file( self, local_path: str, object_name: str, content_type: Optional[str] = None, bucket: Optional[str] = None ) -> str: """ Upload a file to MinIO. Args: local_path: Local file path object_name: Destination path in MinIO content_type: MIME type bucket: Optional bucket override Returns: Object name in MinIO """ client = self._get_client() bucket = bucket or self.bucket # Ensure bucket exists self.ensure_bucket() log.info( "uploading_file", local_path=local_path, bucket=bucket, object_name=object_name ) # Upload result = client.fput_object( bucket, object_name, local_path, content_type=content_type ) log.info( "file_uploaded", object_name=object_name, etag=result.etag ) return object_name def upload_content( self, content: str, object_name: str, content_type: str = "text/plain", bucket: Optional[str] = None ) -> str: """ Upload string content directly to MinIO. Args: content: String content to upload object_name: Destination path in MinIO content_type: MIME type bucket: Optional bucket override Returns: Object name in MinIO """ client = self._get_client() bucket = bucket or self.bucket # Ensure bucket exists self.ensure_bucket() # Convert to bytes data = content.encode("utf-8") data_stream = io.BytesIO(data) log.info( "uploading_content", bucket=bucket, object_name=object_name, size=len(data) ) # Upload result = client.put_object( bucket, object_name, data_stream, length=len(data), content_type=content_type ) log.info( "content_uploaded", object_name=object_name, etag=result.etag ) return object_name def get_content( self, object_name: str, bucket: Optional[str] = None ) -> str: """ Get string content from MinIO. Args: object_name: Path in MinIO bucket bucket: Optional bucket override Returns: File content as string """ client = self._get_client() bucket = bucket or self.bucket response = client.get_object(bucket, object_name) content = response.read().decode("utf-8") response.close() response.release_conn() return content def delete_file( self, object_name: str, bucket: Optional[str] = None ) -> bool: """ Delete a file from MinIO. Args: object_name: Path in MinIO bucket bucket: Optional bucket override Returns: True if deleted """ client = self._get_client() bucket = bucket or self.bucket client.remove_object(bucket, object_name) log.info("file_deleted", object_name=object_name) return True def file_exists( self, object_name: str, bucket: Optional[str] = None ) -> bool: """ Check if a file exists in MinIO. Args: object_name: Path in MinIO bucket bucket: Optional bucket override Returns: True if file exists """ client = self._get_client() bucket = bucket or self.bucket try: client.stat_object(bucket, object_name) return True except Exception: return False def get_presigned_url( self, object_name: str, expires_hours: int = 24, bucket: Optional[str] = None ) -> str: """ Get a presigned URL for temporary file access. Args: object_name: Path in MinIO bucket expires_hours: URL validity in hours bucket: Optional bucket override Returns: Presigned URL """ from datetime import timedelta client = self._get_client() bucket = bucket or self.bucket url = client.presigned_get_object( bucket, object_name, expires=timedelta(hours=expires_hours) ) return url def list_files( self, prefix: str = "", bucket: Optional[str] = None ) -> list: """ List files with a given prefix. Args: prefix: Path prefix to filter bucket: Optional bucket override Returns: List of object names """ client = self._get_client() bucket = bucket or self.bucket objects = client.list_objects(bucket, prefix=prefix, recursive=True) return [obj.object_name for obj in objects]