fix: harden alerts and backup deployment guard
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s
This commit is contained in:
@@ -1,60 +1,179 @@
|
||||
import os
|
||||
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
|
||||
"""
|
||||
# 1. 基礎路徑設定
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
backup_folder = os.path.join(base_dir, 'backups')
|
||||
project_root = _resolve_project_root()
|
||||
backup_folder = _resolve_backup_folder(project_root)
|
||||
|
||||
if not os.path.exists(backup_folder):
|
||||
os.makedirs(backup_folder)
|
||||
if not backup_folder.exists():
|
||||
backup_folder.mkdir(parents=True)
|
||||
print(f"📂 已建立備份目錄: {backup_folder}")
|
||||
|
||||
# 2. 嘗試從 app.py 讀取版本號
|
||||
version = "Unknown"
|
||||
app_py_path = os.path.join(base_dir, 'app.py')
|
||||
try:
|
||||
if os.path.exists(app_py_path):
|
||||
with open(app_py_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
# 尋找 SYSTEM_VERSION = "V9.0"
|
||||
match = re.search(r'SYSTEM_VERSION\s*=\s*["\']([^"\']+)["\']', content)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 無法讀取版本號: {e}")
|
||||
version = _read_system_version(project_root)
|
||||
|
||||
# 3. 產生備份檔名
|
||||
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
base_name = f"momo_pro_system_backup_{timestamp}_{version}.zip"
|
||||
output_path = os.path.join(backup_folder, base_name)
|
||||
output_path = backup_folder / base_name
|
||||
|
||||
print(f"📦 正在打包專案目錄: {base_dir}")
|
||||
print(f"📦 正在打包專案目錄: {project_root}")
|
||||
print(f"🎯 目標檔案: {output_path}")
|
||||
|
||||
# 4. 執行壓縮
|
||||
try:
|
||||
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
# 排除不需要備份的目錄
|
||||
for ignore in ['backups', '__pycache__', '.git', '.idea', '.vscode', 'bin', 'bin 2']:
|
||||
if ignore in dirs:
|
||||
dirs.remove(ignore)
|
||||
|
||||
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:
|
||||
if file == '.DS_Store' or file.endswith('.pyc'): continue
|
||||
file_path = os.path.join(root, file)
|
||||
arcname = os.path.relpath(file_path, base_dir)
|
||||
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()
|
||||
create_backup()
|
||||
|
||||
Reference in New Issue
Block a user