| """MinIO client module for S3-compatible object storage (MP3 files).""" |
|
|
| import os |
| import io |
| from typing import Optional, BinaryIO, Dict, Any |
| from datetime import timedelta |
| from minio import Minio |
| from minio.error import S3Error |
|
|
| |
| MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9000") |
| MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") |
| MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin") |
| MINIO_BUCKET = os.getenv("MINIO_BUCKET", "music-memories") |
| MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" |
|
|
| |
| _minio_client: Optional[Minio] = None |
|
|
|
|
| def get_minio_client() -> Optional[Minio]: |
| """Get or create the MinIO client instance.""" |
| global _minio_client |
| if _minio_client is None: |
| try: |
| _minio_client = Minio( |
| MINIO_ENDPOINT, |
| access_key=MINIO_ACCESS_KEY, |
| secret_key=MINIO_SECRET_KEY, |
| secure=MINIO_SECURE, |
| ) |
| except Exception as e: |
| print(f"⚠ MinIO client creation failed: {e}") |
| return None |
| return _minio_client |
|
|
|
|
| def init_minio() -> bool: |
| """Initialize MinIO bucket.""" |
| client = get_minio_client() |
| if client is None: |
| print("⚠ MinIO not available") |
| return False |
| |
| try: |
| |
| if not client.bucket_exists(MINIO_BUCKET): |
| client.make_bucket(MINIO_BUCKET) |
| print(f"✓ Created MinIO bucket: {MINIO_BUCKET}") |
| else: |
| print(f"✓ MinIO bucket exists: {MINIO_BUCKET}") |
| return True |
| except S3Error as e: |
| print(f"⚠ MinIO bucket creation failed: {e}") |
| return False |
|
|
|
|
| def upload_mp3_file( |
| file_data: BinaryIO, |
| file_size: int, |
| song_id: int, |
| filename: str = None |
| ) -> Optional[Dict[str, Any]]: |
| """Upload an MP3 file to MinIO.""" |
| client = get_minio_client() |
| if client is None: |
| return None |
| |
| |
| object_name = f"songs/{song_id}/{filename or f'song_{song_id}.mp3'}" |
| |
| try: |
| |
| client.put_object( |
| MINIO_BUCKET, |
| object_name, |
| file_data, |
| length=file_size, |
| content_type="audio/mpeg", |
| ) |
| |
| |
| presigned_url = client.presigned_get_object( |
| MINIO_BUCKET, |
| object_name, |
| expires=timedelta(days=7), |
| ) |
| |
| return { |
| "object_name": object_name, |
| "bucket": MINIO_BUCKET, |
| "size": file_size, |
| "presigned_url": presigned_url, |
| "content_type": "audio/mpeg", |
| } |
| except S3Error as e: |
| print(f"⚠ MinIO upload failed: {e}") |
| return None |
|
|
|
|
| def download_mp3_file(song_id: int, filename: str = None, object_name: str = None) -> Optional[bytes]: |
| """Download an MP3 file from MinIO.""" |
| client = get_minio_client() |
| if client is None: |
| return None |
|
|
| |
| if object_name is None: |
| object_name = f"songs/{song_id}/{filename or f'song_{song_id}.mp3'}" |
|
|
| try: |
| response = client.get_object(MINIO_BUCKET, object_name) |
| return response.read() |
| except S3Error as e: |
| print(f"⚠ MinIO download failed: {e}") |
| return None |
|
|
|
|
| def stream_mp3_file(song_id: int, filename: str = None) -> Optional[Any]: |
| """Get a streaming response for an MP3 file.""" |
| client = get_minio_client() |
| if client is None: |
| return None |
| |
| object_name = f"songs/{song_id}/{filename or f'song_{song_id}.mp3'}" |
| |
| try: |
| |
| presigned_url = client.presigned_get_object( |
| MINIO_BUCKET, |
| object_name, |
| expires=timedelta(hours=24), |
| ) |
| return {"presigned_url": presigned_url, "object_name": object_name} |
| except S3Error as e: |
| print(f"⚠ MinIO stream URL generation failed: {e}") |
| return None |
|
|
|
|
| def delete_mp3_file(song_id: int, filename: str = None) -> bool: |
| """Delete an MP3 file from MinIO.""" |
| client = get_minio_client() |
| if client is None: |
| return False |
| |
| object_name = f"songs/{song_id}/{filename or f'song_{song_id}.mp3'}" |
| |
| try: |
| client.remove_object(MINIO_BUCKET, object_name) |
| return True |
| except S3Error as e: |
| print(f"⚠ MinIO delete failed: {e}") |
| return False |
|
|
|
|
| def get_file_metadata(song_id: int, filename: str = None) -> Optional[Dict[str, Any]]: |
| """Get metadata for an MP3 file.""" |
| client = get_minio_client() |
| if client is None: |
| return None |
| |
| object_name = f"songs/{song_id}/{filename or f'song_{song_id}.mp3'}" |
| |
| try: |
| stat = client.stat_object(MINIO_BUCKET, object_name) |
| return { |
| "size": stat.size, |
| "content_type": stat.content_type, |
| "last_modified": stat.last_modified.isoformat() if stat.last_modified else None, |
| "etag": stat.etag, |
| } |
| except S3Error as e: |
| print(f"⚠ MinIO metadata fetch failed: {e}") |
| return None |
|
|
|
|
| def list_all_mp3_files() -> list: |
| """List all MP3 files in the bucket.""" |
| client = get_minio_client() |
| if client is None: |
| return [] |
| |
| try: |
| objects = client.list_objects(MINIO_BUCKET, prefix="songs/", recursive=True) |
| return [ |
| { |
| "object_name": obj.object_name, |
| "size": obj.size, |
| "last_modified": obj.last_modified.isoformat() if obj.last_modified else None, |
| } |
| for obj in objects |
| ] |
| except S3Error as e: |
| print(f"⚠ MinIO list failed: {e}") |
| return [] |
|
|
|
|
| def check_minio_health() -> Dict[str, Any]: |
| """Check MinIO health status.""" |
| client = get_minio_client() |
| if client is None: |
| return {"status": "disconnected", "error": "Client not initialized"} |
| |
| try: |
| |
| exists = client.bucket_exists(MINIO_BUCKET) |
| return { |
| "status": "connected" if exists else "bucket_missing", |
| "bucket": MINIO_BUCKET, |
| "endpoint": MINIO_ENDPOINT, |
| } |
| except S3Error as e: |
| return {"status": "error", "error": str(e)} |
|
|