Files
imap_migrate/progress.py
T
2026-09-10 09:24:22 -06:00

157 lines
4.8 KiB
Python

"""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)