[Tools] ns8-bkps v6.0: refactorización modular de dtic-BKPs (YAML, clases, sin subdir-first)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# ns8-bkps/lib/config.rb — Carga y guardado de config YAML
|
||||
# ==========================================================
|
||||
|
||||
require 'yaml'
|
||||
require 'fileutils'
|
||||
|
||||
module NS8BKPs
|
||||
class Config
|
||||
attr_reader :tareas, :comandos, :auto_avanzar
|
||||
|
||||
def initialize(path)
|
||||
@path = path
|
||||
reload!
|
||||
end
|
||||
|
||||
def reload!
|
||||
unless File.exist?(@path)
|
||||
@tareas = []
|
||||
@comandos = []
|
||||
@auto_avanzar = true
|
||||
return
|
||||
end
|
||||
|
||||
data = YAML.safe_load(File.read(@path), permitted_classes: [Symbol], symbolize_names: true) || {}
|
||||
@auto_avanzar = data[:auto_avanzar] != false
|
||||
@tareas = (data[:tareas] || []).map { |t| simbolizar(t) }
|
||||
@comandos = (data[:comandos] || []).map { |c| simbolizar(c) }
|
||||
end
|
||||
|
||||
def find_tarea(id)
|
||||
@tareas.find { |t| t[:id].to_s == id.to_s }
|
||||
end
|
||||
|
||||
def save!
|
||||
data = {
|
||||
'auto_avanzar' => @auto_avanzar,
|
||||
'tareas' => @tareas.map { |t| stringify(t) },
|
||||
'comandos' => @comandos.map { |c| stringify(c) }
|
||||
}
|
||||
File.write(@path, YAML.dump(data))
|
||||
FileUtils.chmod(0644, @path)
|
||||
end
|
||||
|
||||
def add_tarea(tarea)
|
||||
@tareas << simbolizar(tarea)
|
||||
end
|
||||
|
||||
def remove_tarea(index)
|
||||
@tareas.delete_at(index)
|
||||
end
|
||||
|
||||
def add_comando(comando)
|
||||
@comandos << simbolizar(comando)
|
||||
end
|
||||
|
||||
def remove_comando(index)
|
||||
@comandos.delete_at(index)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def simbolizar(hash)
|
||||
hash.transform_keys(&:to_sym)
|
||||
end
|
||||
|
||||
def stringify(hash)
|
||||
hash.transform_keys(&:to_s).tap do |h|
|
||||
h['tareas'] = h['tareas'].map(&:to_s) if h['tareas']
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# ns8-bkps/lib/logger.rb — Logger con colores y rotación
|
||||
# ==========================================================
|
||||
|
||||
require 'logger'
|
||||
require 'fileutils'
|
||||
|
||||
module NS8BKPs
|
||||
# Colores ANSI
|
||||
module C
|
||||
RESET = "\e[0m"
|
||||
BOLD = "\e[1m"
|
||||
DIM = "\e[2m"
|
||||
RED = "\e[91m"
|
||||
GREEN = "\e[92m"
|
||||
YELLOW = "\e[93m"
|
||||
CYAN = "\e[96m"
|
||||
end
|
||||
|
||||
# Iconos semánticos
|
||||
OK = "#{C::GREEN}✔#{C::RESET}"
|
||||
ERR = "#{C::RED}✖#{C::RESET}"
|
||||
WARN = "#{C::YELLOW}⚠#{C::RESET}"
|
||||
INFO = "#{C::CYAN}ℹ#{C::RESET}"
|
||||
|
||||
class AppLogger
|
||||
def initialize(log_dir)
|
||||
FileUtils.mkdir_p(log_dir)
|
||||
log_path = File.join(log_dir, 'ns8-bkps.log')
|
||||
@file_logger = Logger.new(log_path, 'daily')
|
||||
@file_logger.level = Logger::INFO
|
||||
@file_logger.formatter = proc { |sev, dt, _, msg|
|
||||
"[#{dt.strftime('%Y-%m-%d %H:%M:%S')}] [#{sev}] #{msg}\n"
|
||||
}
|
||||
end
|
||||
|
||||
def info(msg)
|
||||
@file_logger.info(strip_ansi(msg))
|
||||
puts "#{INFO} #{msg}"
|
||||
end
|
||||
|
||||
def warn(msg)
|
||||
@file_logger.warn(strip_ansi(msg))
|
||||
puts "#{WARN} #{msg}"
|
||||
end
|
||||
|
||||
def error(msg)
|
||||
@file_logger.error(strip_ansi(msg))
|
||||
puts "#{ERR} #{msg}"
|
||||
end
|
||||
|
||||
def ok(msg)
|
||||
@file_logger.info(strip_ansi(msg))
|
||||
puts "#{OK} #{msg}"
|
||||
end
|
||||
|
||||
def titulo(msg)
|
||||
@file_logger.info(strip_ansi(msg))
|
||||
puts "\n#{C::BOLD}#{C::CYAN}--- #{msg} ---#{C::RESET}"
|
||||
end
|
||||
|
||||
def paso(msg)
|
||||
@file_logger.info(strip_ansi(msg))
|
||||
puts "#{C::CYAN}→ #{msg}#{C::RESET}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def strip_ansi(s)
|
||||
s.to_s.gsub(/\e\[[\d;]*[mK]/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,174 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# ns8-bkps/lib/menu.rb — Menú TUI interactivo + editor CRUD
|
||||
# ==========================================================
|
||||
|
||||
module NS8BKPs
|
||||
class Menu
|
||||
def initialize(config, dispatcher, log)
|
||||
@config = config
|
||||
@dispatcher = dispatcher
|
||||
@log = log
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
loop do
|
||||
system('clear') || system('cls')
|
||||
mostrar_encabezado
|
||||
opciones = construir_opciones
|
||||
|
||||
opciones.each { |op| puts op[:linea] }
|
||||
puts "#{C::CYAN}────────────────────────#{C::RESET}"
|
||||
|
||||
print "\n#{C::YELLOW}Selecciona > #{C::RESET}"
|
||||
$stdout.flush
|
||||
entrada = $stdin.gets&.chomp&.strip&.upcase
|
||||
next if entrada.nil? || entrada.empty?
|
||||
|
||||
op = opciones.find { |o| o[:id] == entrada }
|
||||
unless op
|
||||
@log.error("Opción no válida: #{entrada}")
|
||||
sleep 1
|
||||
next
|
||||
end
|
||||
|
||||
case op[:tipo]
|
||||
when :tarea
|
||||
@dispatcher.ejecutar_tarea(op[:config])
|
||||
when :comando
|
||||
@dispatcher.ejecutar_comando(op[:config])
|
||||
when :editor
|
||||
editor_interactivo
|
||||
@config.reload!
|
||||
when :salir
|
||||
puts "Saliendo..."
|
||||
exit 0
|
||||
end
|
||||
|
||||
unless [:editor, :salir].include?(op[:tipo]) || @config.auto_avanzar
|
||||
print "\nPresiona Enter para continuar..."
|
||||
$stdin.gets
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mostrar_encabezado
|
||||
puts "#{C::BOLD}#{C::CYAN}╔══════════════════════════════════════════╗#{C::RESET}"
|
||||
puts "#{C::BOLD}#{C::CYAN}║ ns8-bkps — Procesador de Backups v6.0 ║#{C::RESET}"
|
||||
puts "#{C::BOLD}#{C::CYAN}╚══════════════════════════════════════════╝#{C::RESET}"
|
||||
end
|
||||
|
||||
def construir_opciones
|
||||
opciones = []
|
||||
|
||||
puts "\n#{C::BOLD}Tareas Individuales:#{C::RESET}"
|
||||
@config.tareas.each_with_index do |t, i|
|
||||
id = "T#{i + 1}"
|
||||
opciones << { id: id, tipo: :tarea, config: t, linea: " #{C::YELLOW}#{id}.#{C::RESET} #{t[:texto]}" }
|
||||
end
|
||||
|
||||
puts "\n#{C::BOLD}Comandos (Secuencias):#{C::RESET}"
|
||||
@config.comandos.each_with_index do |c, i|
|
||||
id = "C#{i + 1}"
|
||||
opciones << { id: id, tipo: :comando, config: c, linea: " #{C::YELLOW}#{id}.#{C::RESET} #{c[:texto]}" }
|
||||
end
|
||||
|
||||
puts "\n#{C::CYAN}────────────────────────#{C::RESET}"
|
||||
opciones << { id: 'E', tipo: :editor, linea: " #{C::YELLOW}E.#{C::RESET} Editar tareas y comandos" }
|
||||
opciones << { id: 'S', tipo: :salir, linea: " #{C::YELLOW}S.#{C::RESET} Salir" }
|
||||
|
||||
opciones.each { |op| puts op[:linea] if op[:tipo].is_a?(Symbol) && [:editor, :salir].include?(op[:tipo]) }
|
||||
opciones
|
||||
end
|
||||
|
||||
# ─── Editor CRUD ──────────────────────────────────────────────
|
||||
|
||||
def editor_interactivo
|
||||
loop do
|
||||
system('clear') || system('cls')
|
||||
puts "#{C::BOLD}#{C::CYAN}--- Editor de Tareas y Comandos ---#{C::RESET}\n\n"
|
||||
|
||||
puts "#{C::BOLD}Tareas:#{C::RESET}"
|
||||
@config.tareas.each_with_index do |t, i|
|
||||
puts " #{C::YELLOW}T#{i+1}.#{C::RESET} #{t[:texto]} #{C::DIM}(#{t[:id]})#{C::RESET}"
|
||||
end
|
||||
|
||||
puts "\n#{C::BOLD}Comandos:#{C::RESET}"
|
||||
@config.comandos.each_with_index do |c, i|
|
||||
puts " #{C::YELLOW}C#{i+1}.#{C::RESET} #{c[:texto]} #{C::DIM}(#{c[:id]}, tareas: #{c[:tareas]&.join(', ')})#{C::RESET}"
|
||||
end
|
||||
|
||||
puts "\n#{C::CYAN}[Tx]=Editar, [NT]=Nueva Tarea, [BT]=Borrar Tarea, [G]=Guardar y Volver#{C::RESET}"
|
||||
print "> "
|
||||
$stdout.flush
|
||||
op = $stdin.gets&.chomp&.strip&.downcase
|
||||
return if op.nil?
|
||||
|
||||
case op
|
||||
when /^t(\d+)$/
|
||||
idx = $1.to_i - 1
|
||||
editar_tarea(idx) if @config.tareas[idx]
|
||||
when 'nt'
|
||||
nueva_tarea
|
||||
when 'bt'
|
||||
print "Número a borrar (ej: 1): "
|
||||
idx = $stdin.gets&.chomp.to_i - 1
|
||||
@config.remove_tarea(idx) if idx >= 0 && @config.tareas[idx]
|
||||
when 'g'
|
||||
@config.save!
|
||||
@log.ok("Configuración guardada.")
|
||||
sleep 1
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def editar_tarea(idx)
|
||||
tarea = @config.tareas[idx]
|
||||
puts "\n#{C::CYAN}Editando: #{tarea[:texto]}#{C::RESET}"
|
||||
puts "#{C::DIM}(Dejar vacío = mantener valor actual)#{C::RESET}\n"
|
||||
|
||||
tarea.each do |k, v|
|
||||
next if k == :id
|
||||
tipo_hint = [true, false].include?(v) ? " (true/false)" : ""
|
||||
print " #{C::YELLOW}#{k}#{C::RESET}#{tipo_hint} [#{v}]: "
|
||||
$stdout.flush
|
||||
nuevo = $stdin.gets&.chomp&.strip
|
||||
next if nuevo.nil? || nuevo.empty?
|
||||
|
||||
tarea[k] = case v
|
||||
when Symbol then nuevo.to_sym
|
||||
when TrueClass, FalseClass then nuevo.downcase == 'true'
|
||||
else nuevo
|
||||
end
|
||||
end
|
||||
@log.ok("Tarea actualizada.")
|
||||
end
|
||||
|
||||
def nueva_tarea
|
||||
tarea = {
|
||||
id: "tarea_#{Time.now.to_i}",
|
||||
texto: "Nueva Tarea",
|
||||
tipo: "rclone",
|
||||
origen: "",
|
||||
destino: "",
|
||||
eliminar_origen: false,
|
||||
sobrescribir: false
|
||||
}
|
||||
editar_tarea_hash(tarea)
|
||||
@config.add_tarea(tarea)
|
||||
end
|
||||
|
||||
def editar_tarea_hash(tarea)
|
||||
tarea.each do |k, v|
|
||||
print " #{C::YELLOW}#{k}#{C::RESET} [#{v}]: "
|
||||
$stdout.flush
|
||||
nuevo = $stdin.gets&.chomp&.strip
|
||||
tarea[k] = nuevo unless nuevo.nil? || nuevo.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,150 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# ns8-bkps/lib/proc_proxmox.rb — Procesador de backups Proxmox
|
||||
# ==========================================================
|
||||
|
||||
require 'tmpdir'
|
||||
require 'shellwords'
|
||||
require 'open3'
|
||||
require 'fileutils'
|
||||
|
||||
module NS8BKPs
|
||||
module Procesador
|
||||
class Proxmox
|
||||
def initialize(log)
|
||||
@log = log
|
||||
@pv = system('which pv > /dev/null 2>&1')
|
||||
end
|
||||
|
||||
def ejecutar(tarea)
|
||||
@log.titulo(tarea[:texto])
|
||||
@log.info("#{tarea[:id]}: Origen remoto: #{tarea[:origen]}")
|
||||
|
||||
dir_tmp = montar(tarea[:origen], tarea[:id])
|
||||
return false unless dir_tmp
|
||||
|
||||
procesados = 0
|
||||
|
||||
begin
|
||||
logs = Dir.glob(File.join(dir_tmp, '*.log'))
|
||||
if logs.empty?
|
||||
@log.warn("#{tarea[:id]}: Sin archivos .log en origen.")
|
||||
return true
|
||||
end
|
||||
|
||||
@log.info("Encontrados #{logs.length} set(s) de backup Proxmox.")
|
||||
|
||||
logs.each do |ruta_log|
|
||||
base = File.basename(ruta_log, '.log')
|
||||
begin
|
||||
fuentes = Dir.glob(File.join(dir_tmp, "#{base}*"))
|
||||
nombre_vm = extraer_nombre_vm(fuentes, ruta_log, base, tarea[:id])
|
||||
next unless nombre_vm
|
||||
|
||||
nombre_tar = "#{nombre_vm}_#{File.mtime(ruta_log).strftime('%Y%m%d_%H%M%S')}.tar.gz"
|
||||
dir_dest = File.join(tarea[:destino], nombre_vm)
|
||||
FileUtils.mkdir_p(dir_dest)
|
||||
ruta_tar = File.join(dir_dest, nombre_tar)
|
||||
|
||||
if File.exist?(ruta_tar)
|
||||
if tarea[:sobrescribir]
|
||||
@log.warn("#{File.basename(ruta_tar)} existe. Sobrescribiendo.")
|
||||
else
|
||||
@log.warn("#{File.basename(ruta_tar)} existe. Saltando.")
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
@log.paso("Comprimiendo #{base} (VM: #{nombre_vm})")
|
||||
comprimir(dir_tmp, fuentes, ruta_tar)
|
||||
@log.ok("Compresión OK: #{File.basename(ruta_tar)}")
|
||||
|
||||
if tarea[:eliminar_origen]
|
||||
fuentes.each { |f| FileUtils.rm_f(f) }
|
||||
@log.info("Fuentes eliminadas de origen.")
|
||||
end
|
||||
|
||||
procesados += 1
|
||||
rescue => e
|
||||
@log.error("#{tarea[:id]}: Error procesando '#{base}': #{e.message}")
|
||||
FileUtils.rm_f(ruta_tar) if ruta_tar && File.exist?(ruta_tar)
|
||||
end
|
||||
end
|
||||
|
||||
@log.ok("#{procesados} sets Proxmox procesados.")
|
||||
true
|
||||
ensure
|
||||
desmontar(dir_tmp, tarea[:id])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extraer_nombre_vm(fuentes, ruta_log, base, tarea_id)
|
||||
# 1. Intentar .notes
|
||||
notes = fuentes.find { |f| f.end_with?('.notes') }
|
||||
nombre = notes ? File.read(notes).strip : nil
|
||||
|
||||
# 2. Intentar desde el .log (INFO: VM Name: ...)
|
||||
if nombre.nil? || nombre.empty?
|
||||
File.foreach(ruta_log) do |line|
|
||||
if line =~ /INFO: VM Name:\s*(.+)$/
|
||||
nombre = $1.strip
|
||||
break
|
||||
end
|
||||
end rescue nil
|
||||
end
|
||||
|
||||
# 3. Fallback: ID de VM del nombre de archivo
|
||||
if nombre.nil? || nombre.empty?
|
||||
match = base.match(/vzdump-(?:qemu|lxc)-(\d+)-/)
|
||||
nombre = match ? "VM-#{match[1]}" : nil
|
||||
end
|
||||
|
||||
@log.warn("#{tarea_id}: Sin nombre de VM para '#{base}'. Saltando.") unless nombre
|
||||
nombre
|
||||
end
|
||||
|
||||
def comprimir(dir_origen, fuentes, ruta_salida)
|
||||
nombres = fuentes.map { |f| File.basename(f) }
|
||||
if @pv
|
||||
cmd_tar = ['tar', '-cf', '-', '-C', dir_origen] + nombres
|
||||
cmd_pv = ['pv', '-s', fuentes.sum { |f| File.size(f) rescue 0 }.to_s]
|
||||
cmd_gz = ['gzip']
|
||||
File.open(ruta_salida, 'wb') do |f|
|
||||
Open3.pipeline(cmd_tar, cmd_pv, cmd_gz, out: f).each_with_index do |s, i|
|
||||
raise "Falló #{%w[tar pv gzip][i]} (#{s.exitstatus})" unless s.success?
|
||||
end
|
||||
end
|
||||
else
|
||||
args = nombres.map { |f| Shellwords.escape(f) }.join(' ')
|
||||
cmd = "tar -czf #{Shellwords.escape(ruta_salida)} -C #{Shellwords.escape(dir_origen)} #{args}"
|
||||
system(cmd) or raise "tar falló (#{$?.exitstatus})"
|
||||
end
|
||||
end
|
||||
|
||||
def montar(remoto, tarea_id)
|
||||
dir = Dir.mktmpdir("bkps_#{tarea_id}_")
|
||||
@log.info("Montando #{remoto} en #{dir}")
|
||||
pid = spawn("rclone mount #{Shellwords.escape(remoto)} #{dir} --daemon --vfs-cache-mode writes")
|
||||
Process.detach(pid)
|
||||
sleep 2
|
||||
|
||||
if Dir.exist?(dir) && !(Dir.entries(dir) - %w[. ..]).empty?
|
||||
dir
|
||||
else
|
||||
@log.error("Error montando #{remoto}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def desmontar(dir, tarea_id)
|
||||
return unless dir && Dir.exist?(dir)
|
||||
@log.info("Desmontando #{dir}")
|
||||
system("fusermount -u #{dir} 2>/dev/null || umount #{dir} 2>/dev/null")
|
||||
FileUtils.remove_entry(dir) rescue nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# ns8-bkps/lib/proc_xen.rb — Procesador de backups XEN (.xva)
|
||||
# ==========================================================
|
||||
|
||||
require 'shellwords'
|
||||
require 'open3'
|
||||
require 'fileutils'
|
||||
|
||||
module NS8BKPs
|
||||
module Procesador
|
||||
class Xen
|
||||
def initialize(log)
|
||||
@log = log
|
||||
@pv = system('which pv > /dev/null 2>&1')
|
||||
end
|
||||
|
||||
def ejecutar(tarea)
|
||||
@log.titulo(tarea[:texto])
|
||||
@log.info("#{tarea[:id]}: Origen: #{tarea[:origen]}")
|
||||
|
||||
archivos = Dir.glob(File.join(tarea[:origen], '*.xva'))
|
||||
if archivos.empty?
|
||||
@log.warn("#{tarea[:id]}: Sin archivos .xva en origen.")
|
||||
return true
|
||||
end
|
||||
|
||||
@log.info("Encontrados #{archivos.length} archivo(s) .xva.")
|
||||
procesados = 0
|
||||
|
||||
archivos.each do |ruta_xva|
|
||||
nombre_xva = File.basename(ruta_xva)
|
||||
nombre_vm = nombre_xva.split('_').first
|
||||
dir_dest = File.join(tarea[:destino], nombre_vm)
|
||||
FileUtils.mkdir_p(dir_dest)
|
||||
|
||||
nombre_sin_ext = File.basename(nombre_xva, '.xva')
|
||||
ruta_tar = File.join(dir_dest, "#{nombre_sin_ext}.tar.gz")
|
||||
|
||||
if File.exist?(ruta_tar)
|
||||
if tarea[:sobrescribir]
|
||||
@log.warn("#{File.basename(ruta_tar)} existe. Sobrescribiendo.")
|
||||
else
|
||||
@log.warn("#{File.basename(ruta_tar)} existe. Saltando.")
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
@log.paso("Comprimiendo #{nombre_xva}")
|
||||
|
||||
if @pv
|
||||
cmd_tar = ['tar', '-czf', '-', '-C', tarea[:origen], nombre_xva]
|
||||
cmd_pv = ['pv', '-s', File.size(ruta_xva).to_s]
|
||||
File.open(ruta_tar, 'wb') do |f|
|
||||
Open3.pipeline(cmd_tar, cmd_pv, out: f).each_with_index do |s, i|
|
||||
raise "Falló #{%w[tar pv][i]} (#{s.exitstatus})" unless s.success?
|
||||
end
|
||||
end
|
||||
else
|
||||
cmd = "tar -czf #{Shellwords.escape(ruta_tar)} -C #{Shellwords.escape(tarea[:origen])} #{Shellwords.escape(nombre_xva)}"
|
||||
system(cmd) or raise "tar falló (#{$?.exitstatus})"
|
||||
end
|
||||
|
||||
@log.ok("Compresión OK: #{File.basename(ruta_tar)}")
|
||||
|
||||
if tarea[:eliminar_origen]
|
||||
FileUtils.rm_f(ruta_xva)
|
||||
@log.info("XVA original eliminado.")
|
||||
end
|
||||
|
||||
procesados += 1
|
||||
rescue => e
|
||||
@log.error("#{tarea[:id]}: Error comprimiendo #{nombre_xva}: #{e.message}")
|
||||
FileUtils.rm_f(ruta_tar)
|
||||
end
|
||||
end
|
||||
|
||||
@log.ok("#{procesados} archivos .xva procesados.")
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# ns8-bkps/lib/sync_rclone.rb — Sincronización rclone
|
||||
# ==========================================================
|
||||
#
|
||||
# NOTA: Se eliminó la estrategia subdirectories-first que existía
|
||||
# en dtic-BKPs. Con --delete-before rclone ya libera el espacio
|
||||
# remoto antes de transferir, haciendo innecesario el loop por
|
||||
# subdirectorios. El sync root único es suficiente y más eficiente.
|
||||
|
||||
require 'shellwords'
|
||||
|
||||
module NS8BKPs
|
||||
module Procesador
|
||||
class Rclone
|
||||
RCLONE_OPTS = %w[
|
||||
--delete-before
|
||||
--retries 5
|
||||
--retries-sleep 10s
|
||||
--timeout 30m
|
||||
--contimeout 10m
|
||||
--tpslimit 10
|
||||
--progress
|
||||
-v
|
||||
].join(' ').freeze
|
||||
|
||||
def initialize(log)
|
||||
@log = log
|
||||
end
|
||||
|
||||
def ejecutar(tarea)
|
||||
@log.titulo(tarea[:texto])
|
||||
origen = tarea[:origen]
|
||||
destino = tarea[:destino]
|
||||
|
||||
@log.paso("Origen: #{origen}")
|
||||
@log.paso("Destino: #{destino}")
|
||||
|
||||
cmd = "rclone sync #{Shellwords.escape(origen)} #{Shellwords.escape(destino)} #{RCLONE_OPTS}"
|
||||
@log.info("#{tarea[:id]}: Ejecutando → #{cmd}")
|
||||
|
||||
exito = system(cmd)
|
||||
codigo = $?.exitstatus
|
||||
|
||||
if exito
|
||||
@log.ok("Sincronización rclone completada.")
|
||||
else
|
||||
@log.error("#{tarea[:id]}: Fallo en sincronización (código: #{codigo}).")
|
||||
end
|
||||
|
||||
exito
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user