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
+110
View File
@@ -0,0 +1,110 @@
from datetime import datetime, timedelta, timezone
from typing import List, Set
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.models import BackupJob, BackupFile, Client
from app.storage.local import storage_provider
from app.services.event_service import log_event
async def apply_retention_policy(db: AsyncSession, job_id: int) -> int:
"""
Applies retention rules (daily, weekly, monthly) for a specific backup job.
Safely deletes obsolete backup files from storage and database.
Returns the number of pruned backup files.
"""
# Fetch job details
result = await db.execute(select(BackupJob).where(BackupJob.id == job_id))
job = result.scalar_one_or_none()
if not job:
return 0
# Fetch all active backup files for this job ordered by creation date descending
result = await db.execute(
select(BackupFile)
.where(BackupFile.job_id == job_id, BackupFile.is_active == True)
.order_by(BackupFile.created_at.desc())
)
backup_files: List[BackupFile] = result.scalars().all()
if not backup_files:
return 0
now = datetime.now(timezone.utc)
protected_file_ids: Set[int] = set()
# Always keep the most recent backup regardless of age to avoid empty repo
protected_file_ids.add(backup_files[0].id)
# 1. Daily retention: Keep 1 backup per calendar day for the last `job.keep_daily` days
seen_days = set()
for bf in backup_files:
day_key = bf.created_at.strftime("%Y-%m-%d")
age_days = (now - bf.created_at).total_seconds() / 86400.0
if age_days <= job.keep_daily:
if day_key not in seen_days:
seen_days.add(day_key)
protected_file_ids.add(bf.id)
# 2. Weekly retention: Keep 1 backup per calendar week for the last `job.keep_weekly` weeks
seen_weeks = set()
for bf in backup_files:
week_key = f"{bf.created_at.year}-W{bf.created_at.isocalendar()[1]:02d}"
age_weeks = (now - bf.created_at).total_seconds() / (86400.0 * 7)
if age_weeks <= job.keep_weekly:
if week_key not in seen_weeks:
seen_weeks.add(week_key)
protected_file_ids.add(bf.id)
# 3. Monthly retention: Keep 1 backup per calendar month for the last `job.keep_monthly` months
seen_months = set()
for bf in backup_files:
month_key = bf.created_at.strftime("%Y-%m")
# Approximate age in months (30 days per month)
age_months = (now - bf.created_at).total_seconds() / (86400.0 * 30.4375)
if age_months <= job.keep_monthly:
if month_key not in seen_months:
seen_months.add(month_key)
protected_file_ids.add(bf.id)
# Identify files to delete
files_to_delete = [bf for bf in backup_files if bf.id not in protected_file_ids]
pruned_count = 0
reclaimed_bytes = 0
for bf in files_to_delete:
try:
# Delete from physical storage
await storage_provider.delete_backup_file(bf.relative_path)
bf.is_active = False
pruned_count += 1
reclaimed_bytes += bf.file_size
except Exception as ex:
# Log failure but continue processing other files
await log_event(
db=db,
event_type="RETENTION_ERROR",
message=f"Failed to delete pruned backup file {bf.filename}: {str(ex)}",
severity="WARNING",
client_id=job.client_id,
job_id=job.id
)
if pruned_count > 0:
# Update client storage used counter
client_res = await db.execute(select(Client).where(Client.id == job.client_id))
client = client_res.scalar_one_or_none()
if client:
client.storage_used_bytes = max(0, client.storage_used_bytes - reclaimed_bytes)
await db.commit()
await log_event(
db=db,
event_type="RETENTION_APPLIED",
message=f"Retention policy applied for job '{job.name}': {pruned_count} obsolete backups removed, {reclaimed_bytes / (1024*1024):.2f} MB reclaimed.",
severity="INFO",
client_id=job.client_id,
job_id=job.id
)
return pruned_count