Files

77 lines
2.5 KiB
Python

"""Invoice / proforma attachments — metadata in `quote_attachments`, bytes in MinIO.
Multi-file contract: a quote may hold any number of attachments per kind
(invoice / proforma). Every upload creates a new DB row and a new object
(object key `{quote_id}/{kind}/{attachment_id}` inside the MinIO bucket).
"""
import uuid
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.files import QuoteAttachment
from app.services import storage
from app.utils.errors import NotFoundError
def _object_key(quote_id: uuid.UUID, kind: str, attachment_id: uuid.UUID) -> str:
return f"{quote_id}/{kind}/{attachment_id}"
async def create(
db: AsyncSession,
quote_id: uuid.UUID,
kind: str,
file_name: str,
content: bytes,
mime_type: str,
) -> QuoteAttachment:
"""Upload the bytes to MinIO, then persist the metadata row."""
attachment_id = uuid.uuid4()
object_key = _object_key(quote_id, kind, attachment_id)
await storage.put_object(object_key, content, mime_type)
attachment = QuoteAttachment(
id=attachment_id,
quote_id=quote_id,
kind=kind,
file_name=file_name,
mime_type=mime_type,
size_bytes=len(content),
object_key=object_key,
)
db.add(attachment)
try:
await db.flush()
except Exception:
# Metadata insert failed — best-effort cleanup of the orphan object.
await storage.remove_object(object_key)
raise
return attachment
async def list_for(db: AsyncSession, quote_id: uuid.UUID, kind: str) -> list[QuoteAttachment]:
result = await db.execute(
select(QuoteAttachment)
.where(QuoteAttachment.quote_id == quote_id, QuoteAttachment.kind == kind)
.order_by(QuoteAttachment.uploaded_at)
)
return list(result.scalars().all())
async def get(db: AsyncSession, attachment_id: uuid.UUID) -> QuoteAttachment:
attachment = await db.get(QuoteAttachment, attachment_id)
if attachment is None:
raise NotFoundError("فایل پیوست یافت نشد")
return attachment
async def read_bytes(attachment: QuoteAttachment) -> bytes:
"""Stream the object bytes from MinIO."""
return await storage.get_object_bytes(attachment.object_key)
async def remove(db: AsyncSession, attachment: QuoteAttachment) -> None:
"""Delete the object from MinIO (best-effort) and the metadata row."""
await storage.remove_object(attachment.object_key)
await db.delete(attachment)