Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions root/app/backend/file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions tests/test_file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading