melodix-api / app /routers /migration.py
GitHub Action
deploy from github actions
18499c1
Raw
History Blame Contribute Delete
10.2 kB
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()
@router.post("/backup")
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()
}
@router.post("/upload/{backup_id}/{task_id}/{stem_name}")
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}
@router.post("/finalize/{backup_id}")
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}
@router.get("/status")
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
}
@router.get("/restore")
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
}
@router.post("/complete")
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}
@router.delete("/backup")
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}