Files
dtic-DIIAA/tools/adn/subcomando_sync.rb
T

277 lines
9.9 KiB
Ruby

# frozen_string_literal: true
# tools/adn/subcomando_sync.rb — Subcomando para sincronizar archivos MD con API dtic-BITACORAs
# =============================================================================================
# Este subcomando permite sincronizar bitácoras en formato Markdown con la base de datos
# de dtic-BITACORAs mediante la API REST.
require_relative 'clients/dtic_bitacoras_api'
require 'optparse'
require 'fileutils'
module ADN
class SubcomandoSync
def initialize(args, logger)
@args = args
@logger = logger
@opciones = {
sobrescribir: false,
validar_primero: true,
batch: false,
fecha: nil,
ruta: nil
}
@resultados = {
archivos_procesados: 0,
archivos_exitosos: 0,
archivos_fallidos: 0,
entradas_sincronizadas: 0,
errores: []
}
end
def ejecutar
parser = OptionParser.new do |opts|
opts.banner = "Uso: ruby tools/adn.rb sync [OPCIONES] [RUTA_O_FECHA]"
opts.separator ""
opts.separator "Sincroniza archivos Markdown con la API de dtic-BITACORAs"
opts.separator ""
opts.separator "Opciones:"
opts.on("--sobrescribir", "Sobrescribir entradas existentes en la base de datos") do
@opciones[:sobrescribir] = true
end
opts.on("--no-validar", "No validar archivos antes de sincronizar") do
@opciones[:validar_primero] = false
end
opts.on("--batch", "Sincronizar todos los archivos en el directorio bitacoras/") do
@opciones[:batch] = true
end
opts.on("--fecha FECHA", "Sincronizar bitácora específica (formato: YYYY-MM-DD)") do |f|
@opciones[:fecha] = f
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit
end
end
begin
parser.parse!(@args)
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts parser
exit 1
end
# Determinar qué sincronizar
if @args.any? && !@opciones[:batch] && !@opciones[:fecha]
@opciones[:ruta] = @args[0]
end
ejecutar_sincronizacion
end
private
def ejecutar_sincronizacion
@logger.info("Iniciando sincronización con API dtic-BITACORAs", @opciones)
# Inicializar cliente API
begin
@client = DticBitacorasApi::Client.new(logger: @logger)
salud = @client.salud
@logger.exito("API dtic-BITACORAs disponible: #{salud['status']}")
rescue DticBitacorasApi::ApiError => e
@logger.error("No se puede conectar a la API: #{e.message}")
mostrar_conexion_fallback
exit 1
rescue DticBitacorasApi::ConfigError => e
@logger.error("Error de configuración: #{e.message}")
puts "Configura la URL de la API en tools/adn/config.yml o con export DTIC_API_URL=http://localhost:3002"
exit 1
end
# Determinar archivos a sincronizar
archivos = determinar_archivos_a_sincronizar
if archivos.empty?
@logger.advertencia("No se encontraron archivos para sincronizar")
mostrar_estadisticas
exit 0
end
puts "\n#{Color::CYAN}🔄 Sincronizando #{archivos.size} archivo(s) con dtic-BITACORAs#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
# Procesar cada archivo
archivos.each_with_index do |archivo, index|
procesar_archivo(archivo, index + 1, archivos.size)
end
mostrar_estadisticas
end
def determinar_archivos_a_sincronizar
archivos = []
if @opciones[:batch]
# Sincronizar todos los archivos en bitacoras/
patron = File.join(ADN::BITACORAS_DIR, "*.md")
archivos = Dir.glob(patron).sort
elsif @opciones[:fecha]
# Sincronizar archivo específico por fecha
ruta = File.join(ADN::BITACORAS_DIR, "#{@opciones[:fecha]}.md")
archivos = File.exist?(ruta) ? [ruta] : []
elsif @opciones[:ruta]
# Sincronizar archivo específico por ruta
archivos = [File.expand_path(@opciones[:ruta])]
else
# Sincronizar archivo del día actual
fecha_hoy = Date.today.strftime('%Y-%m-%d')
ruta_hoy = File.join(ADN::BITACORAS_DIR, "#{fecha_hoy}.md")
archivos = File.exist?(ruta_hoy) ? [ruta_hoy] : []
end
archivos
end
def procesar_archivo(ruta_archivo, indice, total)
nombre = File.basename(ruta_archivo)
puts "\n#{Color::CYAN}[#{indice}/#{total}] #{nombre}#{Color::RESET}"
unless File.exist?(ruta_archivo)
@resultados[:errores] << "#{nombre}: Archivo no encontrado"
@resultados[:archivos_fallidos] += 1
puts " #{Color::RED}✗ Archivo no encontrado#{Color::RESET}"
return
end
# Extraer fecha del nombre del archivo
fecha_match = nombre.match(/(\d{4}-\d{2}-\d{2})\.md/)
unless fecha_match
@resultados[:errores] << "#{nombre}: Formato de nombre inválido (debe ser YYYY-MM-DD.md)"
@resultados[:archivos_fallidos] += 1
puts " #{Color::RED}✗ Formato de nombre inválido#{Color::RESET}"
return
end
fecha = fecha_match[1]
# Validar formato del archivo si está habilitado
if @opciones[:validar_primero]
unless validar_archivo(ruta_archivo)
@resultados[:errores] << "#{nombre}: Falló validación previa"
@resultados[:archivos_fallidos] += 1
return
end
end
# Sincronizar con la API
begin
resultado = @client.sincronizar_md(ruta_archivo, sobrescribir: @opciones[:sobrescribir])
@resultados[:archivos_procesados] += 1
@resultados[:archivos_exitosos] += 1
@resultados[:entradas_sincronizadas] += resultado[:entradas_procesadas]
puts " #{Color::GREEN}✓ Sincronizado exitosamente#{Color::RESET}"
puts " #{Color::DIM}Bitácora ID: #{resultado[:bitacora_id]}#{Color::RESET}"
puts " #{Color::DIM}Entradas: #{resultado[:entradas_procesadas]}#{Color::RESET}"
@logger.info("Archivo sincronizado: #{nombre}", resultado)
rescue DticBitacorasApi::ApiError => e
@resultados[:archivos_procesados] += 1
@resultados[:archivos_fallidos] += 1
@resultados[:errores] << "#{nombre}: #{e.message}"
puts " #{Color::RED}✗ Error de API: #{e.message}#{Color::RESET}"
@logger.error("Error sincronizando #{nombre}: #{e.message}")
rescue => e
@resultados[:archivos_procesados] += 1
@resultados[:archivos_fallidos] += 1
@resultados[:errores] << "#{nombre}: Error inesperado - #{e.message}"
puts " #{Color::RED}✗ Error inesperado: #{e.message}#{Color::RESET}"
@logger.error("Error inesperado en #{nombre}: #{e.message}", backtrace: e.backtrace)
end
end
def validar_archivo(ruta_archivo)
puts " #{Color::CYAN}🔍 Validando formato...#{Color::RESET}"
contenido = File.read(ruta_archivo, encoding: 'UTF-8')
errores_validacion = []
# Validar formato básico
unless contenido.include?('## 📝 Actividades Detalladas')
errores_validacion << "Falta sección '## 📝 Actividades Detalladas'"
end
# Validar que tenga al menos una tabla de entradas
unless contenido.match(/\|\s*[I]\s*\|\s*[F]\s*\|\s*Descripción\s*\|\s*[E]\s*\|/)
errores_validacion << "No se encontró tabla de entradas I-F-D-E"
end
# Validar formato de tiempo
if contenido.match(/\d{1,2}:\d{2}\s*hs/)
errores_validacion << "Formato de tiempo incorrecto (contiene 'hs')"
end
if errores_validacion.empty?
puts " #{Color::GREEN}✓ Validación exitosa#{Color::RESET}"
true
else
puts " #{Color::YELLOW}⚠ Advertencias de validación:#{Color::RESET}"
errores_validacion.each { |e| puts " - #{e}" }
puts " #{Color::DIM}Continuando con sincronización...#{Color::RESET}"
true # Continuar a pesar de advertencias
end
end
def mostrar_estadisticas
puts "\n#{Color::CYAN}📊 Resultados de sincronización#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts " #{Color::GREEN}Archivos procesados:#{Color::RESET} #{@resultados[:archivos_procesados]}"
puts " #{Color::GREEN}Archivos exitosos:#{Color::RESET} #{@resultados[:archivos_exitosos]}"
if @resultados[:archivos_fallidos] > 0
puts " #{Color::RED}Archivos fallidos:#{Color::RESET} #{@resultados[:archivos_fallidos]}"
else
puts " #{Color::GREEN}Archivos fallidos:#{Color::RESET} 0"
end
puts " #{Color::GREEN}Entradas sincronizadas:#{Color::RESET} #{@resultados[:entradas_sincronizadas]}"
# Mostrar estadísticas de la API
mostrar_estadisticas_api
# Mostrar errores si los hay
unless @resultados[:errores].empty?
puts "\n#{Color::YELLOW}⚠ Errores encontrados:#{Color::RESET}"
@resultados[:errores].each do |error|
puts " #{Color::RED}#{error}#{Color::RESET}"
end
end
# Resumen final
if @resultados[:archivos_fallidos] == 0
@logger.exito("Sincronización completada exitosamente")
puts "\n#{Color::GREEN}✅ Sincronización completada exitosamente#{Color::RESET}"
else
@logger.advertencia("Sincronización completada con errores")
puts "\n#{Color::YELLOW}⚠ Sincronización completada con #{@resultados[:archivos_fallidos]} error(es)#{Color::RESET}"
end
@logger.info("Resumen de sincronización", @resultados)
end
def mostrar_estadisticas_api
begin
stats = @client.estadisticas
puts "\n#{Color::CYAN}📈 Estadísticas de dtic-BITACORAs#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 40}#{Color::RESET}"
puts " #{Color::GREEN}Nodos en sistema:#{Color::RESET} #{stats[:nodos]}"
puts " #{Color::GREEN}Bitácoras en sistema:#{Color::RESET}