diff --git a/root/app/backend/file_manager.py b/root/app/backend/file_manager.py index 28bd15b..569efc8 100644 --- a/root/app/backend/file_manager.py +++ b/root/app/backend/file_manager.py @@ -83,8 +83,10 @@ async def save_uploaded_file(upload_file: UploadFile, relative_path: str = "", o if target_file.exists() and not overwrite: raise HTTPException(status_code=409, detail=f"{filename} already exists") - # Write to a temp file first so a failed upload never leaves a truncated file behind - tmp_file = target_dir / f".{filename}.{uuid.uuid4().hex}.part" + # Write to a temp file first so a failed upload never leaves a truncated file behind. + # Its name is fixed-length: built from the upload's name, a long but valid name went + # over the filesystem's 255-byte limit. + tmp_file = target_dir / f".upload-{uuid.uuid4().hex}.part" try: async with aiofiles.open(tmp_file, "wb") as f: while chunk := await upload_file.read(1024 * 1024): # 1MB chunks diff --git a/tests/test_file_manager.py b/tests/test_file_manager.py index 97c17e1..131b408 100644 --- a/tests/test_file_manager.py +++ b/tests/test_file_manager.py @@ -200,3 +200,37 @@ def test_parent_traversal_is_still_rejected_on_delete(): with pytest.raises(HTTPException) as exc: fm.delete_path("../etc") assert exc.value.status_code == 403 + + +# ----------------- Uploads ----------------- + +def upload(name, data=b"hello", path="", overwrite=False): + import asyncio + import io + from fastapi import UploadFile + return asyncio.run(fm.save_uploaded_file(UploadFile(io.BytesIO(data), filename=name), path, overwrite)) + + +@pytest.mark.parametrize("name", [ + pytest.param("a" * 250 + ".txt", id="254-chars", # a valid name on Linux and NTFS + marks=pytest.mark.skipif(os.name == "nt", reason="exceeds Windows' 260-character path limit here")), + pytest.param("é" * 110 + ".txt", id="224-bytes"), # over the old limit in UTF-8 bytes +]) +def test_a_long_but_valid_name_can_be_uploaded(shared, name): + result = upload(name) + assert result["filename"] == name + assert (shared / name).read_bytes() == b"hello" + + +def test_no_temp_file_is_left_behind(shared): + upload("report.txt") + assert sorted(p.name for p in shared.iterdir()) == ["report.txt"] + + +def test_an_existing_file_is_only_replaced_with_overwrite(shared): + upload("report.txt", b"one") + with pytest.raises(HTTPException) as exc: + upload("report.txt", b"two") + assert exc.value.status_code == 409 + upload("report.txt", b"two", overwrite=True) + assert (shared / "report.txt").read_bytes() == b"two"