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
12 changes: 12 additions & 0 deletions root/app/backend/file_manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import re
import shutil
import time
import uuid
from pathlib import Path
from typing import List, Dict, Any
Expand All @@ -9,6 +11,12 @@

SHARED_DIR = Path(os.environ.get("SHARED_DIR", "/shared")).resolve()

# Uploads are written to a temp file first (see save_uploaded_file). A running upload
# keeps touching its file; one untouched this long was cut off (container stopped
# mid-upload, so the cleanup never ran) and is removed the next time its folder is listed.
UPLOAD_TEMP_NAME = re.compile(r"^\.upload-[0-9a-f]{32}\.part$")
STALE_UPLOAD_SECONDS = 24 * 60 * 60

def ensure_shared_dir():
SHARED_DIR.mkdir(parents=True, exist_ok=True)

Expand Down Expand Up @@ -52,6 +60,10 @@ def list_files(relative_path: str = "") -> List[Dict[str, Any]]:
for entry in target.iterdir():
try:
stat = entry.stat()
if UPLOAD_TEMP_NAME.match(entry.name):
if entry.is_file() and time.time() - stat.st_mtime > STALE_UPLOAD_SECONDS:
entry.unlink(missing_ok=True)
continue
is_dir = entry.is_dir()
rel_entry = entry.relative_to(SHARED_DIR).as_posix()
items.append({
Expand Down
40 changes: 40 additions & 0 deletions tests/test_file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,43 @@ def test_an_existing_file_is_only_replaced_with_overwrite(shared):
assert exc.value.status_code == 409
upload("report.txt", b"two", overwrite=True)
assert (shared / "report.txt").read_bytes() == b"two"


# ----------------- Upload temp files -----------------

def temp_name():
import uuid
return f".upload-{uuid.uuid4().hex}.part"


def test_upload_temp_files_are_not_listed(shared):
(shared / temp_name()).write_bytes(b"partial")
(shared / "report.txt").write_text("x")
assert [f["name"] for f in fm.list_files("")] == ["report.txt"]


def test_a_stale_upload_temp_file_is_removed(shared):
stale = shared / temp_name()
stale.write_bytes(b"cut off")
old = stale.stat().st_mtime - fm.STALE_UPLOAD_SECONDS - 60
os.utime(stale, (old, old))
fm.list_files("")
assert not stale.exists()


def test_a_running_upload_temp_file_is_kept(shared):
running = shared / temp_name()
running.write_bytes(b"in progress")
fm.list_files("")
assert running.exists()


@pytest.mark.parametrize("name", [".upload-notes.part", "upload-" + "0" * 32 + ".part", ".hidden"])
def test_files_that_only_look_similar_are_listed(shared, name):
(shared / name).write_text("x")
assert [f["name"] for f in fm.list_files("")] == [name]


def test_a_finished_upload_leaves_no_temp_file(shared):
upload("done.txt")
assert [p.name for p in shared.iterdir()] == ["done.txt"]
Loading