180 lines
4.8 KiB
Python
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()
|