517 lines
22 KiB
Python
517 lines
22 KiB
Python
"""Tab de configuración."""
|
|
|
|
import sys
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
|
|
QFormLayout, QLineEdit, QPushButton, QSpinBox,
|
|
QCheckBox, QFileDialog, QMessageBox, QScrollArea, QComboBox
|
|
)
|
|
from pathlib import Path
|
|
|
|
from ..utils.crypto import encrypt_password, decrypt_password
|
|
from ..extract.seven_zip import SevenZipExtractor
|
|
from ..sql.sql_manager import SQLServerManager
|
|
from ..panel import panel_client
|
|
|
|
|
|
class ConfigTab(QWidget):
|
|
"""Tab de configuración de la aplicación."""
|
|
|
|
def __init__(self, engine):
|
|
"""Inicializa el tab."""
|
|
super().__init__()
|
|
self.engine = engine
|
|
self.config = {}
|
|
self._catalog_names: list[str] = []
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Configura la interfaz."""
|
|
# Scroll area para la configuración
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
|
|
container = QWidget()
|
|
layout = QVBoxLayout(container)
|
|
layout.setSpacing(15)
|
|
|
|
# Grupo: Carpetas
|
|
paths_group = QGroupBox("Carpetas")
|
|
paths_layout = QFormLayout()
|
|
|
|
self.input_folder_input = QLineEdit()
|
|
input_browse_btn = QPushButton("📁")
|
|
input_browse_btn.clicked.connect(lambda: self._browse_folder(self.input_folder_input))
|
|
input_layout = QHBoxLayout()
|
|
input_layout.addWidget(self.input_folder_input)
|
|
input_layout.addWidget(input_browse_btn)
|
|
paths_layout.addRow("Carpeta de Entrada:", input_layout)
|
|
|
|
self.processed_folder_input = QLineEdit()
|
|
processed_browse_btn = QPushButton("📁")
|
|
processed_browse_btn.clicked.connect(lambda: self._browse_folder(self.processed_folder_input))
|
|
processed_layout = QHBoxLayout()
|
|
processed_layout.addWidget(self.processed_folder_input)
|
|
processed_layout.addWidget(processed_browse_btn)
|
|
paths_layout.addRow("Carpeta Procesados:", processed_layout)
|
|
|
|
self.failed_folder_input = QLineEdit()
|
|
failed_browse_btn = QPushButton("📁")
|
|
failed_browse_btn.clicked.connect(lambda: self._browse_folder(self.failed_folder_input))
|
|
failed_layout = QHBoxLayout()
|
|
failed_layout.addWidget(self.failed_folder_input)
|
|
failed_layout.addWidget(failed_browse_btn)
|
|
paths_layout.addRow("Carpeta Fallados:", failed_layout)
|
|
|
|
self.extract_folder_input = QLineEdit()
|
|
extract_browse_btn = QPushButton("📁")
|
|
extract_browse_btn.clicked.connect(lambda: self._browse_folder(self.extract_folder_input))
|
|
extract_layout = QHBoxLayout()
|
|
extract_layout.addWidget(self.extract_folder_input)
|
|
extract_layout.addWidget(extract_browse_btn)
|
|
paths_layout.addRow("Carpeta Extracción:", extract_layout)
|
|
|
|
self.data_sql_folder_input = QLineEdit()
|
|
data_browse_btn = QPushButton("📁")
|
|
data_browse_btn.clicked.connect(lambda: self._browse_folder(self.data_sql_folder_input))
|
|
data_layout = QHBoxLayout()
|
|
data_layout.addWidget(self.data_sql_folder_input)
|
|
data_layout.addWidget(data_browse_btn)
|
|
paths_layout.addRow("Carpeta Data SQL:", data_layout)
|
|
|
|
self.seven_zip_input = QLineEdit()
|
|
seven_zip_browse_btn = QPushButton("📁")
|
|
seven_zip_browse_btn.clicked.connect(self._browse_seven_zip)
|
|
seven_zip_detect_btn = QPushButton("🔍 Detectar")
|
|
seven_zip_detect_btn.clicked.connect(self._detect_seven_zip)
|
|
seven_zip_layout = QHBoxLayout()
|
|
seven_zip_layout.addWidget(self.seven_zip_input)
|
|
seven_zip_layout.addWidget(seven_zip_browse_btn)
|
|
seven_zip_layout.addWidget(seven_zip_detect_btn)
|
|
paths_layout.addRow("7-Zip (7z.exe):", seven_zip_layout)
|
|
|
|
paths_group.setLayout(paths_layout)
|
|
layout.addWidget(paths_group)
|
|
|
|
# Grupo: SQL Server
|
|
sql_group = QGroupBox("SQL Server")
|
|
sql_layout = QFormLayout()
|
|
|
|
self.sql_server_input = QLineEdit()
|
|
self.sql_server_input.setPlaceholderText("localhost o IP")
|
|
sql_layout.addRow("Servidor:", self.sql_server_input)
|
|
|
|
self.sql_windows_auth_checkbox = QCheckBox()
|
|
self.sql_windows_auth_checkbox.setChecked(True)
|
|
self.sql_windows_auth_checkbox.toggled.connect(self._on_auth_changed)
|
|
sql_layout.addRow("Usar Windows Auth:", self.sql_windows_auth_checkbox)
|
|
if sys.platform != "win32":
|
|
self.sql_windows_auth_checkbox.setChecked(False)
|
|
self.sql_windows_auth_checkbox.setEnabled(False)
|
|
self.sql_windows_auth_checkbox.setToolTip(
|
|
"Windows Auth no está disponible en Linux; use SQL Auth."
|
|
)
|
|
|
|
self.sql_username_input = QLineEdit()
|
|
self.sql_username_input.setEnabled(False)
|
|
sql_layout.addRow("Usuario SQL:", self.sql_username_input)
|
|
|
|
self.sql_password_input = QLineEdit()
|
|
self.sql_password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
|
self.sql_password_input.setEnabled(False)
|
|
sql_layout.addRow("Contraseña SQL:", self.sql_password_input)
|
|
|
|
test_sql_btn = QPushButton("🔌 Probar Conexión")
|
|
test_sql_btn.clicked.connect(self._test_sql_connection)
|
|
sql_layout.addRow("", test_sql_btn)
|
|
|
|
sql_group.setLayout(sql_layout)
|
|
layout.addWidget(sql_group)
|
|
|
|
# Grupo: PANEL de Control (servidor de restauración activo)
|
|
panel_group = QGroupBox("PANEL de Control")
|
|
panel_layout = QFormLayout()
|
|
|
|
self.panel_url_input = QLineEdit()
|
|
self.panel_url_input.setPlaceholderText("http://ip-del-panel:3000")
|
|
panel_layout.addRow("URL del PANEL:", self.panel_url_input)
|
|
|
|
self.panel_token_input = QLineEdit()
|
|
self.panel_token_input.setEchoMode(QLineEdit.EchoMode.Password)
|
|
panel_layout.addRow("API Token:", self.panel_token_input)
|
|
|
|
self.panel_instance_combo = QComboBox()
|
|
self.panel_instance_combo.setToolTip(
|
|
"Nombre de este servidor en el panel. El CRA restaurará aquí lo que le "
|
|
"corresponda y reenviará el resto por SFTP automáticamente."
|
|
)
|
|
self.refresh_instance_btn = QPushButton("Actualizar lista de servidores")
|
|
self.refresh_instance_btn.clicked.connect(self._refresh_instance_combo)
|
|
instance_row = QHBoxLayout()
|
|
instance_row.addWidget(self.panel_instance_combo, stretch=1)
|
|
instance_row.addWidget(self.refresh_instance_btn)
|
|
panel_layout.addRow("Instancia (servidor):", instance_row)
|
|
|
|
test_panel_btn = QPushButton("🔌 Probar Conexión al PANEL")
|
|
test_panel_btn.clicked.connect(self._test_panel_connection)
|
|
panel_layout.addRow("", test_panel_btn)
|
|
|
|
panel_group.setLayout(panel_layout)
|
|
layout.addWidget(panel_group)
|
|
|
|
# Grupo: Concurrencia
|
|
concurrency_group = QGroupBox("Concurrencia")
|
|
concurrency_layout = QFormLayout()
|
|
|
|
self.extract_workers_spinbox = QSpinBox()
|
|
self.extract_workers_spinbox.setMinimum(1)
|
|
self.extract_workers_spinbox.setMaximum(10)
|
|
concurrency_layout.addRow("Workers de Extracción:", self.extract_workers_spinbox)
|
|
|
|
self.restore_workers_spinbox = QSpinBox()
|
|
self.restore_workers_spinbox.setMinimum(1)
|
|
self.restore_workers_spinbox.setMaximum(5)
|
|
concurrency_layout.addRow("Workers de Restore:", self.restore_workers_spinbox)
|
|
|
|
concurrency_group.setLayout(concurrency_layout)
|
|
layout.addWidget(concurrency_group)
|
|
|
|
# Grupo: Estabilidad de Archivos
|
|
stability_group = QGroupBox("Estabilidad de Archivos")
|
|
stability_layout = QFormLayout()
|
|
|
|
self.stability_enabled_checkbox = QCheckBox()
|
|
self.stability_enabled_checkbox.setChecked(True)
|
|
stability_layout.addRow("Verificar Estabilidad:", self.stability_enabled_checkbox)
|
|
|
|
self.stability_interval_spinbox = QSpinBox()
|
|
self.stability_interval_spinbox.setMinimum(1)
|
|
self.stability_interval_spinbox.setMaximum(60)
|
|
self.stability_interval_spinbox.setSuffix(" s")
|
|
stability_layout.addRow("Intervalo de Verificación:", self.stability_interval_spinbox)
|
|
|
|
self.stable_duration_spinbox = QSpinBox()
|
|
self.stable_duration_spinbox.setMinimum(1)
|
|
self.stable_duration_spinbox.setMaximum(300)
|
|
self.stable_duration_spinbox.setSuffix(" s")
|
|
stability_layout.addRow("Duración Estable:", self.stable_duration_spinbox)
|
|
|
|
self.ready_marker_checkbox = QCheckBox()
|
|
stability_layout.addRow("Usar Marcador .ready:", self.ready_marker_checkbox)
|
|
|
|
stability_group.setLayout(stability_layout)
|
|
layout.addWidget(stability_group)
|
|
|
|
# Grupo: Timeouts
|
|
timeouts_group = QGroupBox("Timeouts")
|
|
timeouts_layout = QFormLayout()
|
|
|
|
self.extract_timeout_spinbox = QSpinBox()
|
|
self.extract_timeout_spinbox.setMinimum(1)
|
|
self.extract_timeout_spinbox.setMaximum(240)
|
|
self.extract_timeout_spinbox.setSuffix(" min")
|
|
timeouts_layout.addRow("Extracción:", self.extract_timeout_spinbox)
|
|
|
|
self.restore_timeout_spinbox = QSpinBox()
|
|
self.restore_timeout_spinbox.setMinimum(1)
|
|
self.restore_timeout_spinbox.setMaximum(480)
|
|
self.restore_timeout_spinbox.setSuffix(" min")
|
|
timeouts_layout.addRow("Restauración:", self.restore_timeout_spinbox)
|
|
|
|
timeouts_group.setLayout(timeouts_layout)
|
|
layout.addWidget(timeouts_group)
|
|
|
|
# Grupo: Características
|
|
features_group = QGroupBox("Características")
|
|
features_layout = QFormLayout()
|
|
|
|
self.dry_run_checkbox = QCheckBox()
|
|
features_layout.addRow("Modo Dry Run:", self.dry_run_checkbox)
|
|
|
|
self.auto_scan_checkbox = QCheckBox()
|
|
self.auto_scan_checkbox.setChecked(True)
|
|
features_layout.addRow("Escaneo Automático:", self.auto_scan_checkbox)
|
|
|
|
self.scan_interval_spinbox = QSpinBox()
|
|
self.scan_interval_spinbox.setMinimum(10)
|
|
self.scan_interval_spinbox.setMaximum(600)
|
|
self.scan_interval_spinbox.setSuffix(" s")
|
|
features_layout.addRow("Intervalo de Escaneo:", self.scan_interval_spinbox)
|
|
|
|
features_group.setLayout(features_layout)
|
|
layout.addWidget(features_group)
|
|
|
|
# Botones de acción
|
|
actions_layout = QHBoxLayout()
|
|
|
|
save_btn = QPushButton("💾 Guardar Configuración")
|
|
save_btn.clicked.connect(self._save_config)
|
|
actions_layout.addWidget(save_btn)
|
|
|
|
actions_layout.addStretch()
|
|
|
|
layout.addLayout(actions_layout)
|
|
layout.addStretch()
|
|
|
|
scroll.setWidget(container)
|
|
|
|
# Layout principal
|
|
main_layout = QVBoxLayout(self)
|
|
main_layout.addWidget(scroll)
|
|
|
|
def load_config(self, config: dict):
|
|
"""Carga la configuración en la UI."""
|
|
self.config = config
|
|
|
|
# Carpetas
|
|
paths = config.get("paths", {})
|
|
self.input_folder_input.setText(paths.get("input_folder", ""))
|
|
self.processed_folder_input.setText(paths.get("processed_folder", ""))
|
|
self.failed_folder_input.setText(paths.get("failed_folder", ""))
|
|
self.extract_folder_input.setText(paths.get("extract_folder", ""))
|
|
self.data_sql_folder_input.setText(paths.get("data_sql_folder", ""))
|
|
self.seven_zip_input.setText(paths.get("seven_zip_exe", ""))
|
|
|
|
# SQL
|
|
sql = config.get("sql", {})
|
|
self.sql_server_input.setText(sql.get("server", "localhost"))
|
|
self.sql_windows_auth_checkbox.setChecked(sql.get("use_windows_auth", True))
|
|
self.sql_username_input.setText(sql.get("username", ""))
|
|
|
|
# Descifrar password si existe
|
|
encrypted_pass = sql.get("password_encrypted", "")
|
|
if encrypted_pass:
|
|
try:
|
|
decrypted = decrypt_password(encrypted_pass)
|
|
self.sql_password_input.setText(decrypted)
|
|
except:
|
|
pass
|
|
|
|
# Concurrencia
|
|
concurrency = config.get("concurrency", {})
|
|
self.extract_workers_spinbox.setValue(concurrency.get("extract_workers", 1))
|
|
self.restore_workers_spinbox.setValue(concurrency.get("restore_workers", 1))
|
|
|
|
# Estabilidad
|
|
stability = config.get("stability", {})
|
|
self.stability_enabled_checkbox.setChecked(stability.get("check_enabled", True))
|
|
self.stability_interval_spinbox.setValue(stability.get("check_interval_seconds", 5))
|
|
self.stable_duration_spinbox.setValue(stability.get("stable_duration_seconds", 10))
|
|
self.ready_marker_checkbox.setChecked(stability.get("use_ready_marker", False))
|
|
|
|
# Timeouts
|
|
timeouts = config.get("timeouts", {})
|
|
self.extract_timeout_spinbox.setValue(timeouts.get("extract_minutes", 30))
|
|
self.restore_timeout_spinbox.setValue(timeouts.get("restore_minutes", 60))
|
|
|
|
# Características
|
|
features = config.get("features", {})
|
|
self.dry_run_checkbox.setChecked(features.get("dry_run_mode", False))
|
|
self.auto_scan_checkbox.setChecked(features.get("auto_scan_enabled", True))
|
|
self.scan_interval_spinbox.setValue(features.get("scan_interval_seconds", 30))
|
|
|
|
# PANEL de Control
|
|
panel = config.get("panel", {})
|
|
self.panel_url_input.setText(panel.get("api_url", ""))
|
|
self.panel_token_input.setText(panel.get("api_token", ""))
|
|
self._refresh_instance_combo()
|
|
|
|
def _refresh_instance_combo(self):
|
|
"""Recarga los nombres de servidores desde el catálogo del panel."""
|
|
current = self.panel_instance_combo.currentText().strip()
|
|
saved_key = (self.config.get("panel", {}).get("instance_key") or "").strip()
|
|
preserve = current or saved_key
|
|
|
|
api_url = self.panel_url_input.text().strip()
|
|
api_token = self.panel_token_input.text().strip()
|
|
|
|
names: list[str] = []
|
|
if api_url and api_token:
|
|
names = panel_client.list_restore_target_names(api_url, api_token)
|
|
|
|
self._catalog_names = names
|
|
|
|
items = [""]
|
|
for name in names:
|
|
if name not in items:
|
|
items.append(name)
|
|
if preserve and preserve not in items:
|
|
items.append(preserve)
|
|
|
|
self.panel_instance_combo.blockSignals(True)
|
|
self.panel_instance_combo.clear()
|
|
self.panel_instance_combo.addItems(items)
|
|
idx = self.panel_instance_combo.findText(preserve)
|
|
self.panel_instance_combo.setCurrentIndex(idx if idx >= 0 else 0)
|
|
self.panel_instance_combo.blockSignals(False)
|
|
|
|
def _save_config(self):
|
|
"""Guarda la configuración."""
|
|
# Construir config
|
|
config = {
|
|
"paths": {
|
|
"input_folder": self.input_folder_input.text(),
|
|
"processed_folder": self.processed_folder_input.text(),
|
|
"failed_folder": self.failed_folder_input.text(),
|
|
"extract_folder": self.extract_folder_input.text(),
|
|
"data_sql_folder": self.data_sql_folder_input.text(),
|
|
"seven_zip_exe": self.seven_zip_input.text()
|
|
},
|
|
"sql": {
|
|
"server": self.sql_server_input.text(),
|
|
"use_windows_auth": self.sql_windows_auth_checkbox.isChecked(),
|
|
"username": self.sql_username_input.text(),
|
|
"password_encrypted": ""
|
|
},
|
|
"concurrency": {
|
|
"extract_workers": self.extract_workers_spinbox.value(),
|
|
"restore_workers": self.restore_workers_spinbox.value()
|
|
},
|
|
"stability": {
|
|
"check_enabled": self.stability_enabled_checkbox.isChecked(),
|
|
"check_interval_seconds": self.stability_interval_spinbox.value(),
|
|
"stable_duration_seconds": self.stable_duration_spinbox.value(),
|
|
"use_ready_marker": self.ready_marker_checkbox.isChecked()
|
|
},
|
|
"timeouts": {
|
|
"extract_minutes": self.extract_timeout_spinbox.value(),
|
|
"restore_minutes": self.restore_timeout_spinbox.value()
|
|
},
|
|
"retries": self.config.get("retries", {"max_attempts": 3, "retry_delay_seconds": 60}),
|
|
"features": {
|
|
"dry_run_mode": self.dry_run_checkbox.isChecked(),
|
|
"auto_scan_enabled": self.auto_scan_checkbox.isChecked(),
|
|
"scan_interval_seconds": self.scan_interval_spinbox.value()
|
|
},
|
|
"panel": {
|
|
"api_url": self.panel_url_input.text().strip(),
|
|
"api_token": self.panel_token_input.text().strip(),
|
|
"instance_key": self.panel_instance_combo.currentText().strip(),
|
|
}
|
|
}
|
|
|
|
panel_url = self.panel_url_input.text().strip()
|
|
panel_token = self.panel_token_input.text().strip()
|
|
instance_key = self.panel_instance_combo.currentText().strip()
|
|
if panel_url and panel_token and not instance_key:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Advertencia",
|
|
"Con panel configurado debe seleccionar la instancia (servidor).",
|
|
)
|
|
if panel_url and instance_key and self._catalog_names and instance_key not in self._catalog_names:
|
|
QMessageBox.warning(
|
|
self,
|
|
"Advertencia",
|
|
f'La instancia "{instance_key}" no está en el catálogo del panel. '
|
|
"Verifique el nombre o cree el servidor en Servidores de Restauración.",
|
|
)
|
|
|
|
# Cifrar password si no usa Windows Auth
|
|
if not self.sql_windows_auth_checkbox.isChecked():
|
|
password = self.sql_password_input.text()
|
|
if password:
|
|
try:
|
|
config["sql"]["password_encrypted"] = encrypt_password(password)
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Advertencia", f"Error cifrando contraseña: {e}")
|
|
|
|
# Guardar
|
|
try:
|
|
self.engine.save_config(config)
|
|
QMessageBox.information(self, "Éxito", "Configuración guardada exitosamente.")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Error guardando configuración: {e}")
|
|
|
|
def _browse_folder(self, line_edit: QLineEdit):
|
|
"""Abre el diálogo para seleccionar carpeta."""
|
|
folder = QFileDialog.getExistingDirectory(self, "Seleccionar Carpeta")
|
|
if folder:
|
|
line_edit.setText(folder)
|
|
|
|
def _browse_seven_zip(self):
|
|
"""Abre el diálogo para seleccionar 7z.exe."""
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self,
|
|
"Seleccionar 7z.exe",
|
|
"C:\\Program Files\\7-Zip",
|
|
"Ejecutables (*.exe)"
|
|
)
|
|
if file_path:
|
|
self.seven_zip_input.setText(file_path)
|
|
|
|
def _detect_seven_zip(self):
|
|
"""Auto-detecta 7-Zip."""
|
|
path = SevenZipExtractor._auto_detect_7zip()
|
|
if path:
|
|
self.seven_zip_input.setText(path)
|
|
QMessageBox.information(self, "Éxito", f"7-Zip detectado en:\n{path}")
|
|
else:
|
|
QMessageBox.warning(
|
|
self,
|
|
"No Encontrado",
|
|
"No se pudo auto-detectar 7-Zip. Por favor, selecciona manualmente."
|
|
)
|
|
|
|
def _test_panel_connection(self):
|
|
"""Prueba la conexión al PANEL y muestra el servidor de restauración activo."""
|
|
api_url = self.panel_url_input.text().strip()
|
|
api_token = self.panel_token_input.text().strip()
|
|
|
|
if not api_url:
|
|
QMessageBox.warning(self, "Error", "Debe especificar la URL del PANEL.")
|
|
return
|
|
|
|
success, info = panel_client.test_connection(api_url, api_token)
|
|
|
|
if success:
|
|
self._refresh_instance_combo()
|
|
QMessageBox.information(
|
|
self, "Éxito", f"Conexión al PANEL exitosa.\n{info or ''}"
|
|
)
|
|
else:
|
|
QMessageBox.critical(self, "Error", f"No se pudo conectar al PANEL:\n{info}")
|
|
|
|
def _on_auth_changed(self, checked: bool):
|
|
"""Maneja el cambio en el tipo de autenticación."""
|
|
self.sql_username_input.setEnabled(not checked)
|
|
self.sql_password_input.setEnabled(not checked)
|
|
|
|
def _test_sql_connection(self):
|
|
"""Prueba la conexión a SQL Server."""
|
|
server = self.sql_server_input.text()
|
|
use_windows_auth = self.sql_windows_auth_checkbox.isChecked()
|
|
username = self.sql_username_input.text()
|
|
password = self.sql_password_input.text()
|
|
|
|
if not server:
|
|
QMessageBox.warning(self, "Error", "Debe especificar un servidor.")
|
|
return
|
|
|
|
try:
|
|
manager = SQLServerManager(
|
|
server=server,
|
|
use_windows_auth=use_windows_auth,
|
|
username=username if not use_windows_auth else None,
|
|
password=password if not use_windows_auth else None
|
|
)
|
|
|
|
success, error = manager.test_connection()
|
|
|
|
if success:
|
|
QMessageBox.information(
|
|
self,
|
|
"Éxito",
|
|
"Conexión a SQL Server exitosa."
|
|
)
|
|
else:
|
|
QMessageBox.critical(
|
|
self,
|
|
"Error",
|
|
f"Error conectando a SQL Server:\n{error}"
|
|
)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Error: {e}")
|