Initial commit: CloudRestoreAS v1.0.0 - Aplicación completa de restauración automática SQL Server

This commit is contained in:
2026-01-25 16:53:00 -07:00
commit 854ffa116f
43 changed files with 6357 additions and 0 deletions

399
app/ui/config_tab.py Normal file
View File

@@ -0,0 +1,399 @@
"""Tab de configuración."""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox,
QFormLayout, QLineEdit, QPushButton, QSpinBox,
QCheckBox, QFileDialog, QMessageBox, QScrollArea
)
from pathlib import Path
from ..utils.crypto import encrypt_password, decrypt_password
from ..extract.seven_zip import SevenZipExtractor
from ..sql.sql_manager import SQLServerManager
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._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)
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: 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))
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()
}
}
# 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 _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}")