# frozen_string_literal: true # ========================================================== # 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' module ADN module BKPs 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 # 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] destino = tarea[:destino] @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("Ejecutando → #{cmd}") exito = system(cmd) codigo = $?.exitstatus if exito @log.info("✔ Sincronización rclone completada.") else @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 end