Sfoglia il codice sorgente

Merge pull request #2610 from Person2099/feature/upload-prefer-filename-for-name

Expose prefer_filename_for_name on manual archive upload endpoint
MartinNYHC 1 mese fa
parent
commit
797856bcf9

+ 34 - 2
backend/app/api/routes/archives.py

@@ -3429,10 +3429,28 @@ async def get_plate_preview(
 async def upload_archive(
     file: UploadFile = File(...),
     printer_id: int | None = None,
+    prefer_filename_for_name: bool = Query(
+        False,
+        description=(
+            "Name the archive after the uploaded filename instead of the print_name "
+            "embedded in the 3MF's metadata. Off by default, which keeps the embedded "
+            "name. Turn it on when the filename you send is the meaningful one — an "
+            "integration naming files after its own jobs, or a file whose embedded "
+            "title is a stale name from whoever originally sliced it."
+        ),
+    ),
     db: AsyncSession = Depends(get_db),
     current_user: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_CREATE),
 ):
-    """Manually upload a 3MF file to archive."""
+    """Manually upload a 3MF file to archive.
+
+    prefer_filename_for_name is the same flag the FTP review flow and
+    virtual-printer dispatch already pass to ArchiveService.archive_print —
+    this endpoint just didn't expose it (#1152 follow-up). Those callers derive
+    it from the VP-scoped `virtual_printer_archive_name_source` setting; here it
+    is per-request, because the caller is an API client that knows whether the
+    filename it sent is the meaningful one (#2609).
+    """
     if not file.filename or not file.filename.endswith(".3mf"):
         raise HTTPException(400, "File must be a .3mf file")
 
@@ -3458,6 +3476,7 @@ async def upload_archive(
             printer_id=printer_id,
             source_file=temp_path,
             created_by_id=current_user.id if current_user else None,
+            prefer_filename_for_name=prefer_filename_for_name,
         )
 
         if not archive:
@@ -3473,10 +3492,22 @@ async def upload_archive(
 async def upload_archives_bulk(
     files: list[UploadFile] = File(...),
     printer_id: int | None = None,
+    prefer_filename_for_name: bool = Query(
+        False,
+        description=(
+            "Name each archive after its uploaded filename instead of the print_name "
+            "embedded in the 3MF's metadata. Applies to every file in the batch. Off "
+            "by default, which keeps the embedded name."
+        ),
+    ),
     db: AsyncSession = Depends(get_db),
     current_user: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_CREATE),
 ):
-    """Bulk upload multiple 3MF files to archive."""
+    """Bulk upload multiple 3MF files to archive.
+
+    prefer_filename_for_name applies to every file in the batch. See
+    upload_archive for the flag's lineage.
+    """
     from backend.app.api.routes.library import validate_print_file_upload
 
     results = []
@@ -3511,6 +3542,7 @@ async def upload_archives_bulk(
                 printer_id=printer_id,
                 source_file=temp_path,
                 created_by_id=current_user.id if current_user else None,
+                prefer_filename_for_name=prefer_filename_for_name,
             )
 
             if archive:

+ 117 - 0
backend/tests/integration/test_archives_api.py

@@ -4,6 +4,7 @@ Tests the full request/response cycle for /api/v1/archives/ endpoints.
 """
 
 from pathlib import Path
+from unittest.mock import AsyncMock, patch
 
 import pytest
 from httpx import AsyncClient
@@ -12,6 +13,122 @@ from httpx import AsyncClient
 class TestArchivesAPI:
     """Integration tests for /api/v1/archives/ endpoints."""
 
+    # ========================================================================
+    # Upload endpoint
+    # ========================================================================
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    @pytest.mark.parametrize("prefer_filename_for_name", [True, False])
+    async def test_upload_archive_forwards_prefer_filename_for_name(
+        self, async_client: AsyncClient, archive_factory, printer_factory, prefer_filename_for_name
+    ):
+        """POST /archives/upload must forward prefer_filename_for_name to
+        ArchiveService.archive_print unchanged — this flag lets a caller (e.g.
+        the manual upload UI or an external integration) ask for the uploaded
+        filename to win over the 3MF's embedded print_name (#1152 follow-up:
+        the flag existed on the service but wasn't exposed on this route).
+
+        archive_print is mocked (real 3MF metadata extraction isn't under test
+        here) but its return value is a real PrintArchive row from the
+        factory, so ArchiveResponse.model_validate in the route still exercises
+        real serialization instead of masking a broken response behind a bare
+        MagicMock.
+        """
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id, print_name="Mocked Return Archive")
+        archive_print_mock = AsyncMock(return_value=archive)
+
+        files = {"file": ("My Print (final).gcode.3mf", b"PK\x03\x04fake3mf", "application/octet-stream")}
+        params = {"prefer_filename_for_name": prefer_filename_for_name}
+
+        with patch(
+            "backend.app.api.routes.archives.ArchiveService.archive_print",
+            archive_print_mock,
+        ):
+            response = await async_client.post("/api/v1/archives/upload", files=files, params=params)
+
+        assert response.status_code == 200
+        assert archive_print_mock.await_count == 1
+        kwargs = archive_print_mock.await_args.kwargs
+        assert kwargs.get("prefer_filename_for_name") is prefer_filename_for_name
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_upload_archive_defaults_prefer_filename_for_name_false(
+        self, async_client: AsyncClient, archive_factory, printer_factory
+    ):
+        """Omitting the query param must not change existing behavior for
+        callers that predate this flag."""
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id, print_name="Mocked Return Archive")
+        archive_print_mock = AsyncMock(return_value=archive)
+
+        files = {"file": ("existing-caller.gcode.3mf", b"PK\x03\x04fake3mf", "application/octet-stream")}
+
+        with patch(
+            "backend.app.api.routes.archives.ArchiveService.archive_print",
+            archive_print_mock,
+        ):
+            response = await async_client.post("/api/v1/archives/upload", files=files)
+
+        assert response.status_code == 200
+        kwargs = archive_print_mock.await_args.kwargs
+        assert kwargs.get("prefer_filename_for_name") is False
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    @pytest.mark.parametrize("prefer_filename_for_name", [True, False])
+    async def test_upload_archives_bulk_forwards_prefer_filename_for_name(
+        self, async_client: AsyncClient, archive_factory, printer_factory, prefer_filename_for_name
+    ):
+        """POST /archives/upload-bulk must forward prefer_filename_for_name to
+        ArchiveService.archive_print for every file in the batch, keeping this
+        route consistent with the single-file /archives/upload endpoint."""
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id, print_name="Mocked Return Archive")
+        archive_print_mock = AsyncMock(return_value=archive)
+
+        files = [
+            ("files", ("first.gcode.3mf", b"PK\x03\x04fake3mf", "application/octet-stream")),
+            ("files", ("second.gcode.3mf", b"PK\x03\x04fake3mf", "application/octet-stream")),
+        ]
+        params = {"prefer_filename_for_name": prefer_filename_for_name}
+
+        with patch(
+            "backend.app.api.routes.archives.ArchiveService.archive_print",
+            archive_print_mock,
+        ):
+            response = await async_client.post("/api/v1/archives/upload-bulk", files=files, params=params)
+
+        assert response.status_code == 200
+        assert archive_print_mock.await_count == 2
+        for call in archive_print_mock.await_args_list:
+            assert call.kwargs.get("prefer_filename_for_name") is prefer_filename_for_name
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_upload_archives_bulk_defaults_prefer_filename_for_name_false(
+        self, async_client: AsyncClient, archive_factory, printer_factory
+    ):
+        """Omitting the query param on the bulk route must not change existing
+        behavior for callers that predate this flag."""
+        printer = await printer_factory()
+        archive = await archive_factory(printer.id, print_name="Mocked Return Archive")
+        archive_print_mock = AsyncMock(return_value=archive)
+
+        files = [("files", ("existing-caller.gcode.3mf", b"PK\x03\x04fake3mf", "application/octet-stream"))]
+
+        with patch(
+            "backend.app.api.routes.archives.ArchiveService.archive_print",
+            archive_print_mock,
+        ):
+            response = await async_client.post("/api/v1/archives/upload-bulk", files=files)
+
+        assert response.status_code == 200
+        kwargs = archive_print_mock.await_args.kwargs
+        assert kwargs.get("prefer_filename_for_name") is False
+
     # ========================================================================
     # List endpoints
     # ========================================================================