products_state.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. r"""产物开关 —— 把 outputs\<场>\ 下的产物**直接清掉** / 看当前状态 (人工检查页面空状态用)。
  4. ## 2026-09-16 用户令: 清除产物**不留备份**
  5. 原设计是"移动到 `_products_off/<场>/` + 清单留痕, 一条命令还原"。用户令改为**不备份**:
  6. 清掉就是清掉。于是本脚本:
  7. · `--off --yes` 真删除 (rmtree), 打印删了什么; **不再产生 `_products_off*` 目录**;
  8. · `--status` 看产物在位/已清 (清掉后就是"已清", 没有备份清单可列);
  9. · `--on` 已**移除**: 没有备份就没有"挪回"。恢复缺失的**随包件**按用户令 2026-09-17
  10. **不由交付包补齐**: 放原始件到 `data/raw/<场站>/` 后 `python scripts/rebuild_all.py`;
  11. 没有生成端的族由研发补生成端 (逐族可逆性见 `docs/系统设计说明.md` §13.6)。
  12. (离线人工补救的口子仍在 `scripts/products_restore_missing.py --stash <…>`,
  13. 但它**不在运行链里**, 也不该被运行期依赖。)
  14. ## 保留不动的
  15. - `data\raw\` 现场数据 (检查的是"产物没了会怎样", 不是"数据没了会怎样")
  16. - `release\` 门户与三维/交付件 (那是外壳, 不是数据产物)
  17. - `logs\` `run\` 运行期文件
  18. - ★ `outputs\<场>\windscada\_pre_rebuild_20260911\` **不再特殊保留**: 它曾是"随包基线备份"而在清除时豁免,
  19. 现在按"不留备份"一并删掉 —— 等价验收因此暂时没有标准答案(如实报 rc=5, 不假装通过);
  20. 要恢复验收能力, 就用**当前在位且已核验的产物**重新落一份基线(见 docs §7 那条)。
  21. 用法:
  22. python scripts/products_state.py --status # 看当前产物状态
  23. python scripts/products_state.py --off --yes # 清掉全部产物 (真删除, 不可恢复; --yes 是防手滑)
  24. """
  25. from __future__ import annotations
  26. import argparse
  27. import pathlib
  28. import shutil
  29. import sys
  30. ROOT = pathlib.Path(__file__).resolve().parents[1]
  31. sys.path.insert(0, str(ROOT))
  32. from src import paths as P # noqa: E402
  33. LEGACY_OFF = ROOT / '_products_off' # 旧版暂存区 (只用于提示, 不再写入)
  34. def farms() -> list[pathlib.Path]:
  35. out = ROOT / 'outputs'
  36. return sorted(p for p in out.iterdir() if p.is_dir()) if out.is_dir() else []
  37. def plan() -> list[pathlib.Path]:
  38. """→ 待清除的产物目录列表 (产物仓本身)。"""
  39. return [d for fam in farms() for d in sorted(fam.iterdir())]
  40. def count(p: pathlib.Path) -> tuple[int, float]:
  41. if p.is_file():
  42. return 1, p.stat().st_size / 1024 / 1024
  43. fs = [f for f in p.rglob('*') if f.is_file()]
  44. return len(fs), sum(f.stat().st_size for f in fs) / 1024 / 1024
  45. def do_off(assume_yes=False) -> int:
  46. items = [p for p in plan() if p.exists()]
  47. if not items:
  48. print('产物已经是空的 (用 --status 看)')
  49. return 0
  50. total_n = total_mb = 0
  51. rows = []
  52. for src in items:
  53. n, mb = count(src)
  54. rows.append((P.rel(src), n, mb))
  55. total_n += n
  56. total_mb += mb
  57. print('即将**删除**以下产物 (按用户令: 不留备份, 删除后不可恢复):')
  58. for rel, n, mb in rows:
  59. print(f' {rel:34s} {n:5d} 件 {mb:8.1f} MB')
  60. print(f' 合计 {len(rows)} 项 / {total_n} 件 / {total_mb:.1f} MB')
  61. print(' 恢复办法: 放原始件到 data/raw/<场站>/ 后 python scripts/rebuild_all.py; 缺生成端的族由研发补 (docs §13.6)')
  62. if not assume_yes:
  63. print('\n[X] 未执行: 这是不可恢复操作, 请显式加 --yes (运维控制台里点按钮时已由前端确认过一次)')
  64. return 2
  65. for src in items:
  66. shutil.rmtree(src, ignore_errors=True) if src.is_dir() else src.unlink(missing_ok=True)
  67. if LEGACY_OFF.exists():
  68. print(f' [!] 检测到旧版暂存区 {P.rel(LEGACY_OFF)} —— 按"不留备份"的口径, 本次**未删除**它; '
  69. f'确认不需要后请手工删掉 (它是旧设计留下的产物备份)')
  70. print(f'\n已删除 {len(rows)} 项 / {total_n} 件 / {total_mb:.1f} MB (无备份)')
  71. return 0
  72. def do_status() -> int:
  73. items = [p for p in plan() if p.exists()]
  74. if not items:
  75. print('当前: 产物**已清空** (页面应呈空状态)')
  76. print(' 恢复: python scripts/rebuild_all.py (缺生成端的族由研发补, 见 docs §13.6)')
  77. return 0
  78. print(f'当前: 产物**在位** ({len(items)} 项)')
  79. for src in items:
  80. n, mb = count(src)
  81. print(f' {P.rel(src):34s} {n:5d} 件 {mb:8.1f} MB')
  82. print(' 说明: 按 2026-09-16 用户令, 清除产物不留备份 (没有 _products_off/ 可还原)')
  83. return 0
  84. if __name__ == '__main__':
  85. ap = argparse.ArgumentParser()
  86. g = ap.add_mutually_exclusive_group(required=True)
  87. g.add_argument('--off', action='store_true', help='清掉全部产物 (**直接删除, 不留备份**)')
  88. g.add_argument('--status', action='store_true', help='看当前状态')
  89. g.add_argument('--on', action='store_true', help='[已移除] 旧版的"挪回" —— 见文件头说明')
  90. ap.add_argument('--yes', action='store_true', help='确认执行不可恢复的清除 (--off 必带)')
  91. a = ap.parse_args()
  92. if a.on:
  93. print('[X] --on 已移除: 按用户令"清除产物不留备份", 没有备份可挪回。\n'
  94. ' 运行期恢复: 放原始件后 python scripts/rebuild_all.py (缺生成端的族由研发补, docs §13.6)\n'
  95. ' 要重新算出数据面产物: python scripts/rebuild_all.py (SCADA 侧加 --scada)')
  96. sys.exit(3)
  97. sys.exit(do_off(assume_yes=a.yes) if a.off else do_status())