"""Invoice / proforma attachments — multipart upload, list, download, delete. Multi-file contract (P1-28..33 extended): a quote may hold several attachments per kind; every POST creates a new one. - POST /quotes/{quote_id}/invoice → 201, new attachment metadata - GET /quotes/{quote_id}/invoice → list of attachment metadata - GET /quotes/{quote_id}/invoice/{file_id} → file bytes - DELETE /quotes/{quote_id}/invoice/{file_id} → delete one attachment The same four endpoints exist under /proforma. Bytes live in MinIO (PROCUREMENT_MINIO_BUCKET); the `quote_attachments` table holds metadata. Both invoice and proforma uploads are allowed in every quote status (the user can attach the invoice from the edit page at any time). """ import os import uuid from urllib.parse import quote from fastapi import APIRouter, Depends, File, Request, Response, UploadFile from sqlalchemy.ext.asyncio import AsyncSession from app.auth import UserContext, get_current_user, get_optional_user, require_role from app.db.engine import get_db from app.models.procurement import Quote from app.services import file_service from app.utils.errors import NotFoundError, UnauthorizedError, ValidationError from app.utils.preview_token import make_preview_token, verify_preview_token router = APIRouter(prefix="/quotes", tags=["files"]) # حذف و بارگذاری پیوست‌ها = عملیات تدارکات (ادمین / مدیر تدارکات)؛ دانلود # برای هر کاربر واردشده آزاد است. _attachment_deleter = require_role("procurement_manager", "admin") _attachment_writer = require_role("procurement_manager", "admin") ALLOWED_MIME_TYPES = { "image/png", "image/jpeg", "image/webp", "image/gif", "application/pdf", } ALLOWED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".pdf"} # حداکثر حجم مجاز هر فایل — ۵ مگابایت MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024 def _content_disposition(file_name: str, inline: bool = False) -> str: """Header-safe Content-Disposition for non-ASCII (e.g. Persian) filenames. HTTP headers are encoded as latin-1, so a Persian name raises UnicodeEncodeError and kills the response. Per RFC 6266 / RFC 5987 the UTF-8 name goes percent-encoded in ``filename*``, with an ASCII fallback in ``filename`` for old clients. ``inline=True`` makes the browser render the file (PDF in a viewer, image in the tab) instead of downloading it — required by the preview modal, whose