From 6ebf1c30107cd53777edae86f7e9b8b98958afac Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Thu, 9 Apr 2026 21:13:50 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9D=20F2.T1c:=20Atomizaci=C3=B3n=20rcl?= =?UTF-8?q?one=20=E2=80=94=201=20dron=20por=20directorio=20de=20nodo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sync_rclone.rb: listar() + ejecutar_uno() implementados - Soporta origen local (Dir.children) y remoto (rclone lsf) - Orquestador: nota dinámica (Sync vs Comprimir según tipo) - Validado dry-run: T4 (2 dirs), T5 (17 dirs), T6 (2 dirs) - Legacy ejecutar() preservado para compatibilidad --- adn/tools/bkps/bkps.rb | 3 +- adn/tools/bkps/lib/sync_rclone.rb | 107 +++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/adn/tools/bkps/bkps.rb b/adn/tools/bkps/bkps.rb index 713e89c4..1d80e778 100644 --- a/adn/tools/bkps/bkps.rb +++ b/adn/tools/bkps/bkps.rb @@ -841,7 +841,8 @@ module ADN end # Lanzar dron atómico con reintentos - nota = "Comprimir #{arch[:vm]} (#{tamaño_gb}GB)" + verbo = tipo == 'rclone' ? '☁️ Sync' : 'Comprimir' + nota = "#{verbo} #{arch[:vm]} (#{tamaño_gb}GB)" cmd_dron = "#{adn_run} dron lanzar" \ " --nota \"#{nota}\"" \ " --timeout #{timeout}" \ diff --git a/adn/tools/bkps/lib/sync_rclone.rb b/adn/tools/bkps/lib/sync_rclone.rb index 0c16aa50..8311e113 100644 --- a/adn/tools/bkps/lib/sync_rclone.rb +++ b/adn/tools/bkps/lib/sync_rclone.rb @@ -3,6 +3,11 @@ # ========================================================== # adn/tools/bkps/lib/sync_rclone.rb — Sincronización rclone # Migrado desde dtic-BKPs v6.0 → ADN::BKPs +# +# Fase 2: Soporta orquestación 1-dron-por-directorio +# listar(tarea) → lista subdirectorios (nodos) +# ejecutar_uno(tarea, nodo) → sincroniza UN solo directorio +# ejecutar(tarea) → sincroniza TODO el origen (legacy) # ========================================================== require 'shellwords' @@ -26,6 +31,32 @@ module ADN @log = log end + # Scanner: devuelve lista de subdirectorios (nodos) en el origen + # Compatible con el orquestador que espera {archivo:, vm:, tamaño:, pendiente:} + def listar(tarea) + origen = tarea[:origen] + + # Determinar si el origen es local o remoto (rclone remote) + if origen.include?(':') + listar_remoto(tarea) + else + listar_local(tarea) + end + end + + # Atómico: sincroniza UN solo directorio de nodo + def ejecutar_uno(tarea, nombre_directorio) + origen = File.join(tarea[:origen], nombre_directorio, '') + destino = File.join(tarea[:destino], nombre_directorio, '') + + @log.info("☁️ Sync atómico: #{nombre_directorio}") + @log.info(" Origen: #{origen}") + @log.info(" Destino: #{destino}") + + sincronizar(tarea, origen, destino) + end + + # Legacy: sincroniza TODO el origen (compatibilidad con sistema previo) def ejecutar(tarea) @log.info("--- #{tarea[:texto]} ---") origen = tarea[:origen] @@ -34,8 +65,72 @@ module ADN @log.info("Origen: #{origen}") @log.info("Destino: #{destino}") + sincronizar(tarea, origen, destino) + end + + private + + # ─── Listado local ────────────────────────────────────────── + def listar_local(tarea) + origen = tarea[:origen] + destino = tarea[:destino] + + unless Dir.exist?(origen) + @log.error("Directorio origen no existe: #{origen}") + return [] + end + + Dir.children(origen) + .select { |d| File.directory?(File.join(origen, d)) } + .sort + .map do |nombre| + ruta_local = File.join(origen, nombre) + tamaño = calcular_tamaño_dir(ruta_local) + + { + archivo: nombre, # Identificador para ejecutar_uno + vm: nombre, # Nombre visible (nodo) + ruta: ruta_local, + destino: File.join(destino, nombre, ''), + tamaño: tamaño, + pendiente: true # Siempre sincronizar (rclone decide deltas) + } + end + end + + # ─── Listado remoto (rclone lsf) ──────────────────────────── + def listar_remoto(tarea) + origen = tarea[:origen] + destino = tarea[:destino] + + cmd = "rclone lsf #{Shellwords.escape(origen)} --dirs-only 2>/dev/null" + output = `#{cmd}` + + unless $?.success? + @log.error("No se pudo listar remoto: #{origen}") + return [] + end + + output.split("\n") + .map { |d| d.chomp('/').strip } + .reject(&:empty?) + .sort + .map do |nombre| + { + archivo: nombre, + vm: nombre, + ruta: File.join(origen, nombre, ''), + destino: File.join(destino, nombre, ''), + tamaño: 0, # No calculamos tamaño remoto (costoso) + pendiente: true + } + end + end + + # ─── Sincronización ───────────────────────────────────────── + def sincronizar(_tarea, origen, destino) cmd = "rclone sync #{Shellwords.escape(origen)} #{Shellwords.escape(destino)} #{RCLONE_OPTS}" - @log.info("#{tarea[:id]}: Ejecutando → #{cmd}") + @log.info("Ejecutando → #{cmd}") exito = system(cmd) codigo = $?.exitstatus @@ -43,11 +138,19 @@ module ADN if exito @log.info("✔ Sincronización rclone completada.") else - @log.error("#{tarea[:id]}: Fallo en sincronización (código: #{codigo}).") + @log.error("Fallo en sincronización (código: #{codigo}).") end exito end + + # ─── Helpers ──────────────────────────────────────────────── + def calcular_tamaño_dir(ruta) + output = `du -sb #{Shellwords.escape(ruta)} 2>/dev/null`.strip + output.split("\t").first.to_i + rescue + 0 + end end end end