From 4e431cf8786fa7777b7533cf4a2cf84de91244f6 Mon Sep 17 00:00:00 2001 From: Fish <186391338+wxhking@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:48:18 +0000 Subject: [PATCH] fix(downloads): keep numbered filenames unique beyond 999 collisions --- CHANGELOG.md | 1 + src/octop/infra/utils/user_downloads.py | 12 +++++------- tests/unit/utils/test_user_downloads.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2043145..f067340c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ 格式遵循 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),版本号遵循 [语义化版本规范](https://semver.org/spec/v2.0.0.html)。 ## [Unreleased] +- 本地下载遇到大量同名文件时继续递增编号,避免超过 999 个编号后重复使用进程号文件名并覆盖之前的下载 - 从技能市场把技能集导入为技能包时,某个技能没有安装包不再让整包失败;该技能会被跳过,其余技能照常导入。若全部技能都没有安装包,导入仍然失败 - 老会话在历史还在加载时再发一条消息,这条消息不再从列表里消失;长会话向上补更早的消息时,新消息也不会被错当成插入而留下空白 - 模型提供商配置里点「撤销授权」会清掉已保存的 API Key,卡片回到未授权。只改备注或流式用量时不会误删密钥 diff --git a/src/octop/infra/utils/user_downloads.py b/src/octop/infra/utils/user_downloads.py index 9dfba0cb9..382617aab 100644 --- a/src/octop/infra/utils/user_downloads.py +++ b/src/octop/infra/utils/user_downloads.py @@ -63,14 +63,12 @@ def unique_download_path(filename: str, *, home: Path | None = None) -> Path: dest_dir.mkdir(parents=True, exist_ok=True) safe = sanitize_download_filename(filename) path = dest_dir / safe - if not path.exists(): - return path stem, suffix = path.stem, path.suffix - for index in range(1, 1000): - candidate = dest_dir / f"{stem} ({index}){suffix}" - if not candidate.exists(): - return candidate - return dest_dir / f"{stem}-{os.getpid()}{suffix}" + index = 1 + while path.exists(): + path = dest_dir / f"{stem} ({index}){suffix}" + index += 1 + return path def save_user_download( diff --git a/tests/unit/utils/test_user_downloads.py b/tests/unit/utils/test_user_downloads.py index 3d718aaa1..22b69fa1c 100644 --- a/tests/unit/utils/test_user_downloads.py +++ b/tests/unit/utils/test_user_downloads.py @@ -45,6 +45,22 @@ def test_save_user_download_writes_bytes(tmp_path: Path) -> None: assert path.parent == downloads +def test_save_user_download_preserves_files_after_many_collisions(tmp_path: Path) -> None: + downloads = tmp_path / "Downloads" + downloads.mkdir() + (downloads / "note.txt").write_bytes(b"original") + for index in range(1, 1000): + (downloads / f"note ({index}).txt").touch() + + first = save_user_download("note.txt", b"first", home=tmp_path) + second = save_user_download("note.txt", b"second", home=tmp_path) + + assert first != second + assert first.read_bytes() == b"first" + assert second.read_bytes() == b"second" + assert (downloads / "note.txt").read_bytes() == b"original" + + def test_local_dashboard_request_requires_loopback_host_and_peer() -> None: assert is_local_dashboard_request(_Req("127.0.0.1:8088", "127.0.0.1")) assert is_local_dashboard_request(_Req("localhost:8088", "127.0.0.1"))