Skip to content

Commit 0321474

Browse files
committed
fix: harden attachment export ids and tune io chunking
1 parent 553e5b7 commit 0321474

4 files changed

Lines changed: 37 additions & 3 deletions

File tree

astrbot/core/backup/exporter.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,15 @@ def _export_attachments_sync(
369369
"""在单个线程中批量导出附件,减少高频线程切换。"""
370370
for attachment in attachments:
371371
file_path = attachment.get("path", "")
372-
attachment_id = attachment.get("attachment_id", "")
372+
attachment_id = attachment.get("attachment_id")
373373
try:
374374
if not file_path:
375375
continue
376+
if not attachment_id:
377+
logger.warning(
378+
f"跳过附件导出:attachment_id 为空 (path={file_path})"
379+
)
380+
continue
376381
# 使用 attachment_id 作为文件名
377382
ext = os.path.splitext(file_path)[1]
378383
archive_path = f"files/attachments/{attachment_id}{ext}"

astrbot/core/utils/io.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .astrbot_path import get_astrbot_data_path, get_astrbot_path, get_astrbot_temp_path
2020

2121
logger = logging.getLogger("astrbot")
22+
_DOWNLOAD_READ_CHUNK_SIZE = 64 * 1024
2223

2324

2425
def on_error(func, path, exc_info) -> None:
@@ -197,7 +198,7 @@ async def _stream_to_file(
197198
known_total = total_size if total_size > 0 else None
198199

199200
while True:
200-
chunk = await stream.read(8192)
201+
chunk = await stream.read(_DOWNLOAD_READ_CHUNK_SIZE)
201202
if not chunk:
202203
break
203204
await asyncio.to_thread(file_obj.write, chunk)

tests/performance/test_benchmarks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ async def bench_file_component_get_file() -> None:
176176
await file_component.get_file()
177177

178178
async def bench_to_thread_exists() -> None:
179-
await asyncio.to_thread(Path.exists, exists_path)
179+
await asyncio.to_thread(exists_path.exists)
180180

181181
async def bench_export_attachments_existing() -> None:
182182
if zip_path.exists():

tests/test_backup.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,34 @@ async def test_export_attachments_exports_existing_and_skips_missing(
275275
assert "files/attachments/att_ok.txt" in namelist
276276
assert "files/attachments/att_missing.txt" not in namelist
277277

278+
@pytest.mark.asyncio
279+
async def test_export_attachments_skips_empty_attachment_id(
280+
self, mock_main_db, tmp_path
281+
):
282+
"""测试附件导出:attachment_id 为空时跳过,避免覆盖冲突。"""
283+
exporter = AstrBotExporter(main_db=mock_main_db, kb_manager=None)
284+
285+
file_a = tmp_path / "a.txt"
286+
file_b = tmp_path / "b.txt"
287+
file_a.write_text("a", encoding="utf-8")
288+
file_b.write_text("b", encoding="utf-8")
289+
zip_path = tmp_path / "attachments_empty_id.zip"
290+
291+
attachments = [
292+
{"attachment_id": "", "path": str(file_a)},
293+
{"path": str(file_b)},
294+
{"attachment_id": "att_ok", "path": str(file_a)},
295+
]
296+
297+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
298+
await exporter._export_attachments(zf, attachments)
299+
300+
with zipfile.ZipFile(zip_path, "r") as zf:
301+
namelist = zf.namelist()
302+
303+
assert "files/attachments/att_ok.txt" in namelist
304+
assert "files/attachments/.txt" not in namelist
305+
278306

279307
class TestAstrBotImporter:
280308
"""AstrBotImporter 类测试"""

0 commit comments

Comments
 (0)