325 lines
12 KiB
Python
325 lines
12 KiB
Python
"""Worker para procesar jobs de restauración."""
|
|
|
|
import time
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
from PySide6.QtCore import QObject, Signal, QRunnable
|
|
|
|
from ..constants import JobStatus, StepType
|
|
from ..db.job_repository import JobRepository
|
|
from ..db.job_step_repository import JobStepRepository
|
|
from ..db.event_repository import EventRepository
|
|
from ..db.node_repository import NodeRepository
|
|
from ..extract.seven_zip import SevenZipExtractor
|
|
from ..sql.sql_manager import SQLServerManager
|
|
from ..utils.logger import app_logger
|
|
|
|
|
|
class RestoreWorkerSignals(QObject):
|
|
"""Señales para comunicación con la UI."""
|
|
job_started = Signal(str) # job_id
|
|
job_progress = Signal(str, str) # job_id, status
|
|
job_completed = Signal(str, bool) # job_id, success
|
|
error_occurred = Signal(str, str) # job_id, error
|
|
|
|
|
|
class RestoreWorker(QRunnable):
|
|
"""Worker que procesa un job de restauración."""
|
|
|
|
def __init__(
|
|
self,
|
|
job_id: str,
|
|
config: dict,
|
|
dry_run: bool = False
|
|
):
|
|
"""
|
|
Inicializa el worker.
|
|
|
|
Args:
|
|
job_id: ID del job a procesar
|
|
config: Configuración de la aplicación
|
|
dry_run: Modo dry run (no ejecuta RESTORE)
|
|
"""
|
|
super().__init__()
|
|
self.job_id = job_id
|
|
self.config = config
|
|
self.dry_run = dry_run
|
|
self.signals = RestoreWorkerSignals()
|
|
|
|
self._extract_dir: Optional[str] = None
|
|
self._bak_path: Optional[str] = None
|
|
|
|
def run(self):
|
|
"""Ejecuta el procesamiento del job."""
|
|
start_time = time.time()
|
|
|
|
try:
|
|
app_logger.info(f"Iniciando procesamiento de job {self.job_id}")
|
|
self.signals.job_started.emit(self.job_id)
|
|
|
|
# Obtener job
|
|
job = JobRepository.get(self.job_id)
|
|
if not job:
|
|
raise ValueError(f"Job {self.job_id} no encontrado")
|
|
|
|
# Pipeline de procesamiento
|
|
self._process_node_mapping(job)
|
|
self._extract_backup(job)
|
|
self._restore_database(job)
|
|
self._cleanup(job)
|
|
|
|
# Actualizar tiempos
|
|
total_ms = int((time.time() - start_time) * 1000)
|
|
JobRepository.update_timing(self.job_id, total_ms=total_ms)
|
|
|
|
# Marcar como completado
|
|
JobRepository.update_status(self.job_id, JobStatus.COMPLETED)
|
|
|
|
app_logger.info(
|
|
f"Job {self.job_id} completado exitosamente en {total_ms}ms"
|
|
)
|
|
self.signals.job_completed.emit(self.job_id, True)
|
|
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
app_logger.error(f"Error procesando job {self.job_id}: {error_msg}", exc_info=True)
|
|
|
|
JobRepository.update_status(
|
|
self.job_id,
|
|
JobStatus.FAILED,
|
|
error=error_msg,
|
|
increment_attempts=True
|
|
)
|
|
|
|
EventRepository.create("ERROR", f"Job falló: {error_msg}", self.job_id)
|
|
|
|
self.signals.error_occurred.emit(self.job_id, error_msg)
|
|
self.signals.job_completed.emit(self.job_id, False)
|
|
|
|
def _process_node_mapping(self, job):
|
|
"""Procesa el mapeo de nodo a base de datos."""
|
|
step_id = JobStepRepository.create(self.job_id, StepType.NODE_MAPPING)
|
|
|
|
try:
|
|
# Obtener node name del archivo (con extensión, uppercase)
|
|
node_name = Path(job.source_name).name.upper()
|
|
|
|
# Buscar mapeo en la tabla nodes
|
|
db_name = NodeRepository.get_db_for_node(node_name)
|
|
|
|
if not db_name:
|
|
raise ValueError(
|
|
f"NODE_NOT_MAPPED: No existe mapeo activo para nodo '{node_name}'"
|
|
)
|
|
|
|
# Actualizar job con node_name y db_name
|
|
JobRepository.update_node_and_db(self.job_id, node_name, db_name)
|
|
|
|
app_logger.info(f"Node '{node_name}' mapeado a DB '{db_name}'")
|
|
JobStepRepository.complete(step_id, exit_code=0, stdout=f"DB: {db_name}")
|
|
|
|
except Exception as e:
|
|
JobStepRepository.complete(step_id, exit_code=1, error=str(e))
|
|
raise
|
|
|
|
def _extract_backup(self, job):
|
|
"""Extrae el backup del archivo ZIP."""
|
|
JobRepository.update_status(self.job_id, JobStatus.EXTRACTING)
|
|
self.signals.job_progress.emit(self.job_id, JobStatus.EXTRACTING)
|
|
|
|
step_id = JobStepRepository.create(self.job_id, StepType.EXTRACT)
|
|
extract_start = time.time()
|
|
|
|
try:
|
|
# Configuración
|
|
seven_zip_path = self.config["paths"]["seven_zip_exe"]
|
|
extract_base = self.config["paths"]["extract_folder"]
|
|
timeout_minutes = self.config["timeouts"]["extract_minutes"]
|
|
|
|
# Crear carpeta de extracción para este job
|
|
self._extract_dir = str(Path(extract_base) / self.job_id)
|
|
Path(self._extract_dir).mkdir(parents=True, exist_ok=True)
|
|
|
|
# Extraer
|
|
extractor = SevenZipExtractor(seven_zip_path)
|
|
|
|
# Si es multipart, asegurar que usamos el primer archivo
|
|
source_path = job.source_path
|
|
if extractor.is_multipart(source_path):
|
|
source_path = extractor.get_first_part(source_path)
|
|
app_logger.info(f"Archivo multipart detectado, usando: {source_path}")
|
|
|
|
exit_code, stdout, stderr = extractor.extract(
|
|
source_path,
|
|
self._extract_dir,
|
|
timeout_minutes
|
|
)
|
|
|
|
extract_ms = int((time.time() - extract_start) * 1000)
|
|
JobRepository.update_timing(self.job_id, extract_ms=extract_ms)
|
|
|
|
if exit_code != 0:
|
|
raise RuntimeError(f"7-Zip falló con código {exit_code}: {stderr}")
|
|
|
|
JobStepRepository.complete(
|
|
step_id,
|
|
exit_code=exit_code,
|
|
stdout=stdout[:1000] if stdout else None,
|
|
stderr=stderr[:1000] if stderr else None
|
|
)
|
|
|
|
# Buscar archivo .bak
|
|
locate_step_id = JobStepRepository.create(self.job_id, StepType.LOCATE_BAK)
|
|
|
|
self._bak_path = SevenZipExtractor.find_bak_file(self._extract_dir)
|
|
|
|
if not self._bak_path:
|
|
raise FileNotFoundError("No se encontró archivo .bak en el archivo extraído")
|
|
|
|
JobStepRepository.complete(
|
|
locate_step_id,
|
|
exit_code=0,
|
|
stdout=f"BAK: {Path(self._bak_path).name}"
|
|
)
|
|
|
|
except Exception as e:
|
|
JobStepRepository.complete(step_id, exit_code=1, error=str(e))
|
|
raise
|
|
|
|
def _restore_database(self, job):
|
|
"""Restaura la base de datos desde el backup."""
|
|
JobRepository.update_status(self.job_id, JobStatus.RESTORING)
|
|
self.signals.job_progress.emit(self.job_id, JobStatus.RESTORING)
|
|
|
|
# Recargar job para obtener db_name actualizado
|
|
job = JobRepository.get(self.job_id)
|
|
|
|
if not job.db_name:
|
|
raise ValueError("DB name no está configurado en el job")
|
|
|
|
# Conectar a SQL
|
|
connect_step_id = JobStepRepository.create(self.job_id, StepType.SQL_CONNECT)
|
|
|
|
try:
|
|
sql_config = self.config["sql"]
|
|
sql_manager = SQLServerManager(
|
|
server=sql_config["server"],
|
|
use_windows_auth=sql_config["use_windows_auth"],
|
|
username=sql_config.get("username"),
|
|
password=sql_config.get("password")
|
|
)
|
|
|
|
# Test connection
|
|
success, error = sql_manager.test_connection()
|
|
if not success:
|
|
raise RuntimeError(f"Conexión SQL falló: {error}")
|
|
|
|
JobStepRepository.complete(connect_step_id, exit_code=0)
|
|
|
|
except Exception as e:
|
|
JobStepRepository.complete(connect_step_id, exit_code=1, error=str(e))
|
|
raise
|
|
|
|
# Obtener FILELISTONLY
|
|
filelist_step_id = JobStepRepository.create(self.job_id, StepType.FILELIST)
|
|
filelist_start = time.time()
|
|
|
|
try:
|
|
logical_files, error = sql_manager.get_filelist_from_backup(
|
|
self._bak_path,
|
|
timeout_minutes=self.config["timeouts"]["restore_minutes"]
|
|
)
|
|
|
|
filelist_ms = int((time.time() - filelist_start) * 1000)
|
|
JobRepository.update_timing(self.job_id, filelist_ms=filelist_ms)
|
|
|
|
if error:
|
|
raise RuntimeError(error)
|
|
|
|
files_info = ", ".join([f"{lf.logical_name}({lf.type})" for lf in logical_files])
|
|
JobStepRepository.complete(
|
|
filelist_step_id,
|
|
exit_code=0,
|
|
stdout=files_info[:1000]
|
|
)
|
|
|
|
except Exception as e:
|
|
JobStepRepository.complete(filelist_step_id, exit_code=1, error=str(e))
|
|
raise
|
|
|
|
# RESTORE DATABASE
|
|
restore_step_id = JobStepRepository.create(self.job_id, StepType.RESTORE)
|
|
restore_start = time.time()
|
|
|
|
try:
|
|
data_folder = self.config["paths"]["data_sql_folder"]
|
|
|
|
success, stdout, error = sql_manager.restore_database(
|
|
db_name=job.db_name,
|
|
backup_path=self._bak_path,
|
|
data_folder=data_folder,
|
|
logical_files=logical_files,
|
|
timeout_minutes=self.config["timeouts"]["restore_minutes"],
|
|
dry_run=self.dry_run
|
|
)
|
|
|
|
restore_ms = int((time.time() - restore_start) * 1000)
|
|
JobRepository.update_timing(self.job_id, restore_ms=restore_ms)
|
|
|
|
if not success:
|
|
raise RuntimeError(error or "RESTORE falló sin mensaje de error")
|
|
|
|
JobStepRepository.complete(
|
|
restore_step_id,
|
|
exit_code=0,
|
|
stdout=stdout[:1000] if stdout else None
|
|
)
|
|
|
|
except Exception as e:
|
|
JobStepRepository.complete(restore_step_id, exit_code=1, error=str(e))
|
|
raise
|
|
|
|
def _cleanup(self, job):
|
|
"""Limpia archivos temporales y mueve el ZIP."""
|
|
JobRepository.update_status(self.job_id, JobStatus.CLEANING)
|
|
self.signals.job_progress.emit(self.job_id, JobStatus.CLEANING)
|
|
|
|
step_id = JobStepRepository.create(self.job_id, StepType.CLEANUP)
|
|
|
|
try:
|
|
# Eliminar carpeta de extracción
|
|
if self._extract_dir and Path(self._extract_dir).exists():
|
|
shutil.rmtree(self._extract_dir)
|
|
app_logger.info(f"Carpeta de extracción eliminada: {self._extract_dir}")
|
|
|
|
# Mover ZIP a Processed
|
|
processed_folder = Path(self.config["paths"]["processed_folder"])
|
|
date_folder = processed_folder / datetime.now().strftime("%Y-%m-%d")
|
|
date_folder.mkdir(parents=True, exist_ok=True)
|
|
|
|
source_path = Path(job.source_path)
|
|
dest_path = date_folder / source_path.name
|
|
|
|
# Si es multipart, mover todas las partes
|
|
if SevenZipExtractor.is_multipart(str(source_path)):
|
|
# Buscar todas las partes
|
|
base_name = source_path.stem.split('.zip')[0]
|
|
parts = list(source_path.parent.glob(f"{base_name}.zip.*"))
|
|
|
|
for part in parts:
|
|
part_dest = date_folder / part.name
|
|
shutil.move(str(part), str(part_dest))
|
|
app_logger.info(f"Movido: {part.name} -> {part_dest}")
|
|
else:
|
|
shutil.move(str(source_path), str(dest_path))
|
|
app_logger.info(f"Movido: {source_path.name} -> {dest_path}")
|
|
|
|
JobStepRepository.complete(step_id, exit_code=0)
|
|
|
|
except Exception as e:
|
|
# No fallar el job por errores de limpieza
|
|
app_logger.warning(f"Error en limpieza (no crítico): {e}")
|
|
JobStepRepository.complete(step_id, exit_code=1, error=str(e))
|