""" cleanup_b2_versions.py ====================== Script de limpieza para eliminar versiones duplicadas de archivos en Backblaze B2. Mantiene únicamente la versión más reciente de cada archivo bajo el prefijo 'stems/'. Uso: cd MelodixAPI .\\venv_musical\\Scripts\\python cleanup_b2_versions.py [--dry-run] """ import os import sys import argparse from dotenv import load_dotenv load_dotenv() import boto3 from botocore.config import Config B2_KEY_ID = os.getenv("B2_KEY_ID") B2_APPLICATION_KEY = os.getenv("B2_APPLICATION_KEY") B2_ENDPOINT = os.getenv("B2_ENDPOINT") B2_BUCKET_NAME = os.getenv("B2_BUCKET_NAME") def get_client(): region = "us-east-005" try: parts = B2_ENDPOINT.replace("https://", "").replace("http://", "").split(".") if len(parts) >= 2: region = parts[1] if "s3" in parts[0] else parts[0] except Exception: pass return boto3.client( 's3', endpoint_url=B2_ENDPOINT, aws_access_key_id=B2_KEY_ID, aws_secret_access_key=B2_APPLICATION_KEY, region_name=region, config=Config(signature_version='s3', s3={'addressing_style': 'path'}) ) def cleanup_b2_duplicates(prefix="stems/", dry_run=False): client = get_client() print(f"{'[DRY RUN] ' if dry_run else ''}Escaneando versiones en B2 bucket '{B2_BUCKET_NAME}' bajo prefijo '{prefix}'...\n") paginator = client.get_paginator('list_object_versions') pages = paginator.paginate(Bucket=B2_BUCKET_NAME, Prefix=prefix) # Agrupar versiones por key versions_by_key = {} for page in pages: for version in page.get('Versions', []): key = version['Key'] versions_by_key.setdefault(key, []).append(version) for marker in page.get('DeleteMarkers', []): key = marker['Key'] versions_by_key.setdefault(key, []).append(marker) total_files = len(versions_by_key) total_deleted = 0 total_kept = 0 to_delete = [] for key, versions in versions_by_key.items(): # Filtrar solo versiones reales (no delete markers) y ordenar por LastModified desc real_versions = [v for v in versions if v.get('ETag')] # ETag presente = versión real delete_markers = [v for v in versions if not v.get('ETag')] if not real_versions and not delete_markers: total_kept += 1 continue if len(real_versions) <= 1 and not delete_markers: total_kept += 1 continue # Ordenar versiones reales por fecha desc, mantener la más reciente real_versions.sort(key=lambda v: v['LastModified'], reverse=True) old_versions = real_versions[1:] if real_versions else [] # Las demás son duplicados if old_versions or delete_markers: print(f" {key}: {len(real_versions)} versiones, {len(delete_markers)} delete markers -> Eliminando {len(old_versions) + len(delete_markers)} antiguas") for v in old_versions: to_delete.append({'Key': key, 'VersionId': v['VersionId']}) total_deleted += 1 for m in delete_markers: to_delete.append({'Key': key, 'VersionId': m['VersionId']}) total_deleted += 1 else: total_kept += 1 print(f"\nResumen: {total_files} archivos únicos, {total_deleted} versiones/markers a eliminar, {total_kept} sin duplicados") if not to_delete: print("OK: No hay duplicados que limpiar.") return if dry_run: print(f"\n[DRY RUN] Se eliminarian {len(to_delete)} versiones antiguas (sin cambios reales).") return # Eliminar en lotes de 1000 confirmed = input(f"\nConfirmar eliminacion de {len(to_delete)} versiones antiguas? (s/N): ").strip().lower() if confirmed != 's': print("Cancelado.") return deleted_count = 0 for i in range(0, len(to_delete), 1000): batch = to_delete[i:i+1000] resp = client.delete_objects( Bucket=B2_BUCKET_NAME, Delete={'Objects': batch, 'Quiet': False} ) deleted_count += len(resp.get('Deleted', [])) errors = resp.get('Errors', []) if errors: print(f" WARNING: {len(errors)} errores al eliminar:") for e in errors: print(f" - {e['Key']} v{e['VersionId']}: {e['Message']}") print(f"\nOK: Limpieza completada: {deleted_count} versiones antiguas eliminadas de B2.") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Limpia versiones duplicadas en B2") parser.add_argument("--dry-run", action="store_true", help="Solo muestra qué se eliminaría sin hacer cambios") parser.add_argument("--prefix", default="stems/", help="Prefijo de B2 a escanear (default: stems/)") args = parser.parse_args() if not all([B2_KEY_ID, B2_APPLICATION_KEY, B2_ENDPOINT, B2_BUCKET_NAME]): print("❌ Faltan variables de entorno B2. Verifica tu .env") sys.exit(1) cleanup_b2_duplicates(prefix=args.prefix, dry_run=args.dry_run)