diff --git a/backend/src/app/api/v1/batch.py b/backend/src/app/api/v1/batch.py index 04eca5c..5250523 100644 --- a/backend/src/app/api/v1/batch.py +++ b/backend/src/app/api/v1/batch.py @@ -2,6 +2,7 @@ import os from pathlib import Path +from urllib.parse import quote from fastapi import APIRouter, BackgroundTasks, status from fastapi.responses import FileResponse @@ -19,6 +20,28 @@ from app.services.batch_operations_service import BatchOperationsService router = APIRouter(prefix="/batch", tags=["batch"]) +def make_content_disposition(filename: str) -> str: + """ + Create Content-Disposition header value with proper encoding for non-ASCII filenames. + + Uses RFC 5987/2231 encoding to support UTF-8 filenames. + + Args: + filename: Original filename (may contain non-ASCII characters) + + Returns: + Properly formatted Content-Disposition header value + """ + # ASCII-safe fallback (replace non-ASCII with underscore) + ascii_filename = filename.encode("ascii", errors="replace").decode("ascii") + + # UTF-8 encoded filename (RFC 5987) + utf8_filename = quote(filename.encode("utf-8")) + + # Return both for maximum compatibility + return f"attachment; filename=\"{ascii_filename}\"; filename*=UTF-8''{utf8_filename}" + + @router.post("/delete", response_model=BatchDeleteResponse) async def batch_delete( request: BatchDeleteRequest, @@ -119,7 +142,7 @@ async def batch_download( media_type="application/zip", filename=filename, headers={ - "Content-Disposition": f'attachment; filename="{filename}"', + "Content-Disposition": make_content_disposition(filename), }, ) @@ -169,6 +192,6 @@ async def download_folder( media_type="application/zip", filename=filename, headers={ - "Content-Disposition": f'attachment; filename="{filename}"', + "Content-Disposition": make_content_disposition(filename), }, )