feat(P2603): sistema de bitácoras web dtic-BITACORAs - React 18 + Node.js + PostgreSQL
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# FILE: fxs/config_handler.rb
|
||||
# ==========================================================
|
||||
|
||||
def cargar_configuracion
|
||||
unless File.exist?(APP_CFG_RUTA_ARCHIVO)
|
||||
puts "#{ICONO_WARN} Archivo de configuración de tareas no encontrado: #{APP_CFG_RUTA_ARCHIVO}."
|
||||
puts "#{ICONO_INFO} Usando configuración interna. Los cambios se guardarán en un nuevo archivo."
|
||||
# Si el archivo no existe, devuelve valores predeterminados (vacíos o un ejemplo)
|
||||
return [], [], true # Retorna tareas vacías, comandos vacíos y auto_avanzar en true
|
||||
end
|
||||
|
||||
begin
|
||||
# Carga el archivo de configuración. Esto definirá TAREAS_CONFIG y COMANDOS_CONFIG
|
||||
load APP_CFG_RUTA_ARCHIVO
|
||||
|
||||
unless defined?(TAREAS_CONFIG) && TAREAS_CONFIG.is_a?(Array)
|
||||
puts "#{ICONO_ERROR} TAREAS_CONFIG no está definida correctamente en '#{APP_CFG_RUTA_ARCHIVO}'."
|
||||
exit 1
|
||||
end
|
||||
|
||||
unless defined?(COMANDOS_CONFIG) && COMANDOS_CONFIG.is_a?(Array)
|
||||
puts "#{ICONO_ERROR} COMANDOS_CONFIG no está definida correctamente en '#{APP_CFG_RUTA_ARCHIVO}'."
|
||||
exit 1
|
||||
end
|
||||
|
||||
auto_avanzar = defined?(AUTO_AVANZAR_TAREA_MENU) ? AUTO_AVANZAR_TAREA_MENU : true
|
||||
return TAREAS_CONFIG, COMANDOS_CONFIG, auto_avanzar
|
||||
rescue LoadError, SyntaxError => e
|
||||
puts "#{ICONO_ERROR} Error cargando configuración desde '#{APP_CFG_RUTA_ARCHIVO}':\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def guardar_configuracion(tareas, comandos, auto_avanzar)
|
||||
begin
|
||||
File.open(APP_CFG_RUTA_ARCHIVO, 'w') do |f|
|
||||
f.puts "# frozen_string_literal: true\n"
|
||||
f.puts "# =========================================================="
|
||||
f.puts "# Archivo de Configuración para #{APP_NOM_BASE}"
|
||||
f.puts "# Generado automáticamente el #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
f.puts "# ==========================================================\n"
|
||||
f.puts "AUTO_AVANZAR_TAREA_MENU = #{auto_avanzar}\n"
|
||||
|
||||
f.puts "TAREAS_CONFIG = ["
|
||||
tareas.each do |tarea|
|
||||
f.puts " {"
|
||||
tarea.each { |key, value| f.puts " #{key}: #{value.inspect}," }
|
||||
f.puts " },"
|
||||
end
|
||||
f.puts "]\n"
|
||||
|
||||
f.puts "COMANDOS_CONFIG = ["
|
||||
comandos.each do |comando|
|
||||
f.puts " {"
|
||||
comando.each do |key, value|
|
||||
if key == :tareas
|
||||
f.puts " #{key}: [#{value.map(&:inspect).join(', ')}],"
|
||||
else
|
||||
f.puts " #{key}: #{value.inspect},"
|
||||
end
|
||||
end
|
||||
f.puts " },"
|
||||
end
|
||||
f.puts "]"
|
||||
end
|
||||
puts "\n#{ICONO_OK} Configuración de tareas y comandos guardada en '#{APP_CFG_NOM_ARCHIVO}'."
|
||||
log :info, "Configuración de tareas y comandos actualizada y guardada por el usuario."
|
||||
sleep 1.5
|
||||
return true
|
||||
rescue StandardError => e
|
||||
puts "\n#{ICONO_ERROR} No se pudo guardar la configuración: #{e.message}"
|
||||
log :error, "Fallo al guardar configuración de tareas y comandos: #{e.message}"
|
||||
sleep 2
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,209 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs - Automatic Backup Processor
|
||||
# ----------------------------------------------------------
|
||||
# APP: dtic-BKPs
|
||||
# FILE: fxs/proc_syncPmox.rb
|
||||
# VERSION: v5.5.1
|
||||
# AUTHOR: Ricardo MONLA (rmonla@)
|
||||
# LICENSE: MIT License
|
||||
# ==========================================================
|
||||
|
||||
require 'tmpdir'
|
||||
require 'shellwords'
|
||||
require 'open3'
|
||||
require 'fileutils'
|
||||
|
||||
# Carga las constantes, logger y helpers globales
|
||||
# require_relative '../core/globals'
|
||||
|
||||
# --- Helpers para montaje rclone ---
|
||||
def montar_rclone(origen_remote, tarea_id)
|
||||
dir_tmp = Dir.mktmpdir("bkps_#{tarea_id}_")
|
||||
log :info, "#{tarea_id}: Montando origen remoto #{origen_remote} en #{dir_tmp}"
|
||||
|
||||
# Ejecutar en background
|
||||
pid = spawn("rclone mount #{Shellwords.escape(origen_remote)} #{dir_tmp} --daemon --vfs-cache-mode writes")
|
||||
Process.detach(pid)
|
||||
|
||||
# Esperar un poco a que monte
|
||||
sleep 2
|
||||
|
||||
if Dir.exist?(dir_tmp) && !(Dir.entries(dir_tmp) - %w[. ..]).empty?
|
||||
return dir_tmp
|
||||
else
|
||||
msg = "#{tarea_id}: Error montando remote #{origen_remote}"
|
||||
log :error, msg
|
||||
puts "#{ICONO_ERROR} #{msg}"
|
||||
sleep 5
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def desmontar_rclone(dir_tmp, tarea_id)
|
||||
return unless dir_tmp && Dir.exist?(dir_tmp)
|
||||
log :info, "#{tarea_id}: Desmontando #{dir_tmp}"
|
||||
system("fusermount -u #{dir_tmp} 2>/dev/null || umount #{dir_tmp} 2>/dev/null")
|
||||
FileUtils.remove_entry(dir_tmp) if Dir.exist?(dir_tmp)
|
||||
end
|
||||
|
||||
# --- Función Principal del Procesador Proxmox ---
|
||||
def procesar_backups_tipo_proxmox(tarea_cfg)
|
||||
system 'clear' or system 'cls'
|
||||
|
||||
# 1. --- Extraer configuración e inicializar variables ---
|
||||
origen_remote = tarea_cfg[:origen]
|
||||
dir_destino_base = tarea_cfg[:destino]
|
||||
eliminar_original = tarea_cfg[:eliminar_origen]
|
||||
sobrescribir_destino = tarea_cfg[:sobrescribir_destino]
|
||||
backups_procesados_llamada = 0
|
||||
|
||||
puts "\n#{NEGRITA}#{CIAN}--- #{tarea_cfg[:menu_texto]} ---#{RESET}"
|
||||
log :info, "#{tarea_cfg[:id]}: Iniciado. Origen remoto: #{origen_remote}, Eliminar: #{eliminar_original}, Sobrescribir: #{sobrescribir_destino}"
|
||||
|
||||
# 2. --- Montar origen remoto ---
|
||||
dir_origen = montar_rclone(origen_remote, tarea_cfg[:id])
|
||||
return unless dir_origen
|
||||
|
||||
begin
|
||||
@pv_disponible ||= !`which pv`.to_s.strip.empty?
|
||||
unless @pv_disponible
|
||||
if @pv_notificado.nil?
|
||||
msg = "#{tarea_cfg[:id]}: Comando 'pv' no encontrado. Compresión sin barra de progreso."
|
||||
log :warn, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
@pv_notificado = true
|
||||
end
|
||||
end
|
||||
|
||||
# 3. --- Buscar archivos .log ---
|
||||
archivos_log = Dir.glob(File.join(dir_origen, '*.log'))
|
||||
|
||||
if archivos_log.empty?
|
||||
msg = "#{tarea_cfg[:id]}: No se encontraron archivos .log en '#{dir_origen}'."
|
||||
log :warn, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
sleep 5
|
||||
$estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { archivos: 0, estado: "OK (Sin archivos)" }
|
||||
return
|
||||
end
|
||||
|
||||
puts "#{ICONO_INFO} Encontrados #{archivos_log.length} set(s) de backup Proxmox a procesar."
|
||||
|
||||
# 4. --- Iterar y procesar cada set ---
|
||||
archivos_log.each do |ruta_log|
|
||||
nombre_base_log = File.basename(ruta_log, '.log')
|
||||
begin
|
||||
archivos_fuente_del_backup = Dir.glob(File.join(dir_origen, "#{nombre_base_log}*"))
|
||||
|
||||
ruta_notes = archivos_fuente_del_backup.find { |f| f.end_with?('.notes') }
|
||||
nombre_vm = nil
|
||||
|
||||
if ruta_notes
|
||||
nombre_vm = File.read(ruta_notes).strip
|
||||
end
|
||||
|
||||
if nombre_vm.nil? || nombre_vm.empty?
|
||||
# Fallback 1: Extraer el nombre de la VM desde dentro del propio archivo .log
|
||||
begin
|
||||
File.foreach(ruta_log) do |line|
|
||||
if line =~ /INFO: VM Name:\s*(.+)$/
|
||||
nombre_vm = $1.strip
|
||||
log :info, "#{tarea_cfg[:id]}: Nombre '#{nombre_vm}' extraído del log '#{nombre_base_log}'."
|
||||
break
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
log :warn, "#{tarea_cfg[:id]}: Error leyendo log para extraer VM Name: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
if nombre_vm.nil? || nombre_vm.empty?
|
||||
# Fallback 2: extraemos el ID de la VM del nombre de archivo (ej. vzdump-qemu-101-...)
|
||||
regex_match = nombre_base_log.match(/vzdump-(?:qemu|lxc)-(\d+)-/)
|
||||
if regex_match
|
||||
nombre_vm = "VM-#{regex_match[1]}"
|
||||
log :info, "#{tarea_cfg[:id]}: Sin .notes ni nombre en log para '#{nombre_base_log}'. Respaldo asignado a: #{nombre_vm}"
|
||||
else
|
||||
log :warn, "#{tarea_cfg[:id]}: No se pudo determinar nombre de VM para '#{nombre_base_log}'. Saltando."
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
nombre_final_tar = "#{nombre_vm}_#{File.mtime(ruta_log).strftime('%Y%m%d_%H%M%S')}.tar.gz"
|
||||
dir_destino_vm = File.join(dir_destino_base, nombre_vm)
|
||||
FileUtils.mkdir_p(dir_destino_vm)
|
||||
ruta_salida_tar = File.join(dir_destino_vm, nombre_final_tar)
|
||||
|
||||
if File.exist?(ruta_salida_tar)
|
||||
if sobrescribir_destino
|
||||
msg = "#{tarea_cfg[:id]}: Destino '#{File.basename(ruta_salida_tar)}' existe. Se sobrescribirá."
|
||||
log :info, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
else
|
||||
msg = "#{tarea_cfg[:id]}: Destino '#{File.basename(ruta_salida_tar)}' existe. No se sobrescribe. Saltando."
|
||||
log :warn, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# --- Realizar compresión ---
|
||||
puts "#{CIAN}→ Comprimiendo set #{NEGRITA}#{nombre_base_log}#{RESET} (VM: #{nombre_vm}) a #{File.basename(ruta_salida_tar)}..."
|
||||
log :info, "#{tarea_cfg[:id]}: Comprimiendo Proxmox set '#{nombre_vm}' (base: #{nombre_base_log})"
|
||||
|
||||
nombres_para_tar = archivos_fuente_del_backup.map { |f| File.basename(f) }
|
||||
|
||||
if @pv_disponible
|
||||
cmd_tar = ['tar', '-cf', '-', '-C', dir_origen] + nombres_para_tar
|
||||
cmd_pv = ['pv', '-s', archivos_fuente_del_backup.sum { |f| File.size(f) rescue 0 }.to_s]
|
||||
cmd_gzip = ['gzip']
|
||||
|
||||
File.open(ruta_salida_tar, 'wb') do |f_out|
|
||||
Open3.pipeline(cmd_tar, cmd_pv, cmd_gzip, out: f_out).each_with_index do |status, i|
|
||||
unless status.success?
|
||||
cmd_name = ['tar', 'pv', 'gzip'][i]
|
||||
raise "Error en compresión (falló #{cmd_name} con código: #{status.exitstatus})"
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
archivos_escapados = nombres_para_tar.map { |f| Shellwords.escape(f) }.join(' ')
|
||||
cmd_tar_directo = "tar -czf #{Shellwords.escape(ruta_salida_tar)} -C #{Shellwords.escape(dir_origen)} #{archivos_escapados}"
|
||||
system(cmd_tar_directo)
|
||||
raise "Error en compresión (falló tar con código: #{$?.exitstatus})" unless $?.success?
|
||||
end
|
||||
|
||||
# --- Finalizar ---
|
||||
log :info, "#{tarea_cfg[:id]}: Compresión OK → #{ruta_salida_tar}"
|
||||
puts "\n#{ICONO_OK} Compresión OK: #{File.basename(ruta_salida_tar)}"
|
||||
|
||||
if eliminar_original
|
||||
log :info, "#{tarea_cfg[:id]}: Eliminando fuentes Proxmox '#{nombre_base_log}'..."
|
||||
archivos_fuente_del_backup.each { |f| FileUtils.rm_f(f) }
|
||||
end
|
||||
|
||||
backups_procesados_llamada += 1
|
||||
|
||||
rescue StandardError => e
|
||||
err_msg = "#{tarea_cfg[:id]}: Error procesando set '#{nombre_base_log}': #{e.message}"
|
||||
log :error, err_msg
|
||||
puts "\n#{ICONO_ERROR} #{err_msg}"
|
||||
sleep 5
|
||||
FileUtils.rm_f(ruta_salida_tar) if ruta_salida_tar
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# 5. --- Reportar estadísticas finales ---
|
||||
$estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { archivos: backups_procesados_llamada, estado: "OK" }
|
||||
log :info, "#{tarea_cfg[:id]}: #{backups_procesados_llamada} sets Proxmox procesados."
|
||||
puts "#{ICONO_OK} Tarea '#{tarea_cfg[:menu_texto]}' completada. Procesados: #{backups_procesados_llamada}."
|
||||
sleep 1.5
|
||||
|
||||
ensure
|
||||
# Siempre desmontar aunque haya error
|
||||
desmontar_rclone(dir_origen, tarea_cfg[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs - Automatic Backup Processor
|
||||
# ----------------------------------------------------------
|
||||
# APP: dtic-BKPs
|
||||
# FILE: fxs/proc_syncXEN.rb
|
||||
# VERSION: v5.5.1
|
||||
# AUTHOR: Ricardo MONLA (rmonla@)
|
||||
# LICENSE: MIT License
|
||||
# ==========================================================
|
||||
|
||||
# --- Función Principal del Procesador XVA ---
|
||||
def procesar_backups_xva(tarea_cfg)
|
||||
system 'clear' or system 'cls'
|
||||
# 1. --- Extraer configuración y inicializar variables ---
|
||||
dir_origen = tarea_cfg[:origen]
|
||||
dir_destino_base = tarea_cfg[:destino]
|
||||
eliminar_original = tarea_cfg[:eliminar_origen]
|
||||
sobrescribir_destino = tarea_cfg[:sobrescribir_destino]
|
||||
archivos_procesados_llamada = 0
|
||||
|
||||
puts "\n#{NEGRITA}#{CIAN}--- #{tarea_cfg[:menu_texto]} ---#{RESET}"
|
||||
log :info, "#{tarea_cfg[:id]}: Iniciado desde #{dir_origen}. Eliminar: #{eliminar_original}, Sobrescribir: #{sobrescribir_destino}"
|
||||
|
||||
@pv_disponible ||= !`which pv`.to_s.strip.empty?
|
||||
unless @pv_disponible
|
||||
if @pv_notificado.nil?
|
||||
msg = "#{tarea_cfg[:id]}: Comando 'pv' no encontrado. Compresión sin barra de progreso."
|
||||
log :warn, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
@pv_notificado = true
|
||||
end
|
||||
end
|
||||
|
||||
# 2. --- Buscar archivos a procesar ---
|
||||
archivos_disponibles = Dir.glob(File.join(dir_origen, '*.xva'))
|
||||
|
||||
if archivos_disponibles.empty?
|
||||
msg = "#{tarea_cfg[:id]}: No hay archivos .xva en '#{dir_origen}'."
|
||||
log :warn, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
$estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { archivos: 0, estado: "OK (Sin archivos)" }
|
||||
return
|
||||
end
|
||||
|
||||
puts "#{ICONO_INFO} Encontrados #{archivos_disponibles.length} archivo(s) .xva a procesar."
|
||||
|
||||
# 3. --- Iterar y procesar cada archivo ---
|
||||
archivos_disponibles.each do |ruta_completa_xva_origen|
|
||||
nombre_base_xva = File.basename(ruta_completa_xva_origen)
|
||||
nombre_vm = nombre_base_xva.split('_').first
|
||||
dir_dest_especifico_vm = File.join(dir_destino_base, nombre_vm)
|
||||
|
||||
begin
|
||||
FileUtils.mkdir_p(dir_dest_especifico_vm)
|
||||
rescue StandardError => e
|
||||
log :error, "#{tarea_cfg[:id]}: Error creando dir '#{dir_dest_especifico_vm}': #{e.message}"
|
||||
next
|
||||
end
|
||||
|
||||
nombre_sin_ext = File.basename(nombre_base_xva, '.xva')
|
||||
ruta_salida_comp = File.join(dir_dest_especifico_vm, "#{nombre_sin_ext}.tar.gz")
|
||||
|
||||
if File.exist?(ruta_salida_comp)
|
||||
if sobrescribir_destino
|
||||
msg = "#{tarea_cfg[:id]}: Destino '#{File.basename(ruta_salida_comp)}' existe. Se sobrescribirá."
|
||||
log :info, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
else
|
||||
msg = "#{tarea_cfg[:id]}: Destino '#{File.basename(ruta_salida_comp)}' existe. No se sobrescribe. Saltando."
|
||||
log :warn, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# 4. --- Realizar la compresión ---
|
||||
puts "#{CIAN}→ Comprimiendo #{NEGRITA}#{nombre_base_xva}#{RESET} a #{File.basename(ruta_salida_comp)}..."
|
||||
log :info, "#{tarea_cfg[:id]}: Comprimiendo XVA #{nombre_base_xva} → #{File.basename(ruta_salida_comp)}"
|
||||
|
||||
begin
|
||||
if @pv_disponible
|
||||
cmd_tar = ['tar', '-czf', '-', '-C', dir_origen, nombre_base_xva]
|
||||
cmd_pv = ['pv', '-s', File.size(ruta_completa_xva_origen).to_s]
|
||||
|
||||
File.open(ruta_salida_comp, 'wb') do |f_out|
|
||||
Open3.pipeline(cmd_tar, cmd_pv, out: f_out).each_with_index do |status, i|
|
||||
unless status.success?
|
||||
cmd_name = i == 0 ? 'tar' : 'pv'
|
||||
raise "Error en compresión (falló #{cmd_name} con código: #{status.exitstatus})"
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
cmd_tar_directo = "tar -czf #{Shellwords.escape(ruta_salida_comp)} -C #{Shellwords.escape(dir_origen)} #{Shellwords.escape(nombre_base_xva)}"
|
||||
system(cmd_tar_directo)
|
||||
raise "Error en compresión (falló tar con código: #{$?.exitstatus})" unless $?.success?
|
||||
end
|
||||
|
||||
# 5. --- Finalizar y limpiar ---
|
||||
log :info, "#{tarea_cfg[:id]}: Compresión OK → #{ruta_salida_comp}"
|
||||
puts "\n#{ICONO_OK} Compresión OK: #{File.basename(ruta_salida_comp)}"
|
||||
|
||||
if eliminar_original
|
||||
log :info, "#{tarea_cfg[:id]}: Eliminando XVA original #{nombre_base_xva}"
|
||||
FileUtils.rm_f(ruta_completa_xva_origen)
|
||||
end
|
||||
|
||||
archivos_procesados_llamada += 1
|
||||
|
||||
rescue StandardError => e
|
||||
err_msg = "#{tarea_cfg[:id]}: Error al comprimir #{nombre_base_xva}: #{e.message}"
|
||||
log :error, err_msg
|
||||
puts "\n#{ICONO_ERROR} #{err_msg}"
|
||||
FileUtils.rm_f(ruta_salida_comp)
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# 6. --- Reportar estadísticas finales ---
|
||||
$estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { archivos: archivos_procesados_llamada, estado: "OK" }
|
||||
log :info, "#{tarea_cfg[:id]}: #{archivos_procesados_llamada} archivos .xva procesados"
|
||||
puts "#{ICONO_OK} Tarea '#{tarea_cfg[:menu_texto]}' completada. Procesados: #{archivos_procesados_llamada}."
|
||||
sleep 1.5
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs - Automatic Backup Processor
|
||||
# ----------------------------------------------------------
|
||||
# APP: dtic-BKPs
|
||||
# FILE: fxs/sync_rclone.rb
|
||||
# VERSION: v5.5.1
|
||||
# AUTHOR: Ricardo MONLA (rmonla@)
|
||||
# LICENSE: MIT License
|
||||
# ==========================================================
|
||||
|
||||
# Carga las constantes y helpers globales
|
||||
# require_relative '../core/globals'
|
||||
|
||||
def sincronizar_rclone(tarea_cfg)
|
||||
system 'clear' or system 'cls'
|
||||
origen = tarea_cfg[:origen]
|
||||
destino = tarea_cfg[:destino]
|
||||
|
||||
puts "\n#{NEGRITA}#{CIAN}--- #{tarea_cfg[:menu_texto]} ---#{RESET}"
|
||||
log :info, "#{tarea_cfg[:id]}: Iniciado rclone sync. Origen: #{origen}, Destino: #{destino}"
|
||||
|
||||
# Get all subdirectories recursively
|
||||
subdirs = Dir.glob("#{origen}/**/*").select { |f| File.directory?(f) }.map { |d| d.sub("#{origen}/", '') }
|
||||
|
||||
all_exito = true
|
||||
|
||||
# Opciones base de rclone:
|
||||
# --delete-before: Borra origen del remoto ANTES de transferir, liberando espacio clave para respaldos grandes (Code 7 limits)
|
||||
# --retries 5 etc: Blindaje de conexión para archivos gigantescos hacia la nube.
|
||||
rclone_ops = "--delete-before --retries 5 --retries-sleep 10s --timeout 30m --contimeout 10m --tpslimit 10"
|
||||
|
||||
# Execute rclone for each subdirectory
|
||||
subdirs.each do |subdir|
|
||||
comando_sub = "rclone sync #{Shellwords.escape(File.join(origen, subdir))} #{Shellwords.escape(File.join(destino, subdir))} --progress -v #{rclone_ops}"
|
||||
log :info, "#{tarea_cfg[:id]}: Ejecutando subdir #{subdir} → #{comando_sub}"
|
||||
puts "#{CIAN}→ Ejecutando rclone sync subdir (#{subdir})#{RESET}"
|
||||
|
||||
exito_sub = system(comando_sub)
|
||||
if !exito_sub
|
||||
log :error, "#{tarea_cfg[:id]}: Fallo en sincronización subdir #{subdir}"
|
||||
puts "#{ICONO_ERROR} Fallo sincronización rclone subdir (#{subdir})."
|
||||
end
|
||||
all_exito &= exito_sub
|
||||
end
|
||||
|
||||
# Execute rclone for the root directory
|
||||
comando_rclone = "rclone sync #{Shellwords.escape(origen)} #{Shellwords.escape(destino)} --progress -v #{rclone_ops}"
|
||||
log :info, "#{tarea_cfg[:id]}: Ejecutando root → #{comando_rclone}"
|
||||
puts "#{CIAN}→ Ejecutando rclone sync root (Origen: #{origen} → Destino: #{destino})#{RESET}"
|
||||
|
||||
exito = system(comando_rclone)
|
||||
all_exito &= exito
|
||||
codigo_salida = $?.exitstatus
|
||||
estado_final = all_exito ? "Éxito" : "Fallo (cód: #{codigo_salida})"
|
||||
|
||||
$estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { estado: estado_final }
|
||||
|
||||
if all_exito
|
||||
log :info, "#{tarea_cfg[:id]}: Sincronización OK"
|
||||
puts "#{ICONO_OK} Sincronización rclone OK."
|
||||
else
|
||||
log :error, "#{tarea_cfg[:id]}: Fallo en sincronización (#{estado_final})"
|
||||
puts "#{ICONO_ERROR} Fallo sincronización rclone (#{estado_final})."
|
||||
end
|
||||
|
||||
sleep 1.5
|
||||
all_exito
|
||||
end
|
||||
@@ -0,0 +1,119 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# FILE: fxs/ui_handlers.rb
|
||||
# ==========================================================
|
||||
|
||||
def leer_entrada_simple(prompt)
|
||||
print prompt; $stdout.flush; $stdin.gets.chomp
|
||||
end
|
||||
|
||||
def mostrar_encabezado_app
|
||||
puts "#{NEGRITA}#{CIAN}--- dtic-BKPs - Procesador de Backups #{APP_VER} ---#{RESET}"
|
||||
end
|
||||
|
||||
def editar_item_config(item, type_name)
|
||||
system 'clear' or system 'cls'
|
||||
puts "#{NEGRITA}#{CIAN}--- Editando #{type_name}: #{item[:id]} ---#{RESET}"
|
||||
puts "Deja el campo en blanco para mantener el valor actual."
|
||||
|
||||
item.each do |key, value|
|
||||
next if key == :id # ID no se edita directamente
|
||||
|
||||
if key == :tareas && type_name == "Comando"
|
||||
current_tasks = value.map(&:to_s).join(', ')
|
||||
nuevo_valor_str = leer_entrada_simple("#{AMARILLO}#{key}#{RESET} (IDs de tareas separados por coma) [Actual: #{current_tasks}] > ").strip
|
||||
unless nuevo_valor_str.empty?
|
||||
item[key] = nuevo_valor_str.split(',').map(&:strip).map(&:to_sym).uniq
|
||||
end
|
||||
else
|
||||
tipo = value.is_a?(TrueClass) || value.is_a?(FalseClass) ? "(true/false)" : ""
|
||||
nuevo_valor = leer_entrada_simple("#{AMARILLO}#{key}#{RESET} #{tipo} [Actual: #{value.inspect}] > ").strip
|
||||
unless nuevo_valor.empty?
|
||||
if value.is_a?(Symbol)
|
||||
item[key] = nuevo_valor.to_sym
|
||||
elsif tipo == "(true/false)"
|
||||
item[key] = nuevo_valor.downcase == 'true'
|
||||
else
|
||||
item[key] = nuevo_valor
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
puts "#{ICONO_OK} #{type_name} actualizada."
|
||||
sleep 1
|
||||
end
|
||||
|
||||
def modificar_tareas_y_comandos_menu(tareas_actuales, comandos_actuales, auto_avanzar_actual)
|
||||
loop do
|
||||
system 'clear' or system 'cls'
|
||||
puts "#{NEGRITA}#{CIAN}--- Editor de Tareas y Comandos ---#{RESET}"
|
||||
|
||||
puts "\n#{NEGRITA}Tareas Individuales: #{RESET}"
|
||||
tareas_actuales.each_with_index do |tarea, index|
|
||||
puts "#{AMARILLO}T#{index + 1}.#{RESET} #{tarea[:menu_texto]} (#{AMARILLO}id: :#{tarea[:id]}#{RESET})"
|
||||
end
|
||||
|
||||
puts "\n#{NEGRITA}Comandos: #{RESET}"
|
||||
comandos_actuales.each_with_index do |comando, index|
|
||||
puts "#{AMARILLO}C#{index + 1}.#{RESET} #{comando[:menu_texto]} (#{AMARILLO}id: :#{comando[:id]}#{RESET}, tareas: #{comando[:tareas].join(', ')}#{RESET})"
|
||||
end
|
||||
|
||||
puts "#{CIAN}-------------------------#{RESET}"
|
||||
prompt = "[Tx]=Editar Tarea, [Cx]=Editar Comando, [NT]=Nueva Tarea, [NC]=Nuevo Comando, [BT]=Borrar Tarea, [BC]=Borrar Comando, [G]=Guardar y Volver > "
|
||||
opcion = leer_entrada_simple(prompt).downcase
|
||||
|
||||
case opcion
|
||||
when /^t(\d+)$/
|
||||
index = $1.to_i - 1
|
||||
editar_item_config(tareas_actuales[index], "Tarea") if tareas_actuales[index]
|
||||
when /^c(\d+)$/
|
||||
index = $1.to_i - 1
|
||||
editar_item_config(comandos_actuales[index], "Comando") if comandos_actuales[index]
|
||||
when 'nt'
|
||||
# Generar un ID único para la nueva tarea
|
||||
new_id = "nueva_tarea_#{Time.now.to_i}".to_sym
|
||||
nueva_tarea = {
|
||||
id: new_id,
|
||||
menu_texto: "Nueva Tarea (#{new_id})",
|
||||
tipo_proceso: :rclone_sync,
|
||||
origen: "",
|
||||
destino: "",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false
|
||||
}
|
||||
editar_item_config(nueva_tarea, "Nueva Tarea")
|
||||
tareas_actuales << nueva_tarea
|
||||
when 'nc'
|
||||
# Generar un ID único para el nuevo comando
|
||||
new_id = "nuevo_comando_#{Time.now.to_i}".to_sym
|
||||
nuevo_comando = {
|
||||
id: new_id,
|
||||
menu_texto: "Nuevo Comando (#{new_id})",
|
||||
tareas: [] # Se pueden añadir tareas después
|
||||
}
|
||||
editar_item_config(nuevo_comando, "Nuevo Comando")
|
||||
comandos_actuales << nuevo_comando
|
||||
when 'bt'
|
||||
index_b = leer_entrada_simple("Número de tarea a borrar (Ej: T1) > ").downcase.gsub('t', '').to_i - 1
|
||||
if tareas_actuales[index_b]
|
||||
puts "#{ICONO_WARN} Borrando tarea '#{tareas_actuales[index_b][:menu_texto]}'."
|
||||
tareas_actuales.delete_at(index_b)
|
||||
sleep 1
|
||||
end
|
||||
when 'bc'
|
||||
index_b = leer_entrada_simple("Número de comando a borrar (Ej: C1) > ").downcase.gsub('c', '').to_i - 1
|
||||
if comandos_actuales[index_b]
|
||||
puts "#{ICONO_WARN} Borrando comando '#{comandos_actuales[index_b][:menu_texto]}'."
|
||||
comandos_actuales.delete_at(index_b)
|
||||
sleep 1
|
||||
end
|
||||
when 'g'
|
||||
guardar_configuracion(tareas_actuales, comandos_actuales, auto_avanzar_actual)
|
||||
return
|
||||
else
|
||||
puts "#{ICONO_ERROR} Opción no válida."
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user