#!/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