Spaces:
Running
Running
| """ | |
| 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) | |