fix(files): name single-folder downloads after the folder

The File Browser's selected-files download flow zips any selection
that is not a single file and always applies the generic
agent-zero-selected-<count>-<stamp>.zip archive name, so downloading
one folder produced agent-zero-selected-1-<date>-<time>.zip instead
of <folder>.zip.

Name single-directory downloads after the folder in both the local and
remote branches of prepare_files_download; true multi-selections keep
the timestamped generic name. Add regression tests for both cases.
This commit is contained in:
GreifMax 2026-10-12 21:58:36 +02:00
parent bea0c16d50
commit 17f06bfcdc
3 changed files with 42 additions and 3 deletions

View file

@ -532,7 +532,9 @@ def prepare_files_download(paths, current_path=""):
except BaseException:
Path(path).unlink(missing_ok=True)
raise
name = posixpath.basename(paths[0]) if single else selected_archive_name(len(paths))
name = posixpath.basename(paths[0]) if single else (
f"{posixpath.basename(paths[0])}.zip" if len(paths) == 1 else selected_archive_name(len(paths))
)
temporary = True
elif len(paths) == 1 and Path(paths[0]).is_file():
import stat
@ -557,7 +559,10 @@ def prepare_files_download(paths, current_path=""):
temporary = True
else:
path = create_selected_zip(paths, current_path, limit, FileBrowser.max_archive_entries())
name = selected_archive_name(len(paths))
name = (
f"{Path(paths[0]).name}.zip" if len(paths) == 1
else selected_archive_name(len(paths))
)
temporary = True
return {"file_source": path, "download_name": name, "max_bytes": limit, "delete_after": temporary}

View file

@ -58,7 +58,7 @@
No child DOX files.
`read_text` owns bounded local Editor reads. `encode_upload` bounds the development RFC adapter. `prepare_files_download` owns Files routing and policy; `register_files_download` rechecks remote permission at delivery. API handlers do not select byte limits. Generic attachment/Connector downloads remain separate.
`read_text` owns bounded local Editor reads. `encode_upload` bounds the development RFC adapter. `prepare_files_download` owns Files routing and policy; a single selected directory downloads as `<folder>.zip` while true multi-selections keep the timestamped `agent-zero-selected-<count>-<stamp>.zip` archive name. `register_files_download` rechecks remote permission at delivery. API handlers do not select byte limits. Generic attachment/Connector downloads remain separate.
Destructive entry operations resolve parent directories while preserving the final symlink. Delete/Rename operate on the link itself; empty/root targets are rejected in the shared helper. Local Files downloads stage a bounded snapshot and reject in-place source changes during preparation. Cancelled preparation closes late responses and their temporary descriptors.

View file

@ -0,0 +1,34 @@
from pathlib import Path
import zipfile
from helpers.file_browser import prepare_files_download
def test_single_folder_download_is_named_after_the_folder(tmp_path: Path) -> None:
folder = tmp_path / "my-folder"
folder.mkdir()
(folder / "file.txt").write_text("content", encoding="utf-8")
# A single selected folder downloads as <folder>.zip, not the generic
# agent-zero-selected-<count>-<stamp>.zip archive name.
download = prepare_files_download([str(folder)], current_path=str(tmp_path))
try:
assert download["download_name"] == "my-folder.zip"
assert zipfile.is_zipfile(download["file_source"])
finally:
Path(download["file_source"]).unlink(missing_ok=True)
def test_multi_selection_download_keeps_generic_archive_name(tmp_path: Path) -> None:
one = tmp_path / "one.txt"
two = tmp_path / "two.txt"
one.write_text("1", encoding="utf-8")
two.write_text("2", encoding="utf-8")
download = prepare_files_download([str(one), str(two)], current_path=str(tmp_path))
try:
# True multi-selections keep the timestamped generic archive name.
assert download["download_name"].startswith("agent-zero-selected-2-")
assert download["download_name"].endswith(".zip")
finally:
Path(download["file_source"]).unlink(missing_ok=True)