"""Constantes globales de la aplicación.""" import platform import sys from pathlib import Path IS_WINDOWS = sys.platform == "win32" # Plataforma y arquitectura de ESTE build, en el vocabulario que usa el PANEL para # decidir qué artefacto le corresponde a cada servidor (a24c.cras_releases). # Se reportan en POST /api/restore/instance-config junto con app_version. APP_PLATFORM = "windows" if IS_WINDOWS else "linux" # platform.machine() varía por SO para la misma arquitectura ("AMD64" en Windows, # "x86_64" en Linux); se normaliza a un solo vocabulario. _ARCH_ALIASES = { "x86_64": "x86_64", "amd64": "x86_64", "x86": "x86", "i386": "x86", "i686": "x86", "aarch64": "arm64", "arm64": "arm64", } def _resolve_arch() -> str: raw = (platform.machine() or "").strip().lower() return _ARCH_ALIASES.get(raw, raw or "unknown") APP_ARCH = _resolve_arch() def _resolve_app_dir() -> Path: """Directorio donde vive el ejecutable (persistente).""" if getattr(sys, "frozen", False): return Path(sys.executable).resolve().parent return Path(__file__).resolve().parent.parent def _resolve_bundle_dir() -> Path: """Recursos embebidos en el binario PyInstaller onefile.""" if getattr(sys, "frozen", False): return Path(sys._MEIPASS) return _resolve_app_dir() / "packaging" / "bundled" / APP_PLATFORM APP_DIR = _resolve_app_dir() CONFIG_DIR = APP_DIR / "config" ENV_PATH = CONFIG_DIR / ".env" # Sello con la versión que corrió por última vez en esta instalación. El bootstrap lo # reescribe en cada arranque; el instalador remoto del PANEL lo lee por SFTP para # verificar el despliegue sin depender de stdout (el .exe se compila con console=False). VERSION_PATH = CONFIG_DIR / ".version" # Sello con el hash de packaging/bundled-versions.json del build que desplegó 7zip/odbc. # Si cambia, el bootstrap re-copia esas carpetas (ver _copy_bundled_tree). BUNDLED_STAMP_PATH = CONFIG_DIR / ".bundled_deps" DATA_DIR = CONFIG_DIR / "data" LOGS_DIR = CONFIG_DIR / "logs" ODBC_DIR = CONFIG_DIR / "odbc" SEVEN_ZIP_DIR = CONFIG_DIR / "7zip" BUNDLE_DIR = _resolve_bundle_dir() BUNDLED_SOURCE_7ZIP = BUNDLE_DIR / "bundled" / "7zip" BUNDLED_SOURCE_ODBC = BUNDLE_DIR / "bundled" / "odbc" def _default_7zip_name() -> str: if IS_WINDOWS: return "7z.exe" return "7zz" BUNDLED_7ZIP_EXE = SEVEN_ZIP_DIR / _default_7zip_name() BUNDLED_7ZIP_FALLBACK = SEVEN_ZIP_DIR / ("7za.exe" if IS_WINDOWS else "7zz") # Carpetas de trabajo junto al ejecutable DIR_ENTRADA = APP_DIR / "Entrada" DIR_PROCESADOS = APP_DIR / "Procesados" DIR_FALLADOS = APP_DIR / "Fallados" DIR_TEMP = APP_DIR / "Temp" DB_PATH = DATA_DIR / "app.db" # Estados de jobs class JobStatus: QUEUED = "queued" EXTRACTING = "extracting" RESTORING = "restoring" CLEANING = "cleaning" COMPLETED = "completed" FAILED = "failed" FAILED_RESTART = "failed_restart" CANCELLED = "cancelled" class LogLevel: DEBUG = "DEBUG" INFO = "INFO" WARNING = "WARNING" ERROR = "ERROR" CRITICAL = "CRITICAL" class StepType: STABILITY_CHECK = "stability_check" NODE_MAPPING = "node_mapping" EXTRACT = "extract" LOCATE_BAK = "locate_bak" SQL_CONNECT = "sql_connect" FILELIST = "filelist" RESTORE = "restore" CLEANUP = "cleanup" SFTP_COPY = "sftp_copy" FORWARD_ZIP = "forward_zip" def default_seven_zip_path() -> str: for candidate in (BUNDLED_7ZIP_EXE, BUNDLED_7ZIP_FALLBACK): if candidate.exists(): return str(candidate) return "" def default_work_paths() -> dict[str, str]: return { "input_folder": str(DIR_ENTRADA), "processed_folder": str(DIR_PROCESADOS), "failed_folder": str(DIR_FALLADOS), "extract_folder": str(DIR_TEMP), "data_sql_folder": "", "seven_zip_exe": default_seven_zip_path(), } DEFAULT_CONFIG = { "paths": default_work_paths(), "sql": { "server": "localhost", "use_windows_auth": IS_WINDOWS, "username": "", "password_encrypted": "", }, "concurrency": { "extract_workers": 1, "restore_workers": 1, }, "stability": { "check_enabled": True, "check_interval_seconds": 5, "stable_duration_seconds": 10, "use_ready_marker": False, }, "timeouts": { "extract_minutes": 30, "restore_minutes": 60, }, "retries": { "max_attempts": 3, "retry_delay_seconds": 60, }, "features": { "dry_run_mode": False, "auto_scan_enabled": True, "scan_interval_seconds": 30, }, "panel": { "api_url": "", "api_token": "", "instance_key": "", "verify_ssl": False, }, "retention": { # Limpieza diaria de respaldos obsoletos para no saturar el disco del servidor. "enabled": True, # Procesados/: por nodo, borra los aplicados con finished_at < (ref_del_nodo - days). "days": 2, # Fallados/: por antigüedad absoluta, borra los más viejos que (hoy - failed_days). "failed_days": 7, # Hora local (0-23) a la que corre la limpieza diaria. "run_at_hour": 3, # Cada cuánto despierta el hilo para evaluar si toca correr. "check_interval_seconds": 3600, # Arranca en SECO: solo reporta qué borraría. El operador lo desactiva tras validar. "dry_run": True, }, }