From a2ab68ceb0e7557bdaa14f30cc2991475d71fe95 Mon Sep 17 00:00:00 2001 From: rangoDJ <18576423+rangoDJ@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:43:03 -0400 Subject: [PATCH] Give upload temp files a fixed-length name Uploads were written to ".{filename}.{32 hex}.part" first, which adds 39 bytes to the name, so a valid name over ~216 bytes went past the 255-byte filename limit and the upload failed with a server error. Fixes #43 Co-Authored-By: Claude Opus 5.5 --- root/app/backend/file_manager.py | 6 ++++-- tests/test_file_manager.py | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/root/app/backend/file_manager.py b/root/app/backend/file_manager.py index f31e56a..8663955 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 e82c92d..260d809 100644 --- a/tests/test_file_manager.py +++ b/tests/test_file_manager.py @@ -145,3 +145,37 @@ def test_create_folder_rejects_a_duplicate(shared): with pytest.raises(HTTPException) as exc: fm.create_folder("", "docs") assert exc.value.status_code == 409 + + +# ----------------- 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"