feature/integracion-cpanel-asrecovery
This commit is contained in:
36
app/config/__init__.py
Normal file
36
app/config/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Configuración de entorno, bootstrap y arranque."""
|
||||
|
||||
from .bootstrap import ensure_runtime_layout
|
||||
from .env_loader import (
|
||||
apply_env_overrides,
|
||||
get_launch_options,
|
||||
is_panel_configured,
|
||||
load_env_file,
|
||||
)
|
||||
from .odbc_setup import configure_odbc_environment
|
||||
from .autostart import register_autostart_if_requested
|
||||
|
||||
|
||||
def initialize_runtime() -> None:
|
||||
"""Prepara directorios, ODBC y auto-arranque (llamar antes de importar pyodbc)."""
|
||||
ensure_runtime_layout()
|
||||
configure_odbc_environment()
|
||||
|
||||
|
||||
def initialize_env() -> None:
|
||||
"""Post-bootstrap: carga .env y registra auto-arranque si aplica."""
|
||||
load_env_file()
|
||||
register_autostart_if_requested()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"initialize_runtime",
|
||||
"initialize_env",
|
||||
"ensure_runtime_layout",
|
||||
"load_env_file",
|
||||
"apply_env_overrides",
|
||||
"get_launch_options",
|
||||
"is_panel_configured",
|
||||
"configure_odbc_environment",
|
||||
"register_autostart_if_requested",
|
||||
]
|
||||
88
app/config/autostart.py
Normal file
88
app/config/autostart.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Registro de arranque automático al login (Windows / Linux)."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ..constants import APP_DIR, CONFIG_DIR, IS_WINDOWS
|
||||
from .env_loader import _env_bool, load_env_file
|
||||
|
||||
|
||||
def _marker_path() -> Path:
|
||||
return CONFIG_DIR / ".autostart_registered"
|
||||
|
||||
|
||||
def _executable_command() -> tuple[str, list[str]]:
|
||||
if getattr(sys, "frozen", False):
|
||||
exe = Path(sys.executable).resolve()
|
||||
return str(exe), ["--start-engine"]
|
||||
runner = (APP_DIR / "runner.py").resolve()
|
||||
return sys.executable, [str(runner), "--start-engine"]
|
||||
|
||||
|
||||
def _register_windows(exe: str, args: list[str]) -> None:
|
||||
task_name = "CloudRestoreAS"
|
||||
arg_str = " ".join(f'"{a}"' if " " in a else a for a in args)
|
||||
tr = f'"{exe}" {arg_str}'
|
||||
check = subprocess.run(
|
||||
["schtasks", "/Query", "/TN", task_name],
|
||||
capture_output=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
|
||||
)
|
||||
if check.returncode == 0:
|
||||
return
|
||||
subprocess.run(
|
||||
[
|
||||
"schtasks",
|
||||
"/Create",
|
||||
"/TN",
|
||||
task_name,
|
||||
"/TR",
|
||||
tr,
|
||||
"/SC",
|
||||
"ONLOGON",
|
||||
"/RL",
|
||||
"LIMITED",
|
||||
"/F",
|
||||
],
|
||||
check=False,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
|
||||
)
|
||||
|
||||
|
||||
def _register_linux(exe: str, args: list[str]) -> None:
|
||||
autostart_dir = Path.home() / ".config" / "autostart"
|
||||
autostart_dir.mkdir(parents=True, exist_ok=True)
|
||||
desktop = autostart_dir / "cloudrestoreas.desktop"
|
||||
if desktop.exists():
|
||||
return
|
||||
exec_line = " ".join([exe, *args])
|
||||
content = f"""[Desktop Entry]
|
||||
Type=Application
|
||||
Name=CloudRestoreAS
|
||||
Comment=Restauración automática SQL Server
|
||||
Exec={exec_line}
|
||||
Path={APP_DIR}
|
||||
Terminal=false
|
||||
X-GNOME-Autostart-enabled=true
|
||||
"""
|
||||
desktop.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def register_autostart_if_requested() -> None:
|
||||
load_env_file()
|
||||
if not _env_bool("CLOUDRESTORE_REGISTER_AUTOSTART", True):
|
||||
return
|
||||
if _marker_path().exists():
|
||||
return
|
||||
|
||||
exe, args = _executable_command()
|
||||
try:
|
||||
if IS_WINDOWS:
|
||||
_register_windows(exe, args)
|
||||
else:
|
||||
_register_linux(exe, args)
|
||||
_marker_path().write_text("ok", encoding="utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
83
app/config/bootstrap.py
Normal file
83
app/config/bootstrap.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Creación automática de config/ y carpetas de trabajo."""
|
||||
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ..constants import (
|
||||
APP_DIR,
|
||||
BUNDLE_DIR,
|
||||
BUNDLED_SOURCE_7ZIP,
|
||||
BUNDLED_SOURCE_ODBC,
|
||||
CONFIG_DIR,
|
||||
DATA_DIR,
|
||||
DIR_ENTRADA,
|
||||
DIR_FALLADOS,
|
||||
DIR_PROCESADOS,
|
||||
DIR_TEMP,
|
||||
ENV_PATH,
|
||||
LOGS_DIR,
|
||||
ODBC_DIR,
|
||||
SEVEN_ZIP_DIR,
|
||||
)
|
||||
from ..db.database import DatabaseManager
|
||||
from .env_loader import render_env_template
|
||||
|
||||
|
||||
def _copy_tree_if_missing(src: Path, dest: Path) -> None:
|
||||
if not src.is_dir() or dest.exists():
|
||||
return
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(src, dest)
|
||||
|
||||
|
||||
def _write_env_if_missing() -> None:
|
||||
if ENV_PATH.exists():
|
||||
return
|
||||
template_path = BUNDLE_DIR / "packaging" / "templates" / "env.default"
|
||||
if not template_path.is_file():
|
||||
template_path = APP_DIR / "packaging" / "templates" / "env.default"
|
||||
if template_path.is_file():
|
||||
content = template_path.read_text(encoding="utf-8")
|
||||
content = content.replace("{APP_DIR}", str(APP_DIR))
|
||||
else:
|
||||
content = render_env_template(APP_DIR)
|
||||
ENV_PATH.write_text(content, encoding="utf-8")
|
||||
try:
|
||||
ENV_PATH.chmod(0o600)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def ensure_runtime_layout() -> None:
|
||||
"""
|
||||
Idempotente: crea config/, carpetas de trabajo y .env si no existen.
|
||||
No sobrescribe .env ni app.db existentes.
|
||||
"""
|
||||
for directory in (
|
||||
CONFIG_DIR,
|
||||
DATA_DIR,
|
||||
LOGS_DIR,
|
||||
ODBC_DIR,
|
||||
SEVEN_ZIP_DIR,
|
||||
DIR_ENTRADA,
|
||||
DIR_PROCESADOS,
|
||||
DIR_FALLADOS,
|
||||
DIR_TEMP,
|
||||
):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_write_env_if_missing()
|
||||
|
||||
_copy_tree_if_missing(BUNDLED_SOURCE_7ZIP, SEVEN_ZIP_DIR)
|
||||
_copy_tree_if_missing(BUNDLED_SOURCE_ODBC, ODBC_DIR)
|
||||
|
||||
from ..constants import DB_PATH
|
||||
|
||||
if not DB_PATH.exists():
|
||||
DatabaseManager(DB_PATH)
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
marker = CONFIG_DIR / ".bootstrap_ok"
|
||||
if not marker.exists():
|
||||
marker.write_text("ok", encoding="utf-8")
|
||||
138
app/config/env_loader.py
Normal file
138
app/config/env_loader.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Carga de variables desde config/.env."""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from ..constants import (
|
||||
APP_DIR,
|
||||
DIR_ENTRADA,
|
||||
DIR_FALLADOS,
|
||||
DIR_PROCESADOS,
|
||||
DIR_TEMP,
|
||||
ENV_PATH,
|
||||
default_seven_zip_path,
|
||||
)
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool = False) -> bool:
|
||||
raw = os.getenv(name, "").strip().lower()
|
||||
if not raw:
|
||||
return default
|
||||
return raw in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def load_env_file() -> bool:
|
||||
"""Carga config/.env si existe."""
|
||||
if ENV_PATH.is_file():
|
||||
load_dotenv(ENV_PATH, override=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_panel_configured() -> bool:
|
||||
"""True si URL, token e instancia del panel están definidos."""
|
||||
url = os.getenv("CLOUDRESTORE_PANEL_API_URL", "").strip()
|
||||
token = os.getenv("CLOUDRESTORE_PANEL_API_TOKEN", "").strip()
|
||||
instance = os.getenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", "").strip()
|
||||
return bool(url and token and instance)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LaunchOptions:
|
||||
minimized: bool
|
||||
start_engine: bool
|
||||
register_autostart: bool
|
||||
|
||||
|
||||
def get_launch_options() -> LaunchOptions:
|
||||
load_env_file()
|
||||
return LaunchOptions(
|
||||
minimized=_env_bool("CLOUDRESTORE_START_MINIMIZED", False),
|
||||
start_engine=_env_bool("CLOUDRESTORE_AUTO_START", True),
|
||||
register_autostart=_env_bool("CLOUDRESTORE_REGISTER_AUTOSTART", True),
|
||||
)
|
||||
|
||||
|
||||
def _path_from_env(name: str, default: Path) -> str:
|
||||
value = os.getenv(name, "").strip()
|
||||
return value if value else str(default)
|
||||
|
||||
|
||||
def apply_env_overrides(config: dict) -> dict:
|
||||
"""Aplica variables de entorno sobre la configuración en memoria."""
|
||||
load_env_file()
|
||||
|
||||
paths = config.setdefault("paths", {})
|
||||
paths["input_folder"] = _path_from_env("CLOUDRESTORE_INPUT_FOLDER", DIR_ENTRADA)
|
||||
paths["processed_folder"] = _path_from_env(
|
||||
"CLOUDRESTORE_PROCESSED_FOLDER", DIR_PROCESADOS
|
||||
)
|
||||
paths["failed_folder"] = _path_from_env("CLOUDRESTORE_FAILED_FOLDER", DIR_FALLADOS)
|
||||
paths["extract_folder"] = _path_from_env("CLOUDRESTORE_EXTRACT_FOLDER", DIR_TEMP)
|
||||
data_sql = os.getenv("CLOUDRESTORE_DATA_SQL_FOLDER", "").strip()
|
||||
if data_sql:
|
||||
paths["data_sql_folder"] = data_sql
|
||||
|
||||
seven_zip = os.getenv("CLOUDRESTORE_SEVEN_ZIP_EXE", "").strip()
|
||||
paths["seven_zip_exe"] = seven_zip or default_seven_zip_path()
|
||||
|
||||
panel = config.setdefault("panel", {})
|
||||
api_url = os.getenv("CLOUDRESTORE_PANEL_API_URL", "").strip()
|
||||
if api_url:
|
||||
panel["api_url"] = api_url
|
||||
api_token = os.getenv("CLOUDRESTORE_PANEL_API_TOKEN", "").strip()
|
||||
if api_token:
|
||||
panel["api_token"] = api_token
|
||||
instance = os.getenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", "").strip()
|
||||
if instance:
|
||||
panel["instance_key"] = instance
|
||||
if os.getenv("CLOUDRESTORE_PANEL_VERIFY_SSL"):
|
||||
panel["verify_ssl"] = _env_bool("CLOUDRESTORE_PANEL_VERIFY_SSL", False)
|
||||
|
||||
sql = config.setdefault("sql", {})
|
||||
server = os.getenv("CLOUDRESTORE_SQL_SERVER", "").strip()
|
||||
if server:
|
||||
sql["server"] = server
|
||||
if os.getenv("CLOUDRESTORE_SQL_USERNAME"):
|
||||
sql["username"] = os.getenv("CLOUDRESTORE_SQL_USERNAME", "").strip()
|
||||
if os.getenv("CLOUDRESTORE_SQL_USE_WINDOWS_AUTH"):
|
||||
sql["use_windows_auth"] = _env_bool("CLOUDRESTORE_SQL_USE_WINDOWS_AUTH", False)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def render_env_template(app_dir: Path | None = None) -> str:
|
||||
"""Genera contenido inicial de config/.env."""
|
||||
base = (app_dir or APP_DIR).resolve()
|
||||
sep = "\\" if os.name == "nt" else "/"
|
||||
|
||||
def p(*parts: str) -> str:
|
||||
return sep.join([str(base), *parts])
|
||||
|
||||
return f"""# CloudRestoreAS — configuración local (editar y reiniciar la app)
|
||||
|
||||
# Arranca el motor al abrir (la ventana siempre se muestra salvo START_MINIMIZED=true)
|
||||
CLOUDRESTORE_AUTO_START=true
|
||||
CLOUDRESTORE_START_MINIMIZED=false
|
||||
CLOUDRESTORE_REGISTER_AUTOSTART=true
|
||||
|
||||
# Servicio PANEL_BASES_ANEXO24 (enrutamiento automático de nodos)
|
||||
CLOUDRESTORE_PANEL_API_URL=
|
||||
CLOUDRESTORE_PANEL_API_TOKEN=
|
||||
CLOUDRESTORE_PANEL_INSTANCE_KEY=
|
||||
CLOUDRESTORE_PANEL_VERIFY_SSL=false
|
||||
|
||||
# Carpetas de trabajo
|
||||
CLOUDRESTORE_INPUT_FOLDER={p("Entrada")}
|
||||
CLOUDRESTORE_PROCESSED_FOLDER={p("Procesados")}
|
||||
CLOUDRESTORE_FAILED_FOLDER={p("Fallados")}
|
||||
CLOUDRESTORE_EXTRACT_FOLDER={p("Temp")}
|
||||
|
||||
# SQL local (solo si no usa panel para credenciales)
|
||||
# CLOUDRESTORE_SQL_SERVER=localhost
|
||||
# CLOUDRESTORE_SQL_USERNAME=
|
||||
# CLOUDRESTORE_DATA_SQL_FOLDER=
|
||||
"""
|
||||
71
app/config/odbc_setup.py
Normal file
71
app/config/odbc_setup.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Configura ODBC portable desde config/odbc/."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from ..constants import IS_WINDOWS, ODBC_DIR
|
||||
from ..utils.logger import app_logger
|
||||
|
||||
|
||||
def _write_portable_odbc_ini(odbc_dir: Path) -> None:
|
||||
"""Genera odbcinst.ini mínimo si no existe."""
|
||||
inst = odbc_dir / "odbcinst.ini"
|
||||
if inst.exists():
|
||||
return
|
||||
|
||||
if IS_WINDOWS:
|
||||
driver_path = odbc_dir / "msodbcsql18.dll"
|
||||
if not driver_path.exists():
|
||||
for candidate in odbc_dir.glob("msodbcsql*.dll"):
|
||||
driver_path = candidate
|
||||
break
|
||||
driver_line = str(driver_path.resolve()) if driver_path.exists() else "msodbcsql18.dll"
|
||||
content = f"""[ODBC Driver 18 for SQL Server]
|
||||
Description=Microsoft ODBC Driver 18 for SQL Server
|
||||
Driver={driver_line}
|
||||
UsageCount=1
|
||||
"""
|
||||
else:
|
||||
so_path = None
|
||||
for pattern in ("libmsodbcsql-18*.so*", "msodbcsql-18*.so*"):
|
||||
matches = list(odbc_dir.glob(pattern))
|
||||
if matches:
|
||||
so_path = matches[0]
|
||||
break
|
||||
driver_line = str(so_path.resolve()) if so_path else "libmsodbcsql-18.so"
|
||||
content = f"""[ODBC Driver 18 for SQL Server]
|
||||
Description=Microsoft ODBC Driver 18 for SQL Server
|
||||
Driver={driver_line}
|
||||
UsageCount=1
|
||||
"""
|
||||
inst.write_text(content, encoding="utf-8")
|
||||
|
||||
ini = odbc_dir / "odbc.ini"
|
||||
if not ini.exists():
|
||||
ini.write_text("[ODBC Data Sources]\n", encoding="utf-8")
|
||||
|
||||
|
||||
def configure_odbc_environment() -> None:
|
||||
"""Apunta pyodbc al driver embebido en config/odbc si está disponible."""
|
||||
if not ODBC_DIR.is_dir():
|
||||
return
|
||||
|
||||
lib_dir = ODBC_DIR / "lib"
|
||||
if lib_dir.is_dir():
|
||||
existing = os.environ.get("LD_LIBRARY_PATH", "")
|
||||
lib_path = str(lib_dir.resolve())
|
||||
if lib_path not in existing.split(":"):
|
||||
os.environ["LD_LIBRARY_PATH"] = (
|
||||
f"{lib_path}:{existing}" if existing else lib_path
|
||||
)
|
||||
|
||||
_write_portable_odbc_ini(ODBC_DIR)
|
||||
odbc_sys = str(ODBC_DIR.resolve())
|
||||
os.environ["ODBCSYSINI"] = odbc_sys
|
||||
os.environ["ODBCINI"] = str((ODBC_DIR / "odbc.ini").resolve())
|
||||
|
||||
if IS_WINDOWS:
|
||||
os.environ["PATH"] = odbc_sys + os.pathsep + os.environ.get("PATH", "")
|
||||
|
||||
app_logger.debug(f"ODBC configurado: ODBCSYSINI={odbc_sys}")
|
||||
Reference in New Issue
Block a user