import os from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from app.core.database import get_db from app.models.models import User, BackupFile, Client from app.schemas.schemas import BackupFileResponse from app.api.deps import get_current_user, require_admin from app.storage.local import storage_provider from app.services.event_service import log_event router = APIRouter(prefix="/backups", tags=["Backups & Restore"]) @router.get("", response_model=List[BackupFileResponse]) async def list_backups( client_id: Optional[int] = None, job_id: Optional[int] = None, limit: int = 100, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): query = select(BackupFile).where(BackupFile.is_active == True) if client_id: query = query.where(BackupFile.client_id == client_id) if job_id: query = query.where(BackupFile.job_id == job_id) query = query.order_by(desc(BackupFile.created_at)).limit(limit) result = await db.execute(query) return result.scalars().all() @router.get("/{backup_id}", response_model=BackupFileResponse) async def get_backup( backup_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): res = await db.execute(select(BackupFile).where(BackupFile.id == backup_id)) bf = res.scalar_one_or_none() if not bf: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Backup file not found") return bf @router.get("/{backup_id}/download") async def download_backup( backup_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Allows downloading a backup file directly from the Web interface.""" res = await db.execute(select(BackupFile).where(BackupFile.id == backup_id)) bf = res.scalar_one_or_none() if not bf or not bf.is_active: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Backup file not found") try: abs_path = await storage_provider.get_file_path(bf.relative_path) except Exception as ex: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(ex)) await log_event( db=db, event_type="RESTORE_STARTED", message=f"Backup download initiated for '{bf.filename}' ({bf.file_size / (1024*1024):.2f} MB).", severity="INFO", client_id=bf.client_id, job_id=bf.job_id, user_email=current_user.email ) return FileResponse( path=abs_path, filename=bf.filename, media_type="application/octet-stream" ) @router.delete("/{backup_id}") async def delete_backup( backup_id: int, db: AsyncSession = Depends(get_db), admin_user: User = Depends(require_admin) ): res = await db.execute(select(BackupFile).where(BackupFile.id == backup_id)) bf = res.scalar_one_or_none() if not bf: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Backup file not found") try: await storage_provider.delete_backup_file(bf.relative_path) except Exception: pass bf.is_active = False # Update client storage counter client_res = await db.execute(select(Client).where(Client.id == bf.client_id)) client = client_res.scalar_one_or_none() if client: client.storage_used_bytes = max(0, client.storage_used_bytes - bf.file_size) await db.commit() await log_event( db=db, event_type="FILE_DELETED", message=f"Manual deletion of backup '{bf.filename}'.", severity="WARNING", client_id=bf.client_id, job_id=bf.job_id, user_email=admin_user.email ) return {"message": "Backup file deleted"}