feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment

This commit is contained in:
2026-08-13 19:33:44 -03:00
commit 1bfb808c79
77 changed files with 10675 additions and 0 deletions
+175
View File
@@ -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"}