feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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"}
|
||||
Reference in New Issue
Block a user