#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Limpia archivos y registros de SQLite que se acumulan con el uso del proyecto. Categorías que limpia (todas configurables vía flags): - generated/browser/screenshots/*.png → más viejos de --screenshots-days (default 30) - generated/runtime/batch/_bulk_batch_*.json → más viejos de --batch-files-hours (default 1) - error_log (SQLite) → filas más viejas de --error-log-days (default 90) - sync_log (SQLite) → filas más viejas de --sync-log-days (default 60) NO toca (auditoría): - script_runs / script_change_log / script_run_control (auditoría de mutaciones). Modo dry-run por defecto: solo reporta qué borraría. Para ejecutar, pasa --apply. """ import argparse import glob import os import sqlite3 import sys import time ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if ROOT_DIR not in sys.path: sys.path.insert(0, ROOT_DIR) from paths import SCREENSHOTS_DIR as SCREENSHOT_DIR, BATCH_DIR, DB_PATH as SQLITE_DB # noqa: E402 def _human_bytes(n): for unit in ("B", "KB", "MB", "GB"): if n < 1024: return f"{n:.1f} {unit}" n /= 1024 return f"{n:.1f} TB" def cleanup_screenshots(days, apply_changes, log=print): """Borra capturas más viejas de N días.""" if not os.path.isdir(SCREENSHOT_DIR): return {"files_count": 0, "bytes": 0, "applied": apply_changes} cutoff = time.time() - (days * 86400) candidates = [] for f in os.listdir(SCREENSHOT_DIR): path = os.path.join(SCREENSHOT_DIR, f) try: if os.path.isfile(path) and os.path.getmtime(path) < cutoff: candidates.append((path, os.path.getsize(path))) except OSError: continue total_bytes = sum(s for _, s in candidates) log(f" Screenshots > {days} d: {len(candidates)} archivos, {_human_bytes(total_bytes)}") if apply_changes: for path, _ in candidates: try: os.remove(path) except OSError as e: log(f" ! No se pudo borrar {path}: {e}") return {"files_count": len(candidates), "bytes": total_bytes, "applied": apply_changes} def cleanup_batch_files(hours, apply_changes, log=print): """Borra archivos _bulk_batch_*.json más viejos de N horas.""" pattern = os.path.join(BATCH_DIR, "_bulk_batch_*.json") cutoff = time.time() - (hours * 3600) candidates = [] for path in glob.glob(pattern): try: if os.path.getmtime(path) < cutoff: candidates.append((path, os.path.getsize(path))) except OSError: continue total_bytes = sum(s for _, s in candidates) log(f" Batch files > {hours} h: {len(candidates)} archivos, {_human_bytes(total_bytes)}") if apply_changes: for path, _ in candidates: try: os.remove(path) except OSError as e: log(f" ! No se pudo borrar {path}: {e}") return {"files_count": len(candidates), "bytes": total_bytes, "applied": apply_changes} def cleanup_sqlite_table(table, ts_column, days, apply_changes, log=print): """Borra filas de `table` cuyo `ts_column` sea más viejo de N días.""" if not os.path.exists(SQLITE_DB): return {"table": table, "rows_count": 0, "applied": apply_changes} cutoff_epoch = time.time() - (days * 86400) # `created_at` y `started_at` se guardan como TEXT con datetime('now','localtime') # → formato 'YYYY-MM-DD HH:MM:SS'. SQLite puede comparar como string en este formato. cutoff_iso = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(cutoff_epoch)) conn = sqlite3.connect(SQLITE_DB) try: cur = conn.execute(f"SELECT COUNT(*) FROM {table} WHERE {ts_column} < ?", (cutoff_iso,)) n = cur.fetchone()[0] log(f" {table} con {ts_column} < {cutoff_iso}: {n} filas") if apply_changes and n > 0: conn.execute(f"DELETE FROM {table} WHERE {ts_column} < ?", (cutoff_iso,)) conn.commit() log(f" → borradas {n} filas de {table}.") return {"table": table, "rows_count": n, "applied": apply_changes} finally: conn.close() def cleanup_all(args, log=print): """Ejecuta todas las limpiezas. Devuelve un dict-resumen con métricas por categoría.""" if args.apply: log("[APPLY] Ejecutando limpieza (borrado real)...") else: log("[DRY-RUN] Simulación — no se borra nada. Usa --apply para ejecutar.") log("") log("--- Archivos ---") s = cleanup_screenshots(args.screenshots_days, args.apply, log=log) b = cleanup_batch_files(args.batch_files_hours, args.apply, log=log) log("") log("--- SQLite ---") el = cleanup_sqlite_table("error_log", "created_at", args.error_log_days, args.apply, log=log) sl = cleanup_sqlite_table("sync_log", "started_at", args.sync_log_days, args.apply, log=log) total_bytes = s["bytes"] + b["bytes"] total_rows = el["rows_count"] + sl["rows_count"] total_files = s["files_count"] + b["files_count"] log("") log("=== RESUMEN ===") log(f" Archivos a borrar: {total_files} ({_human_bytes(total_bytes)})") log(f" Filas SQLite a borrar: {total_rows}") if not args.apply and (total_files > 0 or total_rows > 0): log("") log(" Para ejecutar la limpieza real, vuelve a correr con --apply") if args.apply and total_rows > 0: # Compactar el archivo SQLite después de borrar (libera espacio en disco). log("") log(" Compactando mp_manager.sqlite con VACUUM...") conn = sqlite3.connect(SQLITE_DB) try: size_before = os.path.getsize(SQLITE_DB) conn.execute("VACUUM") size_after = os.path.getsize(SQLITE_DB) log(f" Tamaño SQLite: {_human_bytes(size_before)} → {_human_bytes(size_after)} " f"(liberados {_human_bytes(max(0, size_before - size_after))})") except Exception as e: log(f" ! VACUUM falló: {e}") finally: conn.close() return { "applied": args.apply, "screenshots": s, "batch_files": b, "error_log": el, "sync_log": sl, "total_files": total_files, "total_bytes": total_bytes, "total_rows": total_rows, } def main(): parser = argparse.ArgumentParser(description="Limpia archivos y registros que se acumulan con el uso.") parser.add_argument("--apply", action="store_true", help="Ejecuta el borrado. Sin esto es dry-run.") parser.add_argument("--screenshots-days", type=int, default=30, help="Borrar capturas más viejas de N días (default 30).") parser.add_argument("--batch-files-hours", type=int, default=1, help="Borrar _bulk_batch_*.json más viejos de N horas (default 1).") parser.add_argument("--error-log-days", type=int, default=90, help="Borrar filas de error_log más viejas de N días (default 90).") parser.add_argument("--sync-log-days", type=int, default=60, help="Borrar filas de sync_log más viejas de N días (default 60).") args = parser.parse_args() print("=== CONTROL DE SCRIPTS: cleanup_storage.py ===") print(f"ROOT: {ROOT_DIR}") print(f"Modo: {'APPLY (borra)' if args.apply else 'DRY-RUN (solo reporta)'}") print(f"Umbrales: screenshots>{args.screenshots_days}d, batch>{args.batch_files_hours}h, " f"error_log>{args.error_log_days}d, sync_log>{args.sync_log_days}d") print("-" * 70) result = cleanup_all(args) sys.exit(0) if __name__ == "__main__": main()