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,175 @@
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.models import User, Client, BackupJob
|
||||
from app.schemas.schemas import JobCreate, JobUpdate, JobResponse
|
||||
from app.api.deps import get_current_user, require_admin, get_current_client
|
||||
from app.services.event_service import log_event
|
||||
from app.ws.manager import ws_manager
|
||||
|
||||
router = APIRouter(prefix="/jobs", tags=["Backup Jobs"])
|
||||
|
||||
@router.get("", response_model=List[JobResponse])
|
||||
async def list_jobs(
|
||||
client_id: Optional[int] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
query = select(BackupJob)
|
||||
if client_id:
|
||||
query = query.where(BackupJob.client_id == client_id)
|
||||
query = query.order_by(desc(BackupJob.created_at))
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.get("/agent/assigned", response_model=List[JobResponse])
|
||||
async def get_agent_jobs(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_client: Client = Depends(get_current_client)
|
||||
):
|
||||
"""Called by the Windows Agent to query its active backup jobs."""
|
||||
query = select(BackupJob).where(BackupJob.client_id == current_client.id, BackupJob.is_active == True)
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("", response_model=JobResponse)
|
||||
async def create_job(
|
||||
payload: JobCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
# Verify client exists
|
||||
client_res = await db.execute(select(Client).where(Client.id == payload.client_id))
|
||||
client = client_res.scalar_one_or_none()
|
||||
if not client:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Client not found")
|
||||
|
||||
count_res = await db.execute(select(func.count(BackupJob.id)))
|
||||
job_count = count_res.scalar() or 0
|
||||
job_code = f"JOB-{job_count + 1:03d}"
|
||||
|
||||
job = BackupJob(
|
||||
job_code=job_code,
|
||||
client_id=payload.client_id,
|
||||
name=payload.name,
|
||||
source_path=payload.source_path,
|
||||
file_patterns=payload.file_patterns,
|
||||
schedule_cron=payload.schedule_cron,
|
||||
keep_daily=payload.keep_daily,
|
||||
keep_weekly=payload.keep_weekly,
|
||||
keep_monthly=payload.keep_monthly,
|
||||
min_stable_time_seconds=payload.min_stable_time_seconds,
|
||||
status="IDLE",
|
||||
is_active=True
|
||||
)
|
||||
db.add(job)
|
||||
await db.commit()
|
||||
await db.refresh(job)
|
||||
|
||||
await log_event(
|
||||
db=db,
|
||||
event_type="JOB_CREATED",
|
||||
message=f"Created backup job '{job.name}' ({job.job_code}) for client {client.name}.",
|
||||
severity="INFO",
|
||||
client_id=client.id,
|
||||
job_id=job.id,
|
||||
user_email=admin_user.email
|
||||
)
|
||||
|
||||
return job
|
||||
|
||||
@router.get("/{job_id}", response_model=JobResponse)
|
||||
async def get_job(
|
||||
job_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
res = await db.execute(select(BackupJob).where(BackupJob.id == job_id))
|
||||
job = res.scalar_one_or_none()
|
||||
if not job:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found")
|
||||
return job
|
||||
|
||||
@router.put("/{job_id}", response_model=JobResponse)
|
||||
async def update_job(
|
||||
job_id: int,
|
||||
payload: JobUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
res = await db.execute(select(BackupJob).where(BackupJob.id == job_id))
|
||||
job = res.scalar_one_or_none()
|
||||
if not job:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found")
|
||||
|
||||
for field, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(job, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(job)
|
||||
|
||||
await log_event(
|
||||
db=db,
|
||||
event_type="CONFIG_CHANGED",
|
||||
message=f"Updated backup job '{job.name}' ({job.job_code}).",
|
||||
severity="INFO",
|
||||
client_id=job.client_id,
|
||||
job_id=job.id,
|
||||
user_email=admin_user.email
|
||||
)
|
||||
|
||||
return job
|
||||
|
||||
@router.delete("/{job_id}")
|
||||
async def delete_job(
|
||||
job_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
res = await db.execute(select(BackupJob).where(BackupJob.id == job_id))
|
||||
job = res.scalar_one_or_none()
|
||||
if not job:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found")
|
||||
|
||||
await db.delete(job)
|
||||
await db.commit()
|
||||
|
||||
await log_event(
|
||||
db=db,
|
||||
event_type="JOB_DELETED",
|
||||
message=f"Deleted backup job {job.job_code}.",
|
||||
severity="WARNING",
|
||||
client_id=job.client_id,
|
||||
user_email=admin_user.email
|
||||
)
|
||||
|
||||
return {"message": f"Job {job.job_code} deleted"}
|
||||
|
||||
@router.post("/{job_id}/trigger")
|
||||
async def trigger_job(
|
||||
job_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Notifies the connected agent to start executing this backup job immediately."""
|
||||
res = await db.execute(select(BackupJob).where(BackupJob.id == job_id))
|
||||
job = res.scalar_one_or_none()
|
||||
if not job:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found")
|
||||
|
||||
job.status = "QUEUED"
|
||||
await db.commit()
|
||||
|
||||
# Broadcast event so the agent / web UI knows the job was triggered
|
||||
await ws_manager.broadcast("JOB_TRIGGERED", {
|
||||
"job_id": job.id,
|
||||
"job_code": job.job_code,
|
||||
"client_id": job.client_id,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat()
|
||||
})
|
||||
|
||||
return {"message": f"Job {job.job_code} triggered"}
|
||||
Reference in New Issue
Block a user