117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
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"}
|