""" Client MinIO per il backend – upload allegati outbound. Percorso allegati outbound: tenants/{tenant_id}/mailboxes/{mailbox_id}/attachments/{message_id}/{filename} """ import io import logging import re from functools import lru_cache from miniopy_async import Minio from app.config import get_settings logger = logging.getLogger(__name__) @lru_cache(maxsize=1) def get_minio_client() -> Minio: """Restituisce l'istanza singleton del client MinIO.""" settings = get_settings() return Minio( endpoint=settings.minio_endpoint, access_key=settings.minio_access_key, secret_key=settings.minio_secret_key, secure=settings.minio_use_ssl, ) async def upload_attachment( tenant_id: str, mailbox_id: str, message_id: str, filename: str, content: bytes, content_type: str = "application/octet-stream", ) -> str: """ Carica un allegato outbound su MinIO. Args: tenant_id: UUID del tenant (stringa) mailbox_id: UUID della casella mittente message_id: UUID del messaggio associato filename: Nome file originale content: Byte del file content_type: MIME type Returns: Percorso oggetto su MinIO (senza nome bucket) """ settings = get_settings() client = get_minio_client() bucket = settings.minio_bucket safe_filename = _sanitize_filename(filename) object_path = ( f"tenants/{tenant_id}/mailboxes/{mailbox_id}" f"/attachments/{message_id}/{safe_filename}" ) data_stream = io.BytesIO(content) await client.put_object( bucket_name=bucket, object_name=object_path, data=data_stream, length=len(content), content_type=content_type, ) logger.debug( f"Allegato outbound caricato: {object_path} " f"({len(content)} bytes, {content_type})" ) return object_path def _sanitize_filename(filename: str) -> str: """Sanitizza il nome file per uso sicuro come path MinIO.""" safe = filename.replace("/", "_").replace("\\", "_").replace("\x00", "") safe = re.sub(r"[^\w.\-() ]", "_", safe, flags=re.UNICODE) if len(safe) > 200: parts = safe.rsplit(".", 1) if len(parts) == 2: safe = parts[0][:196] + "." + parts[1] else: safe = safe[:200] return safe or "attachment"