294 lines
10 KiB
Ruby
Executable File
294 lines
10 KiB
Ruby
Executable File
#!/usr/bin/ruby
|
||
# frozen_string_literal: true
|
||
|
||
# ==========================================================
|
||
# dtic-BKPs - Automatic Backup Processor (Unified Script)
|
||
# ----------------------------------------------------------
|
||
# APP: dtic-BKPs
|
||
# VERSION: v5.5.1
|
||
# AUTHOR: Ricardo MONLA ( rmonla@ )
|
||
# LICENSE: MIT License
|
||
# ==========================================================
|
||
|
||
# --- REQUIRES GLOBALES ---
|
||
require 'fileutils'
|
||
require 'open3'
|
||
require 'time'
|
||
require 'io/console'
|
||
require 'shellwords'
|
||
require 'logger'
|
||
|
||
# --- CARGA INICIAL DE MÓDULOS ---
|
||
# (Se asume que los otros archivos están en el mismo directorio o accesibles)
|
||
# require_relative 'core/globals'
|
||
require_relative 'fxs/config_handler'
|
||
require_relative 'fxs/ui_handlers'
|
||
require_relative 'fxs/proc_syncPmox'
|
||
require_relative 'fxs/sync_rclone'
|
||
require_relative 'fxs/proc_syncXEN'
|
||
|
||
# ==========================================================
|
||
# CONSTANTES GLOBALES
|
||
# ==========================================================
|
||
|
||
# --- CONSTANTES GLOBALES DEL SCRIPT ---
|
||
APP_NOM_BASE = 'dtic-BKPs'
|
||
APP_NOM = "#{APP_NOM_BASE}_app.rb"
|
||
APP_VER = 'v5.5.1' # Actualizado
|
||
APP_AUTOR = "Ricardo MONLA ( rmonla@ )"
|
||
APP_DIR_SCRIPT = File.dirname(File.expand_path(__FILE__))
|
||
APP_CFG_NOM_ARCHIVO = "#{APP_NOM_BASE}_tasks.rb"
|
||
APP_LOG_NOM_ARCHIVO = "logs/#{APP_NOM_BASE}.log"
|
||
APP_CFG_RUTA_ARCHIVO = File.join(APP_DIR_SCRIPT, APP_CFG_NOM_ARCHIVO)
|
||
APP_LOG_RUTA_ARCHIVO = File.join(APP_DIR_SCRIPT, APP_LOG_NOM_ARCHIVO)
|
||
|
||
# --- COLORES E ICONOS GLOBALES ---
|
||
RESET = "\e[0m"; NEGRITA = "\e[1m"; SUBRAYADO="\e[4m"; INTERMITENTE = "\e[5m"
|
||
CIAN = "\e[96m"; AMARILLO = "\e[93m"; ROJO = "\e[91m"; VERDE = "\e[92m"
|
||
ICONO_OK = "#{VERDE}✔#{RESET}"; ICONO_ERROR = "#{ROJO}✖#{RESET}"; ICONO_WARN = "#{AMARILLO}⚠#{RESET}"; ICONO_INFO = "#{CIAN}ℹ#{RESET}"
|
||
|
||
# --- CONFIGURACIÓN GLOBAL ---
|
||
|
||
# Bloque para configurar el logger directamente aquí
|
||
require 'logger' # Asegúrate de que Logger esté cargado
|
||
$logger = Logger.new(APP_LOG_RUTA_ARCHIVO, 'daily')
|
||
$logger.level = Logger::INFO # Puedes ajustar el nivel de log aquí
|
||
$logger.formatter = proc do |severity, datetime, progname, msg|
|
||
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] [#{severity}] #{msg}\n"
|
||
end
|
||
|
||
$estadisticas_ejecucion = { tareas_completadas: {} }
|
||
|
||
# --- FUNCIÓN DE LOGGING GLOBAL ---
|
||
def log(nivel, mensaje)
|
||
# Usa esta expresión regular más robusta para eliminar códigos ANSI
|
||
msg_l = mensaje.gsub(/\e\[(?:\d{1,3}(?:;\d{1,3})*)?[m|K]/, '')
|
||
case nivel
|
||
when :info then $logger.info(msg_l)
|
||
when :error then $logger.error(msg_l)
|
||
when :warn then $logger.warn(msg_l)
|
||
else $logger.debug(msg_l)
|
||
end
|
||
end
|
||
|
||
# Función auxiliar para verificar si un comando existe
|
||
def which(cmd)
|
||
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
||
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
||
exts.each { |ext|
|
||
exe = File.join(path, "#{cmd}#{ext}")
|
||
return exe if File.executable?(exe) && !File.directory?(exe)
|
||
}
|
||
end
|
||
nil
|
||
end
|
||
|
||
# ==========================================================
|
||
# (Bucle Principal)
|
||
# ==========================================================
|
||
|
||
# Mapea los tipos de proceso a sus funciones correspondientes.
|
||
PROCESSOR_MAP = {
|
||
xva: ->(task_cfg) { procesar_backups_xva(task_cfg) },
|
||
rclone_sync: ->(task_cfg) { sincronizar_rclone(task_cfg) },
|
||
proxmox_log_notes: ->(task_cfg) { procesar_backups_tipo_proxmox(task_cfg) }
|
||
}
|
||
|
||
def ejecutar_tarea_individual(tarea_cfg)
|
||
processor = PROCESSOR_MAP[tarea_cfg[:tipo_proceso]]
|
||
if processor
|
||
processor.call(tarea_cfg)
|
||
else
|
||
puts "#{ICONO_ERROR} Tipo de proceso desconocido: #{tarea_cfg[:tipo_proceso]}"
|
||
log :error, "Tipo de proceso desconocido para tarea #{tarea_cfg[:id]}: #{tarea_cfg[:tipo_proceso]}"
|
||
end
|
||
end
|
||
|
||
def ejecutar_comando(comando_cfg, todas_las_tareas)
|
||
system 'clear' or system 'cls'
|
||
puts "\n#{NEGRITA}#{CIAN}--- Ejecutando Comando: #{comando_cfg[:menu_texto]} ---#{RESET}"
|
||
log :info, "Iniciando ejecución del comando: #{comando_cfg[:id]}"
|
||
|
||
comando_cfg[:tareas].each_with_index do |id_tarea, index|
|
||
tarea_actual = todas_las_tareas.find { |t| t[:id] == id_tarea }
|
||
if tarea_actual
|
||
puts "\n#{AMARILLO}--- Paso #{index + 1}/#{comando_cfg[:tareas].length}: #{tarea_actual[:menu_texto]} ---#{RESET}"
|
||
log :info, "Ejecutando tarea '#{id_tarea}' dentro del comando '#{comando_cfg[:id]}'."
|
||
ejecutar_tarea_individual(tarea_actual)
|
||
# Aquí puedes añadir lógica para pausar o continuar automáticamente
|
||
# if !AUTO_AVANZAR_TAREA_MENU # O una configuración específica para comandos
|
||
# puts "\nPresiona Enter para continuar con la siguiente tarea del comando..."
|
||
# $stdin.gets
|
||
# end
|
||
else
|
||
puts "#{ICONO_ERROR} Tarea con ID '#{id_tarea}' no encontrada para el comando '#{comando_cfg[:id]}'. Saltando."
|
||
log :error, "Tarea con ID '#{id_tarea}' no encontrada para el comando '#{comando_cfg[:id]}'."
|
||
end
|
||
end
|
||
puts "\n#{ICONO_OK} Comando '#{comando_cfg[:menu_texto]}' completado."
|
||
log :info, "Comando '#{comando_cfg[:id]}' completado."
|
||
sleep 2
|
||
end
|
||
|
||
def bucle_principal(tareas_config, comandos_config, auto_avanzar)
|
||
|
||
loop do
|
||
opciones_menu = []
|
||
|
||
# Añadir tareas individuales al menú
|
||
tareas_config.each_with_index do |tarea_cfg, index|
|
||
opciones_menu << {
|
||
id: "T#{index + 1}",
|
||
texto: tarea_cfg[:menu_texto],
|
||
tipo: :tarea,
|
||
config: tarea_cfg
|
||
}
|
||
end
|
||
|
||
# Añadir comandos al menú
|
||
comandos_config.each_with_index do |comando_cfg, index|
|
||
opciones_menu << {
|
||
id: "C#{index + 1}",
|
||
texto: comando_cfg[:menu_texto],
|
||
tipo: :comando,
|
||
config: comando_cfg
|
||
}
|
||
end
|
||
|
||
opciones_menu << { id: 'E', texto: "Editar tareas y comandos programados", tipo: :especial }
|
||
opciones_menu << { id: 'S', texto: "Salir del procesador", tipo: :especial }
|
||
|
||
system 'clear' or system 'cls'
|
||
mostrar_encabezado_app
|
||
puts "\n#{CIAN}--- Menú Principal ---#{RESET}"
|
||
|
||
puts "\n#{NEGRITA}Tareas Individuales: #{RESET}"
|
||
opciones_menu.select { |op| op[:tipo] == :tarea }.each do |op|
|
||
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
|
||
end
|
||
|
||
puts "\n#{NEGRITA}Comandos (Secuencias de Tareas): #{RESET}"
|
||
opciones_menu.select { |op| op[:tipo] == :comando }.each do |op|
|
||
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
|
||
end
|
||
|
||
puts "\n#{CIAN}--------------------#{RESET}"
|
||
# Opciones especiales al final
|
||
opciones_menu.select { |op| op[:tipo] == :especial }.each do |op|
|
||
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
|
||
end
|
||
puts "#{CIAN}--------------------#{RESET}"
|
||
|
||
opcion_usuario = leer_entrada_simple("Selecciona una opción > ").strip.upcase
|
||
opcion_encontrada = opciones_menu.find { |op| op[:id].to_s == opcion_usuario }
|
||
|
||
if opcion_encontrada
|
||
case opcion_encontrada[:tipo]
|
||
when :tarea
|
||
ejecutar_tarea_individual(opcion_encontrada[:config])
|
||
when :comando
|
||
ejecutar_comando(opcion_encontrada[:config], tareas_config)
|
||
when :especial
|
||
case opcion_encontrada[:id]
|
||
when 'E'
|
||
modificar_tareas_y_comandos_menu(tareas_config, comandos_config, auto_avanzar)
|
||
# Recargar configuración después de editar
|
||
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
|
||
when 'S'
|
||
puts "Saliendo..."; exit 0
|
||
end
|
||
end
|
||
|
||
# Pausar si no es una opción de edición/salir
|
||
unless ['E', 'S'].include?(opcion_encontrada[:id].to_s) || auto_avanzar
|
||
puts "\nPresiona Enter para continuar..."
|
||
$stdin.gets
|
||
end
|
||
else
|
||
puts "#{ICONO_ERROR} Opción no válida."
|
||
sleep 1
|
||
end
|
||
end
|
||
end
|
||
|
||
# --- Inicio del Script ---
|
||
Signal.trap("INT") { puts "\n#{ROJO}Ctrl+C detectado. Saliendo...#{RESET}"; log :warn, "Script interrumpido (Ctrl+C)."; exit 130; }
|
||
|
||
# === AUTO-SCREEN WRAPPER ===
|
||
# Verificamos si ya estamos dentro de una sesión de GNU Screen
|
||
unless ENV['TERM'] == 'screen' || ENV['TERM'] =~ /^screen/ || ENV['STY']
|
||
puts "\n#{CIAN}ℹ Relanzando dtic-BKPs en una sesión separada (screen) para permitir desatención...#{RESET}"
|
||
|
||
# Construimos el comando original con sus argumentos
|
||
comando_original = [$PROGRAM_NAME] + ARGV
|
||
comando_escapado = comando_original.map { |arg| Shellwords.escape(arg) }.join(' ')
|
||
|
||
# Nombre de sesión dinámico
|
||
nombre_sesion = "dticBKPs_#{Time.now.strftime('%H%M%S')}"
|
||
|
||
puts "Para reconectarte luego, usa: #{AMARILLO}screen -r #{nombre_sesion}#{RESET}"
|
||
sleep 2
|
||
|
||
# Reemplaza el proceso actual por screen ejecutando este mismo script
|
||
exec('screen', '-S', nombre_sesion, 'bash', '-c', "#{comando_escapado}; echo ''; read -p 'Presiona Enter para cerrar la sesión...'")
|
||
end
|
||
|
||
log :info, "===== Inicio script #{APP_NOM} #{APP_VER} (en Screen) ====="
|
||
|
||
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
|
||
|
||
opciones_menu = []
|
||
|
||
# Añadir tareas individuales al menú
|
||
tareas_config.each_with_index do |tarea_cfg, index|
|
||
opciones_menu << {
|
||
id: "T#{index + 1}",
|
||
texto: tarea_cfg[:menu_texto],
|
||
tipo: :tarea,
|
||
config: tarea_cfg
|
||
}
|
||
end
|
||
|
||
# Añadir comandos al menú
|
||
comandos_config.each_with_index do |comando_cfg, index|
|
||
opciones_menu << {
|
||
id: "C#{index + 1}",
|
||
texto: comando_cfg[:menu_texto],
|
||
tipo: :comando,
|
||
config: comando_cfg
|
||
}
|
||
end
|
||
|
||
opciones_menu << { id: 'E', texto: "Editar tareas y comandos programados", tipo: :especial }
|
||
opciones_menu << { id: 'S', texto: "Salir del procesador", tipo: :especial }
|
||
|
||
if ARGV[0]
|
||
opcion_usuario = ARGV[0].strip.upcase
|
||
opcion_encontrada = opciones_menu.find { |op| op[:id].to_s == opcion_usuario }
|
||
|
||
if opcion_encontrada
|
||
case opcion_encontrada[:tipo]
|
||
when :tarea
|
||
ejecutar_tarea_individual(opcion_encontrada[:config])
|
||
when :comando
|
||
ejecutar_comando(opcion_encontrada[:config], tareas_config)
|
||
when :especial
|
||
case opcion_encontrada[:id]
|
||
when 'E'
|
||
modificar_tareas_y_comandos_menu(tareas_config, comandos_config, auto_avanzar)
|
||
# Recargar configuración después de editar
|
||
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
|
||
when 'S'
|
||
puts "Saliendo..."; exit 0
|
||
end
|
||
end
|
||
log :info, "Ejecutado automáticamente desde línea de comandos: #{opcion_usuario}"
|
||
exit 0
|
||
else
|
||
puts "#{ICONO_ERROR} Opción no válida: #{opcion_usuario}"
|
||
log :error, "Opción no válida desde línea de comandos: #{opcion_usuario}"
|
||
exit 1
|
||
end
|
||
else
|
||
bucle_principal(tareas_config, comandos_config, auto_avanzar)
|
||
end |