Files
ewoooc/scripts/tools/backup_system.py
ogt 3b14368d4e
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
fix: harden alerts and backup deployment guard
2026-06-26 17:52:06 +08:00

180 lines
4.8 KiB
Python

import datetime
import os
import re
import zipfile
from pathlib import Path
EXCLUDED_DIRS = {
".claude",
".codex",
".coverage",
".cache",
".git",
".idea",
".mypy_cache",
".next",
".pytest_cache",
".ruff_cache",
".venv",
".vscode",
"__pycache__",
"backups",
"bin",
"bin 2",
"node_modules",
"runtime",
"volumes",
"build",
"dist",
"htmlcov",
"logs",
"playwright-report",
"screenshots",
"temp",
"tmp",
"uploads",
}
EXCLUDED_PATH_PREFIXES = {
("docs", "design"),
("export_assets",),
("frontend", ".next"),
("MOMO Pro",),
}
EXCLUDED_FILES = {
".DS_Store",
".env",
"google_credentials.json",
"google_token.json",
"google_token.pickle",
}
EXCLUDED_SUFFIXES = (".pyc", ".pyo", ".sqlite", ".sqlite3", ".tsbuildinfo")
def _resolve_project_root() -> Path:
configured = os.getenv("MOMO_BACKUP_ROOT")
if configured:
return Path(configured).expanduser().resolve()
return Path(__file__).resolve().parents[2]
def _resolve_backup_folder(project_root: Path) -> Path:
configured = os.getenv("MOMO_BACKUP_DIR")
if configured:
return Path(configured).expanduser().resolve()
return project_root / "backups"
def _read_system_version(project_root: Path) -> str:
version = "Unknown"
config_path = project_root / "config.py"
try:
if config_path.exists():
content = config_path.read_text(encoding="utf-8")
match = re.search(r'SYSTEM_VERSION\s*=\s*["\']([^"\']+)["\']', content)
if match:
version = match.group(1)
except Exception as e:
print(f"⚠️ 無法讀取版本號: {e}")
return version
def _should_skip_file(file_path: Path, project_root: Path, backup_folder: Path) -> bool:
if file_path.is_symlink():
return True
if file_path.name in EXCLUDED_FILES:
return True
if file_path.name.startswith(".env."):
return True
if file_path.suffix in EXCLUDED_SUFFIXES:
return True
try:
file_path.resolve().relative_to(backup_folder)
return True
except ValueError:
pass
rel_parts = file_path.relative_to(project_root).parts
if _is_excluded_path(rel_parts):
return True
return any(_is_excluded_dir_name(part) for part in rel_parts[:-1])
def _is_excluded_dir_name(dirname: str) -> bool:
return dirname in EXCLUDED_DIRS or dirname.startswith("production_v")
def _is_excluded_path(rel_parts: tuple[str, ...]) -> bool:
for prefix in EXCLUDED_PATH_PREFIXES:
if rel_parts[:len(prefix)] == prefix:
return True
return False
def _prune_dirs(dirs: list[str], root: Path, project_root: Path, backup_folder: Path) -> None:
kept = []
for dirname in dirs:
candidate = root / dirname
try:
rel_parts = candidate.relative_to(project_root).parts
except ValueError:
rel_parts = ()
if rel_parts and _is_excluded_path(rel_parts):
continue
if _is_excluded_dir_name(dirname):
continue
if candidate.is_symlink():
continue
try:
candidate.resolve().relative_to(backup_folder)
continue
except ValueError:
pass
kept.append(dirname)
dirs[:] = kept
def create_backup():
"""
建立系統完整備份 (Zip 壓縮檔)
檔名格式: momo_pro_system_backup_YYYYMMDD_HHMMSS_V{version}.zip
"""
project_root = _resolve_project_root()
backup_folder = _resolve_backup_folder(project_root)
if not backup_folder.exists():
backup_folder.mkdir(parents=True)
print(f"📂 已建立備份目錄: {backup_folder}")
version = _read_system_version(project_root)
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
base_name = f"momo_pro_system_backup_{timestamp}_{version}.zip"
output_path = backup_folder / base_name
print(f"📦 正在打包專案目錄: {project_root}")
print(f"🎯 目標檔案: {output_path}")
try:
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(project_root):
root_path = Path(root)
_prune_dirs(dirs, root_path, project_root, backup_folder)
for file in files:
file_path = root_path / file
if _should_skip_file(file_path, project_root, backup_folder):
continue
arcname = file_path.relative_to(project_root)
zipf.write(file_path, arcname)
print(f"✅ 備份完成!")
return str(output_path)
except Exception as e:
print(f"❌ 備份失敗: {e}")
return None
if __name__ == "__main__":
create_backup()