"""封面等 Python 脚本:按相对路径下发源码,供桌面端拉取后在 bundled python.exe 中执行。""" import re from pathlib import Path from fastapi import APIRouter from app.schemas.common import ApiResponse router = APIRouter(prefix="/python-scripts", tags=["python"]) # pythonbackend/scripts/cover/ _SCRIPTS_DIR = Path(__file__).resolve().parents[3] / "scripts" / "cover" _SAFE_NAME = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._/-]*\.py$") def _validate_name(name: str) -> str | None: raw = name.strip().replace("\\", "/") if not raw or not _SAFE_NAME.fullmatch(raw): return None if ".." in raw.split("/"): return None return raw def _resolve_script_path(safe: str) -> Path | None: path = _SCRIPTS_DIR / safe try: base_resolved = _SCRIPTS_DIR.resolve() resolved = path.resolve() resolved.relative_to(base_resolved) except (OSError, ValueError): return None if not path.is_file(): return None return path @router.get("", response_model=ApiResponse[dict]) async def get_python_script(name: str) -> ApiResponse[dict]: safe = _validate_name(name) if not safe: return ApiResponse(ok=False, message="无效的脚本名称", data=None) path = _resolve_script_path(safe) if path is None: return ApiResponse(ok=False, message=f"脚本不存在: {name}", data=None) source = path.read_text(encoding="utf-8") return ApiResponse(ok=True, message="", data={"source": source}) @router.get("/list", response_model=ApiResponse[dict]) async def list_python_scripts() -> ApiResponse[dict]: if not _SCRIPTS_DIR.is_dir(): return ApiResponse(ok=True, message="", data={"names": []}) names: list[str] = [] for p in sorted(_SCRIPTS_DIR.rglob("*.py")): if p.is_file(): rel = p.relative_to(_SCRIPTS_DIR).as_posix() names.append(rel) return ApiResponse(ok=True, message="", data={"names": names})