from __future__ import annotations import json import logging import shutil from datetime import datetime from pathlib import Path from app.config import Config from app.services.host import host_path, run_on_host logger = logging.getLogger(__name__) BACKUP_TARGETS = { "resolved.conf": "etc/systemd/resolved.conf", "resolved.conf.d": "etc/systemd/resolved.conf.d", "sources.list": "etc/apt/sources.list", "sources.list.d": "etc/apt/sources.list.d", "daemon.json": "etc/docker/daemon.json", "gitconfig_system": "etc/gitconfig", } def _backup_dir(name: str) -> Path: path = Config.BACKUP_DIR / name path.mkdir(parents=True, exist_ok=True) return path def _copy_if_exists(src: Path, dst: Path) -> None: if not src.exists(): return if src.is_dir(): if dst.exists(): shutil.rmtree(dst) shutil.copytree(src, dst) else: dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst) def _restore_file(relative: str, backup_root: Path) -> None: key = relative.replace("/", "_").replace(".", "_") for name, rel in BACKUP_TARGETS.items(): if rel == relative or rel.endswith(relative): key = name break src = backup_root / key dst = host_path(relative) if not src.exists(): if dst.exists() and key in ("daemon.json", "gitconfig_system"): dst.unlink(missing_ok=True) return if src.is_dir(): if dst.exists(): shutil.rmtree(dst) shutil.copytree(src, dst) else: dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst) def create_backup(label: str = "manual") -> Path: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_root = _backup_dir(f"{timestamp}_{label}") manifest: dict = {"label": label, "timestamp": timestamp, "files": {}} for key, relative in BACKUP_TARGETS.items(): src = host_path(relative) dst = backup_root / key _copy_if_exists(src, dst) manifest["files"][key] = { "relative": relative, "existed": src.exists(), } gitconfig_global = host_path("root/.gitconfig") if not gitconfig_global.exists(): gitconfig_global = Path.home() / ".gitconfig" if gitconfig_global.exists(): shutil.copy2(gitconfig_global, backup_root / "gitconfig_global") manifest["files"]["gitconfig_global"] = {"path": str(gitconfig_global), "existed": True} pip_conf = host_path("etc/pip.conf") if pip_conf.exists(): shutil.copy2(pip_conf, backup_root / "pip.conf") manifest["files"]["pip.conf"] = {"relative": "etc/pip.conf", "existed": True} npmrc = host_path("root/.npmrc") if npmrc.exists(): shutil.copy2(npmrc, backup_root / "npmrc") manifest["files"]["npmrc"] = {"relative": "root/.npmrc", "existed": True} mirror_dropin = host_path("etc/systemd/resolved.conf.d/mirror-manager.conf") if mirror_dropin.exists(): shutil.copy2(mirror_dropin, backup_root / "mirror_manager_resolved.conf") with open(backup_root / "manifest.json", "w", encoding="utf-8") as f: json.dump(manifest, f, ensure_ascii=False, indent=2) logger.info("Backup created at %s", backup_root) return backup_root def restore_backup(backup_root: Path | str) -> dict: backup_root = Path(backup_root) if not backup_root.exists(): return {"success": False, "error": "مسیر backup یافت نشد"} results: list[str] = [] for key, relative in BACKUP_TARGETS.items(): src = backup_root / key if src.exists(): dst = host_path(relative) _copy_if_exists(src, dst) results.append(f"بازگردانی {relative}") gitconfig_backup = backup_root / "gitconfig_global" if gitconfig_backup.exists(): for target in [host_path("root/.gitconfig"), host_path("home/.gitconfig")]: target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(gitconfig_backup, target) results.append("بازگردانی gitconfig global") pip_backup = backup_root / "pip.conf" if pip_backup.exists(): dst = host_path("etc/pip.conf") dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(pip_backup, dst) results.append("بازگردانی pip.conf") npmrc_backup = backup_root / "npmrc" if npmrc_backup.exists(): dst = host_path("root/.npmrc") dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(npmrc_backup, dst) results.append("بازگردانی .npmrc") dropin = host_path("etc/systemd/resolved.conf.d/mirror-manager.conf") mirror_backup = backup_root / "mirror_manager_resolved.conf" if mirror_backup.exists(): dropin.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(mirror_backup, dropin) elif dropin.exists(): dropin.unlink() run_on_host(["systemctl", "restart", "systemd-resolved"]) run_on_host(["systemctl", "restart", "docker"]) return {"success": True, "restored": results} def create_initial_backup() -> Path: return create_backup("initial") def list_backups() -> list[dict]: backups = [] if not Config.BACKUP_DIR.exists(): return backups for path in sorted(Config.BACKUP_DIR.iterdir(), reverse=True): if path.is_dir(): manifest_path = path / "manifest.json" label = path.name if manifest_path.exists(): with open(manifest_path, encoding="utf-8") as f: manifest = json.load(f) label = manifest.get("label", path.name) backups.append({"path": str(path), "name": path.name, "label": label}) return backups