"""Seguimiento y presentación del progreso de la migración en consola.""" from __future__ import annotations import sys import time def format_bytes(n: float) -> str: units = ["B", "KB", "MB", "GB", "TB"] size = float(max(n, 0)) for unit in units[:-1]: if size < 1024: return f"{size:.1f} {unit}" size /= 1024 return f"{size:.1f} {units[-1]}" def format_duration(seconds: float) -> str: seconds = int(max(0, seconds)) h, rem = divmod(seconds, 3600) m, s = divmod(rem, 60) return f"{h:02d}:{m:02d}:{s:02d}" def compute_eta(elapsed: float, done: int, total: int) -> float: """Tiempo estimado restante en segundos, dado lo hecho hasta ahora.""" if done <= 0 or total <= 0 or elapsed <= 0: return 0.0 rate = done / elapsed if rate <= 0: return 0.0 remaining = max(total - done, 0) return remaining / rate class ProgressTracker: def __init__(self, stream=None): self.stream = stream or sys.stdout self.total_folders = 0 self.folders_done = 0 self.current_folder = "" self.total_messages = 0 self.messages_processed = 0 self.messages_copied = 0 self.messages_skipped = 0 self.messages_errors = 0 self.total_bytes = 0 self.bytes_transferred = 0 self.start_time = None self._last_render = 0.0 def set_total_folders(self, n: int) -> None: self.total_folders = n def set_total_messages(self, n: int) -> None: self.total_messages = n def set_total_bytes(self, n: int) -> None: self.total_bytes = n def set_current_folder(self, name: str) -> None: self.current_folder = name def folder_done(self) -> None: self.folders_done += 1 def start(self) -> None: self.start_time = time.monotonic() def message_copied(self, size: int) -> None: self.messages_processed += 1 self.messages_copied += 1 self.bytes_transferred += size self._maybe_render() def message_error(self) -> None: self.messages_processed += 1 self.messages_errors += 1 self._maybe_render() def elapsed(self) -> float: if self.start_time is None: return 0.0 return time.monotonic() - self.start_time def speed_bps(self) -> float: e = self.elapsed() return self.bytes_transferred / e if e > 0 else 0.0 def eta_seconds(self) -> float: return compute_eta(self.elapsed(), self.messages_processed, self.total_messages) def _maybe_render(self, force: bool = False) -> None: now = time.monotonic() if not force and (now - self._last_render) < 0.5: return self._last_render = now self.render() def render(self) -> None: pct = (self.messages_processed / self.total_messages * 100) if self.total_messages else 100.0 pct = min(pct, 100.0) bar_width = 24 filled = int(bar_width * pct / 100) bar = "█" * filled + "░" * (bar_width - filled) folder = (self.current_folder or "")[:20] line = ( f"\rCarpeta: {folder:<20} [{bar}] {pct:5.1f}% " f"Msjs: {self.messages_processed}/{self.total_messages} " f"Copiados: {self.messages_copied} Omitidos: {self.messages_skipped} " f"Errores: {self.messages_errors} " f"{format_bytes(self.bytes_transferred)}/{format_bytes(self.total_bytes)} " f"{format_bytes(self.speed_bps())}/s " f"T: {format_duration(self.elapsed())} ETA: {format_duration(self.eta_seconds())}" ) self.stream.write(line) self.stream.flush() def stop(self) -> None: self.render() self.stream.write("\n") self.stream.flush() def print_summary(self, host_a: str, host_b: str, logger) -> None: lines = [ "=" * 40, "MIGRACIÓN TERMINADA", "=" * 40, "", "Servidor origen:", host_a, "", "Servidor destino:", host_b, "", f"Carpetas procesadas: {self.folders_done}/{self.total_folders}", "", f"Mensajes nuevos detectados: {self.total_messages}", f"Mensajes copiados: {self.messages_copied}", f"Mensajes existentes: {self.messages_skipped}", f"Mensajes con error: {self.messages_errors}", "", "Datos transferidos:", format_bytes(self.bytes_transferred), "", "Tiempo total:", format_duration(self.elapsed()), "", "Velocidad promedio:", f"{format_bytes(self.speed_bps())}/s", "=" * 40, ] text = "\n".join(lines) print(text) logger.info("Resumen final:\n" + text)