Modello notifiche + Servizio storage MinIO
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
from sqlalchemy import String, Boolean, DateTime, DECIMAL, Integer, Text, ForeignKey
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Notification(Base):
|
||||||
|
__tablename__ = "notifications"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||||
|
body: Mapped[str | None] = mapped_column(Text)
|
||||||
|
link: Mapped[str | None] = mapped_column(String(500))
|
||||||
|
is_read: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AlertConfig(Base):
|
||||||
|
__tablename__ = "alert_config"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||||
|
key: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||||
|
value: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
description: Mapped[str | None] = mapped_column(String(500))
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
default=lambda: datetime.now(timezone.utc),
|
||||||
|
onupdate=lambda: datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import io
|
||||||
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from minio import Minio
|
||||||
|
from minio.error import S3Error
|
||||||
|
from fastapi import UploadFile, HTTPException, status
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
logger = structlog.get_logger()
|
||||||
|
|
||||||
|
_client: Minio | None = None
|
||||||
|
_public_client: Minio | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_client() -> Minio:
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = Minio(
|
||||||
|
settings.minio_endpoint,
|
||||||
|
access_key=settings.minio_access_key,
|
||||||
|
secret_key=settings.minio_secret_key,
|
||||||
|
secure=settings.minio_secure,
|
||||||
|
)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
def _get_public_client() -> Minio:
|
||||||
|
global _public_client
|
||||||
|
if _public_client is None:
|
||||||
|
parsed = urlparse(settings.minio_public_url)
|
||||||
|
endpoint = parsed.netloc
|
||||||
|
secure = parsed.scheme == "https"
|
||||||
|
_public_client = Minio(
|
||||||
|
endpoint,
|
||||||
|
access_key=settings.minio_access_key,
|
||||||
|
secret_key=settings.minio_secret_key,
|
||||||
|
secure=secure,
|
||||||
|
region="us-east-1",
|
||||||
|
)
|
||||||
|
return _public_client
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_bucket(bucket: str) -> None:
|
||||||
|
client = get_client()
|
||||||
|
try:
|
||||||
|
if not client.bucket_exists(bucket):
|
||||||
|
client.make_bucket(bucket)
|
||||||
|
logger.info("minio_bucket_created", bucket=bucket)
|
||||||
|
except S3Error as exc:
|
||||||
|
logger.error("minio_bucket_error", bucket=bucket, error=str(exc))
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def upload_photo(file: UploadFile, valuation_id: int) -> str:
|
||||||
|
bucket = settings.minio_bucket_photos
|
||||||
|
try:
|
||||||
|
_ensure_bucket(bucket)
|
||||||
|
except S3Error as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="Storage non disponibile",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
ext = ""
|
||||||
|
if file.filename and "." in file.filename:
|
||||||
|
ext = "." + file.filename.rsplit(".", 1)[-1].lower()
|
||||||
|
|
||||||
|
object_name = f"valuations/{valuation_id}/{uuid.uuid4().hex}{ext}"
|
||||||
|
|
||||||
|
content = await file.read()
|
||||||
|
content_length = len(content)
|
||||||
|
content_type = file.content_type or "application/octet-stream"
|
||||||
|
|
||||||
|
client = get_client()
|
||||||
|
try:
|
||||||
|
client.put_object(
|
||||||
|
bucket,
|
||||||
|
object_name,
|
||||||
|
io.BytesIO(content),
|
||||||
|
length=content_length,
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
logger.info("minio_upload_ok", object_name=object_name)
|
||||||
|
return object_name
|
||||||
|
except S3Error as exc:
|
||||||
|
logger.error("minio_upload_error", object_name=object_name, error=str(exc))
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="Errore durante il caricamento del file",
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def get_presigned_url(storage_path: str, expires_hours: int = 4) -> str | None:
|
||||||
|
bucket = settings.minio_bucket_photos
|
||||||
|
client = _get_public_client()
|
||||||
|
try:
|
||||||
|
url = client.presigned_get_object(
|
||||||
|
bucket,
|
||||||
|
storage_path,
|
||||||
|
expires=timedelta(hours=expires_hours),
|
||||||
|
)
|
||||||
|
return url
|
||||||
|
except S3Error as exc:
|
||||||
|
logger.warning("minio_presigned_error", path=storage_path, error=str(exc))
|
||||||
|
return None
|
||||||
Reference in New Issue
Block a user