"""Upload voice confirmation WAV objects to MinIO (S3-compatible).""" from __future__ import annotations import hashlib import io from dataclasses import dataclass from loguru import logger from minio import Minio from minio.error import S3Error from app.config import Settings @dataclass(frozen=True) class StoredAudio: object_key: str sha256_hex: str size_bytes: int class MinioAudioStorageService: """Stores raw doctor voice WAV for audit trail.""" def __init__(self, settings: Settings) -> None: self._s = settings self._client: Minio | None = None if settings.minio_configured: endpoint = settings.minio_endpoint.strip() self._client = Minio( endpoint, access_key=settings.minio_access_key.strip(), secret_key=settings.minio_secret_key.strip(), secure=bool(settings.minio_secure), region=(settings.minio_region or "").strip() or None, ) @property def configured(self) -> bool: return self._client is not None def ensure_bucket(self) -> None: if self._client is None: return name = self._s.minio_bucket.strip() try: if not self._client.bucket_exists(name): self._client.make_bucket(name) logger.info("MinIO bucket created: {}", name) except S3Error as exc: logger.warning("MinIO ensure_bucket failed: {}", exc) raise def upload_voice_wav( self, *, surgery_id: str, confirmation_id: str, data: bytes, content_type: str | None, ) -> StoredAudio: if self._client is None: raise RuntimeError("MinIO is not configured") digest = hashlib.sha256(data).hexdigest() suffix = digest[:12] object_key = ( f"surgeries/{surgery_id}/confirmations/{confirmation_id}/{suffix}.wav" ) bucket = self._s.minio_bucket.strip() ct = content_type or "audio/wav" stream = io.BytesIO(data) try: self._client.put_object( bucket, object_key, stream, length=len(data), content_type=ct, ) except S3Error as exc: logger.warning("MinIO put_object failed: {}", exc) raise return StoredAudio( object_key=object_key, sha256_hex=digest, size_bytes=len(data), )