"""Batch operations API routes.""" import os from pathlib import Path from urllib.parse import quote from fastapi import APIRouter, BackgroundTasks, status from fastapi.responses import FileResponse from app.api.dependencies import CurrentUser, DatabaseSession, S3ClientDep from app.api.schemas import ( BatchDeleteRequest, BatchDeleteResponse, BatchDownloadRequest, BatchMoveRequest, BatchMoveResponse, ) 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, current_user: CurrentUser, session: DatabaseSession, s3_client: S3ClientDep, ): """ Delete multiple assets. Args: request: Batch delete request current_user: Current authenticated user session: Database session s3_client: S3 client Returns: Deletion statistics """ batch_service = BatchOperationsService(session, s3_client) result = await batch_service.delete_assets_batch( user_id=current_user.id, asset_ids=request.asset_ids, ) return result @router.post("/move", response_model=BatchMoveResponse) async def batch_move( request: BatchMoveRequest, current_user: CurrentUser, session: DatabaseSession, s3_client: S3ClientDep, ): """ Move multiple assets to a folder. Args: request: Batch move request current_user: Current authenticated user session: Database session s3_client: S3 client Returns: Move statistics """ batch_service = BatchOperationsService(session, s3_client) result = await batch_service.move_assets_batch( user_id=current_user.id, asset_ids=request.asset_ids, target_folder_id=request.folder_id, ) return result @router.post("/download") async def batch_download( request: BatchDownloadRequest, current_user: CurrentUser, session: DatabaseSession, s3_client: S3ClientDep, background_tasks: BackgroundTasks, ): """ Download multiple assets as a ZIP archive using streaming. Uses temp file and FileResponse to avoid loading entire ZIP into memory. Temp file is automatically cleaned up after response is sent. Args: request: Batch download request current_user: Current authenticated user session: Database session s3_client: S3 client background_tasks: Background tasks for cleanup Returns: ZIP file response """ batch_service = BatchOperationsService(session, s3_client) temp_zip_path, filename = await batch_service.download_assets_batch( user_id=current_user.id, asset_ids=request.asset_ids, ) # Schedule temp file cleanup after response is sent def cleanup_temp_file(): try: Path(temp_zip_path).unlink(missing_ok=True) except Exception: pass background_tasks.add_task(cleanup_temp_file) # Return file using streaming FileResponse return FileResponse( path=temp_zip_path, media_type="application/zip", filename=filename, headers={ "Content-Disposition": make_content_disposition(filename), }, ) @router.get("/folders/{folder_id}/download") async def download_folder( folder_id: str, current_user: CurrentUser, session: DatabaseSession, s3_client: S3ClientDep, background_tasks: BackgroundTasks, ): """ Download all assets in a folder as a ZIP archive using streaming. Uses temp file and FileResponse to avoid loading entire ZIP into memory. Temp file is automatically cleaned up after response is sent. Args: folder_id: Folder ID current_user: Current authenticated user session: Database session s3_client: S3 client background_tasks: Background tasks for cleanup Returns: ZIP file response """ batch_service = BatchOperationsService(session, s3_client) temp_zip_path, filename = await batch_service.download_folder( user_id=current_user.id, folder_id=folder_id, ) # Schedule temp file cleanup after response is sent def cleanup_temp_file(): try: Path(temp_zip_path).unlink(missing_ok=True) except Exception: pass background_tasks.add_task(cleanup_temp_file) # Return file using streaming FileResponse return FileResponse( path=temp_zip_path, media_type="application/zip", filename=filename, headers={ "Content-Disposition": make_content_disposition(filename), }, )