export_db_snapshot.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. # -*- coding: utf-8 -*-
  2. """
  3. 将当前 MySQL 演示数据与静态资源导出到仓库,便于他人 clone 后复现相同界面效果。
  4. 生成物:
  5. sql/seed_snapshot.sql — user / model / media / detection / operation 全量数据
  6. seed_assets/snapshot/static/ — medias、models、results、avatars
  7. seed_assets/snapshot/training_meta/ — 训练任务与数据集元数据 JSON
  8. seed_assets/snapshot/manifest.json — 导出摘要
  9. 用法(项目根或 BridgeDiseaseBackend-main 下,需 MySQL 3307 可连):
  10. python scripts/export_db_snapshot.py
  11. """
  12. from __future__ import annotations
  13. import json
  14. import os
  15. import shutil
  16. import subprocess
  17. import sys
  18. from datetime import datetime, timezone
  19. from pathlib import Path
  20. ROOT = Path(__file__).resolve().parents[1]
  21. SQL_OUT = ROOT / 'sql' / 'seed_snapshot.sql'
  22. SNAPSHOT_ROOT = ROOT / 'seed_assets' / 'snapshot'
  23. STATIC_SRC = ROOT / 'app' / 'static'
  24. STATIC_DST = SNAPSHOT_ROOT / 'static'
  25. TRAINING_SRC = ROOT / 'data' / 'training_meta'
  26. TRAINING_DST = SNAPSHOT_ROOT / 'training_meta'
  27. TABLES = ('user', 'model', 'media', 'detection', 'operation')
  28. STATIC_DIRS = ('medias', 'models', 'results', 'avatars')
  29. def _db_env():
  30. uri = os.environ.get(
  31. 'SQLALCHEMY_DATABASE_URI',
  32. 'mysql+pymysql://root:bridgedisease_root@127.0.0.1:3307/bridge_disease?charset=utf8mb4',
  33. )
  34. # mysql+pymysql://user:pass@host:port/db?...
  35. body = uri.split('://', 1)[-1]
  36. auth, rest = body.split('@', 1)
  37. user, password = auth.split(':', 1)
  38. host_port, db_part = rest.split('/', 1)
  39. database = db_part.split('?', 1)[0]
  40. host, port = (host_port.split(':', 1) + ['3306'])[:2]
  41. return {
  42. 'host': host,
  43. 'port': port,
  44. 'user': user,
  45. 'password': password,
  46. 'database': database,
  47. }
  48. def dump_mysql(cfg: dict) -> None:
  49. SQL_OUT.parent.mkdir(parents=True, exist_ok=True)
  50. header = (
  51. '-- 检澜 DockScope 演示数据快照(由 scripts/export_db_snapshot.py 生成)\n'
  52. f"-- 导出时间 UTC: {datetime.now(timezone.utc).isoformat()}\n"
  53. 'SET NAMES utf8mb4;\n'
  54. 'SET FOREIGN_KEY_CHECKS=0;\n'
  55. 'SET sql_mode = \'NO_AUTO_VALUE_ON_ZERO\';\n\n'
  56. )
  57. cmd = [
  58. 'mysqldump',
  59. f"--host={cfg['host']}",
  60. f"--port={cfg['port']}",
  61. f"-u{cfg['user']}",
  62. f"-p{cfg['password']}",
  63. '--default-character-set=utf8mb4',
  64. '--no-create-info',
  65. '--skip-triggers',
  66. '--complete-insert',
  67. '--hex-blob',
  68. cfg['database'],
  69. *TABLES,
  70. ]
  71. try:
  72. proc = subprocess.run(cmd, capture_output=True, check=True)
  73. except FileNotFoundError:
  74. # 尝试 Docker 中的 mysql 客户端
  75. docker_cmd = [
  76. 'docker',
  77. 'exec',
  78. 'bridge-disease-mysql',
  79. 'mysqldump',
  80. f"-u{cfg['user']}",
  81. f"-p{cfg['password']}",
  82. '--default-character-set=utf8mb4',
  83. '--no-create-info',
  84. '--skip-triggers',
  85. '--complete-insert',
  86. '--hex-blob',
  87. cfg['database'],
  88. *TABLES,
  89. ]
  90. proc = subprocess.run(docker_cmd, capture_output=True, check=True)
  91. body = proc.stdout.decode('utf-8', errors='replace')
  92. footer = '\nSET FOREIGN_KEY_CHECKS=1;\n'
  93. SQL_OUT.write_text(header + body + footer, encoding='utf-8')
  94. print(f'[ok] SQL -> {SQL_OUT} ({SQL_OUT.stat().st_size // 1024} KB)')
  95. def copy_tree(src: Path, dst: Path) -> int:
  96. if not src.is_dir():
  97. return 0
  98. if dst.exists():
  99. shutil.rmtree(dst)
  100. shutil.copytree(src, dst)
  101. count = sum(1 for _ in dst.rglob('*') if _.is_file())
  102. print(f'[ok] {src.name} -> {dst} ({count} files)')
  103. return count
  104. def main() -> int:
  105. cfg = _db_env()
  106. print(f"Export from {cfg['user']}@{cfg['host']}:{cfg['port']}/{cfg['database']}")
  107. dump_mysql(cfg)
  108. STATIC_DST.parent.mkdir(parents=True, exist_ok=True)
  109. file_counts = {}
  110. for name in STATIC_DIRS:
  111. src = STATIC_SRC / name
  112. if src.is_dir():
  113. file_counts[name] = copy_tree(src, STATIC_DST / name)
  114. # 用户头像(SQL 引用 static/avatars/1~3.jpg)
  115. avatars_dst = STATIC_DST / 'avatars'
  116. avatars_dst.mkdir(parents=True, exist_ok=True)
  117. media_pool = list((STATIC_SRC / 'medias').glob('*.jpg')) or list(
  118. (ROOT / 'seed_assets' / 'medias').glob('*.jpg')
  119. )
  120. for i in range(1, 4):
  121. dst = avatars_dst / f'{i}.jpg'
  122. if not dst.is_file() and media_pool:
  123. shutil.copy2(media_pool[(i - 1) % len(media_pool)], dst)
  124. if avatars_dst.is_dir():
  125. file_counts['avatars'] = sum(1 for _ in avatars_dst.glob('*.jpg'))
  126. print(f'[ok] avatars -> {avatars_dst} ({file_counts["avatars"]} files)')
  127. training_files = 0
  128. if TRAINING_SRC.is_dir():
  129. TRAINING_DST.parent.mkdir(parents=True, exist_ok=True)
  130. if TRAINING_DST.exists():
  131. shutil.rmtree(TRAINING_DST)
  132. shutil.copytree(TRAINING_SRC, TRAINING_DST)
  133. training_files = sum(1 for _ in TRAINING_DST.rglob('*') if _.is_file())
  134. print(f'[ok] training_meta -> {TRAINING_DST} ({training_files} files)')
  135. manifest = {
  136. 'exported_at': datetime.now(timezone.utc).isoformat(),
  137. 'database': cfg['database'],
  138. 'tables': list(TABLES),
  139. 'static_files': file_counts,
  140. 'training_meta_files': training_files,
  141. 'sql_file': 'sql/seed_snapshot.sql',
  142. }
  143. SNAPSHOT_ROOT.mkdir(parents=True, exist_ok=True)
  144. manifest_path = SNAPSHOT_ROOT / 'manifest.json'
  145. manifest_path.write_text(
  146. json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8'
  147. )
  148. print(f'[ok] manifest -> {manifest_path}')
  149. print('\n请提交 sql/seed_snapshot.sql 与 seed_assets/snapshot/ 到 Git,供他人运行 import_db_snapshot.py')
  150. return 0
  151. if __name__ == '__main__':
  152. sys.exit(main())