#!/usr/bin/env python3 # -*- coding: utf-8 -*- """打包 WorkBuddy 关键资产并上传到百度网盘 AIinfra_Cursor。 流程: 1) 打包本地 +(可选)拉取 207 平台快照 2) bypy 上传到「我的应用数据/bypy/AIinfra_Cursor/<时间戳>/」 3) 再移动到网盘根目录「/AIinfra_Cursor/<时间戳>/」(真正的 AIinfra_Cursor) 用法(Windows,项目根目录): python scripts_cursor/百度网盘备份/backup_01_打包并上传.py python scripts_cursor/百度网盘备份/backup_01_打包并上传.py --dry-run python scripts_cursor/百度网盘备份/backup_01_打包并上传.py --pack-only python scripts_cursor/百度网盘备份/backup_01_打包并上传.py --no-move python scripts_cursor/百度网盘备份/backup_01_打包并上传.py --incremental python scripts_cursor/百度网盘备份/backup_01_打包并上传.py --mode tree 207 端 /opt/workbuddy 独立备份(bundle 带 207_ 前缀): python scripts_cursor/百度网盘备份/backup_02_207端打包并上传.py 环境变量: BAIDUPAN_FOLDER bypy staging 目录名(默认 AIinfra_Cursor) BAIDUPAN_FINAL_ROOT 终盘目录(默认 AIinfra_Cursor,位于网盘根) BAIDUPAN_MOVE_AFTER_UPLOAD 上传后是否自动移动(默认 1) BYPY_UPLOAD_TIMEOUT / BYPY_UPLOAD_RETRIES 前置:pip install bypy && python -m bypy info(完成 OAuth 授权) """ from __future__ import annotations import argparse import json import sys from pathlib import Path import _bootstrap # noqa: F401 import backup_settings as S from backup_bypy import ( check_bypy_auth, ensure_remote_dir, move_bundle_to_final, upload_directory, upload_local_file, ) from backup_collect import build_backup_bundle from backup_progress import write_progress def _load_last_fingerprint() -> str | None: if not S.LAST_MANIFEST_FILE.is_file(): return None try: j = json.loads(S.LAST_MANIFEST_FILE.read_text(encoding="utf-8")) return j.get("zip_sha256") except Exception: return None def _save_last_manifest(meta: dict) -> None: S.STATE_DIR.mkdir(parents=True, exist_ok=True) S.LAST_MANIFEST_FILE.write_text( json.dumps( { "bundle_id": meta["bundle_id"], "created_at": meta["created_at"], "zip_sha256": meta["zip_sha256"], "staging_prefix": f"{S.BAIDUPAN_FOLDER}/{meta['bundle_id']}", "final_path": meta.get("final_path") or f"/{S.BAIDUPAN_FINAL_ROOT}/{meta['bundle_id']}", }, ensure_ascii=False, indent=2, ), encoding="utf-8", ) def upload_bundle(meta: dict, *, mode: str) -> bool: bundle_id = meta["bundle_id"] remote_prefix = f"{S.BAIDUPAN_FOLDER}/{bundle_id}" work = Path(meta["zip_path"]).parent guide = work / "备份说明.html" if mode == "zip": items = [ (Path(meta["zip_path"]), f"{remote_prefix}/{Path(meta['zip_path']).name}"), (work / "manifest.json", f"{remote_prefix}/manifest.json"), (work / "README_backup.txt", f"{remote_prefix}/README_backup.txt"), ] if guide.is_file(): items.insert(0, (guide, f"{remote_prefix}/备份说明.html")) else: tree = work / "tree" if not tree.is_dir(): print("[FAIL] tree 模式需要 staging/tree 目录") return False print(f"[INFO] tree 模式上传目录 → staging {remote_prefix}/tree/") write_progress(status="上传中", detail=f"tree → {remote_prefix}", uploaded=0, total=1) ok = upload_directory(tree, f"{remote_prefix}/tree") for extra in ("manifest.json", "README_backup.txt", "备份说明.html"): fp = work / extra if fp.is_file(): upload_local_file(fp, f"{remote_prefix}/{extra}") write_progress( status="上传完成" if ok else "部分失败", detail=remote_prefix, uploaded=1 if ok else 0, total=1, ) return ok total = len(items) ok_all = True write_progress(status="上传中(staging)", detail=remote_prefix, uploaded=0, total=total) for idx, (local, remote) in enumerate(items, 1): print(f"[{idx}/{total}] {local.name} → 我的应用数据/bypy/{remote}") if not upload_local_file(local, remote): ok_all = False write_progress(status="上传中(staging)", detail=remote, uploaded=idx, total=total) write_progress( status="staging 完成" if ok_all else "部分失败", detail=remote_prefix, uploaded=total if ok_all else 0, total=total, ) return ok_all def main() -> int: if sys.platform == "win32": try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass ap = argparse.ArgumentParser(description="WorkBuddy → 百度网盘 AIinfra_Cursor 备份") ap.add_argument("--dry-run", action="store_true", help="仅打包并打印 manifest,不上传") ap.add_argument("--pack-only", action="store_true", help="仅本地打包,不上传") ap.add_argument("--incremental", action="store_true", help="zip 内容指纹与上次相同则跳过上传") ap.add_argument( "--mode", choices=("zip", "tree"), default="zip", help="zip=上传压缩包+manifest+备份说明.html(默认);tree=逐文件上传目录树", ) ap.add_argument("--no-remote", action="store_true", help="不拉取 207 平台配置快照") ap.add_argument( "--no-move", action="store_true", help="上传后不移动到终盘 /AIinfra_Cursor(留在我的应用数据/bypy)", ) args = ap.parse_args() do_move = S.BAIDUPAN_MOVE_AFTER_UPLOAD and not args.no_move print(f"[INFO] 项目根: {S.ROOT}") print(f"[INFO] staging: 我的应用数据/bypy/{S.BAIDUPAN_FOLDER}/<时间戳>/") print(f"[INFO] 终盘: /{S.BAIDUPAN_FINAL_ROOT}/<时间戳>/ (移动={'开' if do_move else '关'})") write_progress(status="打包中", detail="collect + zip + html") meta = build_backup_bundle(pull_remote=not args.no_remote) print(f"[INFO] bundle: {meta['bundle_id']}") print(f"[INFO] 文件数: {meta['file_count']},原始 {meta['total_bytes']} bytes") print(f"[INFO] zip: {meta['zip_path']} ({meta['zip_bytes']} bytes)") print(f"[INFO] sha256: {meta['zip_sha256']}") print(f"[INFO] 说明: {meta.get('guide_html_path', '')}") if meta.get("categories"): print("[INFO] 分类:", ", ".join(f"{k}={v}" for k, v in sorted(meta["categories"].items()))) if args.dry_run or args.pack_only: print("[INFO] 跳过上传(dry-run / pack-only)") write_progress(status="仅打包", detail=str(meta["zip_path"])) return 0 if args.incremental and _load_last_fingerprint() == meta["zip_sha256"]: print("[INFO] 增量模式:内容与上次相同,跳过上传") write_progress(status="跳过(无变化)", detail=meta["bundle_id"]) return 0 if not check_bypy_auth(): return 1 ensure_remote_dir(S.BAIDUPAN_FOLDER) ok = upload_bundle(meta, mode=args.mode) if not ok: print("[FAIL] 上传未全部成功,可重跑本脚本(会生成新 bundle_id)") return 2 final_path = f"/{S.BAIDUPAN_FINAL_ROOT}/{meta['bundle_id']}" if do_move: print( f"[INFO] 开始移动:我的应用数据/bypy/{S.BAIDUPAN_FOLDER}/{meta['bundle_id']}/ " f"→ /{S.BAIDUPAN_FINAL_ROOT}/{meta['bundle_id']}/" ) write_progress(status="移动到终盘", detail=final_path) move_ok, moved_to = move_bundle_to_final(meta["bundle_id"]) if not move_ok: print("[FAIL] staging 已上传成功,但移动到终盘失败;文件仍在「我的应用数据/bypy」下") write_progress(status="移动失败(staging 已有)", detail=meta["bundle_id"]) return 3 final_path = moved_to or final_path meta["final_path"] = final_path write_progress(status="已完成", detail=final_path, uploaded=1, total=1) _save_last_manifest(meta) print(f"[PASS] 已落到终盘: {final_path}") return 0 meta["final_path"] = f"/apps/bypy/{S.BAIDUPAN_FOLDER}/{meta['bundle_id']}" _save_last_manifest(meta) print(f"[PASS] 已上传到 staging(未移动): 我的应用数据/bypy/{S.BAIDUPAN_FOLDER}/{meta['bundle_id']}/") return 0 if __name__ == "__main__": sys.exit(main())