Spaces:
Running
Running
| import os | |
| import shutil | |
| import logging | |
| from datetime import datetime, timedelta | |
| from typing import Dict, List, Optional | |
| from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File | |
| from sqlalchemy.orm import Session | |
| from sqlalchemy.orm.attributes import flag_modified | |
| from pydantic import BaseModel | |
| from app.database import get_db | |
| from app.models.user import User | |
| from app.models.sync import MigrationBackup | |
| from app.routers.auth import get_current_user | |
| from app.utils.hf_storage import ( | |
| upload_to_hf, | |
| delete_folder_from_hf, | |
| get_hf_download_url | |
| ) | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter(prefix="/migration", tags=["Device Migration"]) | |
| class BackupCreate(BaseModel): | |
| metadata_json: dict | |
| def cleanup_expired_backups(db: Session, user_id: int): | |
| """ | |
| Busca y limpia respaldos que hayan pasado el límite de 7 días de validez. | |
| """ | |
| now = datetime.utcnow() | |
| expired = db.query(MigrationBackup).filter( | |
| MigrationBackup.user_id == user_id, | |
| MigrationBackup.expires_at < now, | |
| MigrationBackup.status.in_(["pending", "completed"]) | |
| ).all() | |
| for eb in expired: | |
| try: | |
| logger.info(f"Limpiando respaldo expirado {eb.id} para usuario {user_id}") | |
| delete_folder_from_hf(f"migrations/{user_id}") | |
| eb.status = "expired" | |
| except Exception as e: | |
| logger.error(f"Error limpiando respaldo expirado {eb.id}: {e}") | |
| if expired: | |
| db.commit() | |
| def create_backup( | |
| data: BackupCreate, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_user) | |
| ): | |
| """ | |
| Inicia un proceso de respaldo. Elimina cualquier respaldo anterior en Hugging Face y BD. | |
| """ | |
| # 1. Limpieza de expirados | |
| cleanup_expired_backups(db, current_user.id) | |
| # 2. Buscar si ya existe algún respaldo activo (pendiente o completado) | |
| active_backups = db.query(MigrationBackup).filter( | |
| MigrationBackup.user_id == current_user.id, | |
| MigrationBackup.status.in_(["pending", "completed"]) | |
| ).all() | |
| # Eliminar archivos en Hugging Face y registros en BD de respaldos anteriores | |
| if active_backups: | |
| logger.info(f"Eliminando {len(active_backups)} respaldos activos previos para usuario {current_user.id}") | |
| delete_folder_from_hf(f"migrations/{current_user.id}") | |
| for ab in active_backups: | |
| db.delete(ab) | |
| db.commit() | |
| # 3. Crear el nuevo registro | |
| expires_at = datetime.utcnow() + timedelta(days=7) | |
| backup = MigrationBackup( | |
| user_id=current_user.id, | |
| expires_at=expires_at, | |
| status="pending", | |
| metadata_json=data.metadata_json, | |
| files_info=[] | |
| ) | |
| db.add(backup) | |
| db.commit() | |
| db.refresh(backup) | |
| return { | |
| "success": True, | |
| "backup_id": backup.id, | |
| "expires_at": backup.expires_at.isoformat() | |
| } | |
| def upload_backup_file( | |
| backup_id: str, | |
| task_id: str, | |
| stem_name: str, | |
| file: UploadFile = File(...), | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_user) | |
| ): | |
| """ | |
| Recibe un stem individual en formato .flac o .json y lo sube a la ruta migrations/ de Hugging Face. | |
| """ | |
| # Buscar y validar el respaldo | |
| backup = db.query(MigrationBackup).filter( | |
| MigrationBackup.id == backup_id, | |
| MigrationBackup.user_id == current_user.id | |
| ).first() | |
| if not backup: | |
| raise HTTPException(status_code=404, detail="Respaldo de migración no encontrado") | |
| if backup.status == "restored": | |
| raise HTTPException(status_code=400, detail="Este respaldo ya ha sido restaurado") | |
| if backup.expires_at < datetime.utcnow(): | |
| backup.status = "expired" | |
| db.commit() | |
| raise HTTPException(status_code=400, detail="El respaldo de migración ha expirado") | |
| # Crear directorio temporal local para recibir el archivo | |
| temp_dir = os.path.join(os.getcwd(), "uploads", "migration_temp") | |
| os.makedirs(temp_dir, exist_ok=True) | |
| orig_ext = "" | |
| if file.filename: | |
| orig_ext = file.filename.split('.')[-1].lower() | |
| if stem_name.endswith("_pitch"): | |
| ext = "json" | |
| elif orig_ext in ["m4a", "flac"]: | |
| ext = orig_ext | |
| else: | |
| ext = "flac" | |
| temp_file_path = os.path.join(temp_dir, f"{backup_id}_{task_id}_{stem_name}.{ext}") | |
| try: | |
| # Guardar en disco local de forma temporal | |
| with open(temp_file_path, "wb") as buffer: | |
| shutil.copyfileobj(file.file, buffer) | |
| # Subir a Hugging Face Dataset | |
| object_key = f"migrations/{current_user.id}/{task_id}/{stem_name}.{ext}" | |
| public_url = upload_to_hf(temp_file_path, object_key) | |
| # Registrar el archivo en la BD | |
| files_list = list(backup.files_info) if backup.files_info else [] | |
| files_list.append({ | |
| "task_id": task_id, | |
| "stem_name": stem_name, | |
| "file_key": object_key | |
| }) | |
| backup.files_info = files_list | |
| flag_modified(backup, "files_info") | |
| db.commit() | |
| except Exception as e: | |
| logger.error(f"Error subiendo archivo de migración {stem_name} para {task_id}: {e}") | |
| raise HTTPException(status_code=500, detail=f"Error en la subida a Hugging Face: {str(e)}") | |
| finally: | |
| if os.path.exists(temp_file_path): | |
| os.remove(temp_file_path) | |
| return {"success": True, "url": public_url} | |
| def finalize_backup( | |
| backup_id: str, | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_user) | |
| ): | |
| """ | |
| Marca el respaldo de migración como completado y listo para ser descargado. | |
| """ | |
| backup = db.query(MigrationBackup).filter( | |
| MigrationBackup.id == backup_id, | |
| MigrationBackup.user_id == current_user.id | |
| ).first() | |
| if not backup: | |
| raise HTTPException(status_code=404, detail="Respaldo no encontrado") | |
| if backup.expires_at < datetime.utcnow(): | |
| backup.status = "expired" | |
| db.commit() | |
| raise HTTPException(status_code=400, detail="El respaldo de migración ha expirado") | |
| backup.status = "completed" | |
| db.commit() | |
| return {"success": True} | |
| def get_backup_status( | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_user) | |
| ): | |
| """ | |
| Verifica si existe un respaldo completado y vigente para el usuario actual. | |
| """ | |
| cleanup_expired_backups(db, current_user.id) | |
| backup = db.query(MigrationBackup).filter( | |
| MigrationBackup.user_id == current_user.id, | |
| MigrationBackup.status == "completed" | |
| ).order_by(MigrationBackup.created_at.desc()).first() | |
| if not backup: | |
| return {"backup_available": False} | |
| return { | |
| "backup_available": True, | |
| "backup_id": backup.id, | |
| "created_at": backup.created_at.isoformat(), | |
| "expires_at": backup.expires_at.isoformat(), | |
| "files_count": len(backup.files_info) if backup.files_info else 0 | |
| } | |
| def restore_backup( | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_user) | |
| ): | |
| """ | |
| Retorna el mapa de AsyncStorage y las URLs de descarga de Hugging Face para todos los stems respaldados. | |
| """ | |
| cleanup_expired_backups(db, current_user.id) | |
| backup = db.query(MigrationBackup).filter( | |
| MigrationBackup.user_id == current_user.id, | |
| MigrationBackup.status == "completed" | |
| ).order_by(MigrationBackup.created_at.desc()).first() | |
| if not backup: | |
| raise HTTPException(status_code=404, detail="No tienes ningún respaldo activo listo para restaurar") | |
| # Generar URLs de descarga de Hugging Face | |
| restore_files = [] | |
| files_list = backup.files_info or [] | |
| for f in files_list: | |
| file_key = f["file_key"] | |
| download_url = get_hf_download_url(file_key) | |
| restore_files.append({ | |
| "task_id": f["task_id"], | |
| "stem_name": f["stem_name"], | |
| "url": download_url, | |
| "file_key": file_key | |
| }) | |
| return { | |
| "metadata_json": backup.metadata_json, | |
| "files": restore_files | |
| } | |
| def complete_restore( | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_user) | |
| ): | |
| """ | |
| Confirma que el nuevo dispositivo ha terminado de descargar los archivos y elimina los datos de Hugging Face. | |
| """ | |
| backup = db.query(MigrationBackup).filter( | |
| MigrationBackup.user_id == current_user.id, | |
| MigrationBackup.status == "completed" | |
| ).order_by(MigrationBackup.created_at.desc()).first() | |
| if backup: | |
| # Eliminar archivos físicamente de Hugging Face | |
| logger.info(f"Limpiando carpeta de HF tras restauración exitosa de {backup.id}") | |
| delete_folder_from_hf(f"migrations/{current_user.id}") | |
| backup.status = "restored" | |
| db.commit() | |
| return {"success": True} | |
| def delete_backup( | |
| db: Session = Depends(get_db), | |
| current_user: User = Depends(get_current_user) | |
| ): | |
| """ | |
| Elimina permanentemente cualquier respaldo activo de migración del usuario en Hugging Face y la base de datos. | |
| """ | |
| active_backups = db.query(MigrationBackup).filter( | |
| MigrationBackup.user_id == current_user.id, | |
| MigrationBackup.status.in_(["pending", "completed"]) | |
| ).all() | |
| if active_backups: | |
| logger.info(f"Eliminando {len(active_backups)} respaldos activos por petición de limpieza para usuario {current_user.id}") | |
| try: | |
| delete_folder_from_hf(f"migrations/{current_user.id}") | |
| except Exception as e: | |
| logger.error(f"Error borrando carpeta de HF para migración del usuario {current_user.id}: {e}") | |
| for ab in active_backups: | |
| db.delete(ab) | |
| db.commit() | |
| return {"success": True} | |