mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 20:55:41 +02:00
196 lines
5.7 KiB
Python
196 lines
5.7 KiB
Python
"""
|
|
Client MinIO/S3 asincrono per il worker.
|
|
|
|
Percorso EML raw: pecflow/tenants/{tenant_id}/mailboxes/{mailbox_id}/raw/{uid}.eml
|
|
Percorso allegati: pecflow/tenants/{tenant_id}/mailboxes/{mailbox_id}/attachments/{msg_id}/{filename}
|
|
"""
|
|
|
|
import io
|
|
import logging
|
|
from functools import lru_cache
|
|
|
|
from miniopy_async import Minio
|
|
|
|
from app.config import get_settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
settings = get_settings()
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_minio_client() -> Minio:
|
|
"""Restituisce l'istanza singleton del client MinIO."""
|
|
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_eml(
|
|
tenant_id: str,
|
|
mailbox_id: str,
|
|
uid: int,
|
|
eml_bytes: bytes,
|
|
) -> str:
|
|
"""
|
|
Carica un raw EML su MinIO e restituisce il percorso oggetto.
|
|
|
|
Percorso: tenants/{tenant_id}/mailboxes/{mailbox_id}/raw/{uid}.eml
|
|
"""
|
|
client = get_minio_client()
|
|
bucket = settings.minio_bucket
|
|
object_path = f"tenants/{tenant_id}/mailboxes/{mailbox_id}/raw/{uid}.eml"
|
|
|
|
try:
|
|
data_stream = io.BytesIO(eml_bytes)
|
|
await client.put_object(
|
|
bucket_name=bucket,
|
|
object_name=object_path,
|
|
data=data_stream,
|
|
length=len(eml_bytes),
|
|
content_type="message/rfc822",
|
|
)
|
|
logger.debug(f"EML caricato: s3://{bucket}/{object_path} ({len(eml_bytes)} bytes)")
|
|
return object_path
|
|
except Exception as e:
|
|
logger.error(f"Errore upload EML {object_path}: {e}")
|
|
raise
|
|
|
|
|
|
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 su MinIO e restituisce il percorso oggetto.
|
|
|
|
Percorso: tenants/{tenant_id}/mailboxes/{mailbox_id}/attachments/{message_id}/{filename}
|
|
|
|
Args:
|
|
tenant_id: UUID del tenant
|
|
mailbox_id: UUID della casella
|
|
message_id: UUID del messaggio a cui appartiene l'allegato
|
|
filename: nome file dell'allegato (usato nel path)
|
|
content: byte del file
|
|
content_type: MIME type del file
|
|
|
|
Returns:
|
|
Percorso oggetto su MinIO (senza il nome bucket)
|
|
"""
|
|
client = get_minio_client()
|
|
bucket = settings.minio_bucket
|
|
|
|
# Sanitizza il filename per evitare path traversal
|
|
safe_filename = _sanitize_filename(filename)
|
|
object_path = (
|
|
f"tenants/{tenant_id}/mailboxes/{mailbox_id}"
|
|
f"/attachments/{message_id}/{safe_filename}"
|
|
)
|
|
|
|
try:
|
|
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 caricato: s3://{bucket}/{object_path} "
|
|
f"({len(content)} bytes, {content_type})"
|
|
)
|
|
return object_path
|
|
except Exception as e:
|
|
logger.error(f"Errore upload allegato {object_path}: {e}")
|
|
raise
|
|
|
|
|
|
def _sanitize_filename(filename: str) -> str:
|
|
"""
|
|
Sanitizza il nome file per uso sicuro come path MinIO.
|
|
|
|
Rimuove caratteri pericolosi mantenendo l'estensione originale.
|
|
"""
|
|
import re
|
|
# Rimuovi path separators e null bytes
|
|
safe = filename.replace("/", "_").replace("\\", "_").replace("\x00", "")
|
|
# Rimuovi caratteri non ASCII e di controllo
|
|
safe = re.sub(r"[^\w.\-() ]", "_", safe, flags=re.UNICODE)
|
|
# Limita la lunghezza (MinIO ha limite 1024 chars per object name)
|
|
if len(safe) > 200:
|
|
# Mantieni estensione
|
|
parts = safe.rsplit(".", 1)
|
|
if len(parts) == 2:
|
|
safe = parts[0][:196] + "." + parts[1]
|
|
else:
|
|
safe = safe[:200]
|
|
return safe or "attachment"
|
|
|
|
|
|
async def upload_outbound_eml(
|
|
tenant_id: str,
|
|
mailbox_id: str,
|
|
message_id: str,
|
|
eml_bytes: bytes,
|
|
) -> str:
|
|
"""
|
|
Carica il raw EML di un messaggio outbound su MinIO.
|
|
|
|
Percorso: tenants/{tenant_id}/mailboxes/{mailbox_id}/outbound/{message_id}.eml
|
|
|
|
Args:
|
|
tenant_id: UUID del tenant
|
|
mailbox_id: UUID della casella mittente
|
|
message_id: UUID del messaggio
|
|
eml_bytes: byte del raw EML
|
|
|
|
Returns:
|
|
Percorso oggetto su MinIO (senza bucket name)
|
|
"""
|
|
client = get_minio_client()
|
|
bucket = settings.minio_bucket
|
|
object_path = (
|
|
f"tenants/{tenant_id}/mailboxes/{mailbox_id}/outbound/{message_id}.eml"
|
|
)
|
|
|
|
try:
|
|
import io as _io
|
|
data_stream = _io.BytesIO(eml_bytes)
|
|
await client.put_object(
|
|
bucket_name=bucket,
|
|
object_name=object_path,
|
|
data=data_stream,
|
|
length=len(eml_bytes),
|
|
content_type="message/rfc822",
|
|
)
|
|
logger.debug(
|
|
f"EML outbound caricato: s3://{bucket}/{object_path} "
|
|
f"({len(eml_bytes)} bytes)"
|
|
)
|
|
return object_path
|
|
except Exception as e:
|
|
logger.error(f"Errore upload EML outbound {object_path}: {e}")
|
|
raise
|
|
|
|
|
|
async def ensure_bucket_exists() -> None:
|
|
"""Verifica che il bucket MinIO esista, altrimenti lo crea."""
|
|
client = get_minio_client()
|
|
bucket = settings.minio_bucket
|
|
try:
|
|
found = await client.bucket_exists(bucket)
|
|
if not found:
|
|
await client.make_bucket(bucket)
|
|
logger.info(f"Bucket MinIO creato: {bucket}")
|
|
else:
|
|
logger.debug(f"Bucket MinIO esistente: {bucket}")
|
|
except Exception as e:
|
|
logger.warning(f"Impossibile verificare/creare bucket MinIO: {e}")
|