feat: 支持历史原始数据下载

This commit is contained in:
2026-05-19 16:45:10 +08:00
parent b25847dbb8
commit 332ed7055d
4 changed files with 159 additions and 5 deletions
+69
View File
@@ -1,14 +1,60 @@
import zipfile
from contextlib import suppress
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, Body, HTTPException
from fastapi.responses import FileResponse
from starlette.background import BackgroundTask
from app import state
from app.config import CACHE_DIR
from app.utils.files import format_size, get_dir_size
router = APIRouter(tags=["history"])
def _remove_file(path: Path) -> None:
with suppress(OSError):
path.unlink()
def _safe_filename_part(value: str) -> str:
safe = "".join(char if char.isalnum() or char in {"-", "_"} else "_" for char in value)
return safe.strip("_") or "history"
def _get_safe_work_dir(record_id: str) -> tuple[Path, str]:
record = state.history_manager.get(record_id)
if not record:
raise HTTPException(status_code=404, detail="记录不存在")
if record.status in {"pending", "processing"}:
raise HTTPException(status_code=409, detail="任务尚未完成,暂不能下载历史数据")
work_dir = Path(record.work_dir).resolve()
cache_dir = CACHE_DIR.resolve()
try:
work_dir.relative_to(cache_dir)
except ValueError as exc:
raise HTTPException(status_code=400, detail="历史目录不合法") from exc
if not work_dir.exists() or not work_dir.is_dir():
raise HTTPException(status_code=404, detail="历史数据目录不存在")
return work_dir, record.id
def _zip_directory(source_dir: Path, archive_path: Path) -> None:
with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED, allowZip64=True) as archive:
for item in source_dir.rglob("*"):
arcname = item.relative_to(source_dir).as_posix()
if item.is_dir():
archive.writestr(f"{arcname}/", "")
elif item.is_file():
archive.write(item, arcname)
@router.post("/api/history")
async def get_history(limit: int = Body(50, embed=True)):
return {"records": state.history_manager.list(limit)}
@@ -51,3 +97,26 @@ async def get_history_size(record_id: str = Body(..., embed=True)):
size = get_dir_size(work_dir)
return {"success": True, "size": size, "size_formatted": format_size(size)}
@router.post("/api/history/download")
async def download_history(record_id: str = Body(..., embed=True)):
work_dir, safe_record_id = _get_safe_work_dir(record_id)
export_dir = CACHE_DIR / ".downloads"
export_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{_safe_filename_part(safe_record_id)}_{timestamp}.zip"
archive_path = export_dir / filename
try:
_zip_directory(work_dir, archive_path)
except Exception:
_remove_file(archive_path)
raise
return FileResponse(
path=str(archive_path),
filename=filename,
media_type="application/zip",
background=BackgroundTask(_remove_file, archive_path),
)