Primer commit
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user