All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 6m15s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 28s
Deploy to Staging / Verify Staging (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Replace per-schedule count-based retention with unified tiered classification. Backups are now classified by timestamp into categories (hourly/daily/weekly/monthly) and are only deleted when they exceed ALL applicable category quotas. Changes: - Add backup-classification.service.ts for timestamp-based classification - Rewrite backup-retention.service.ts with tiered logic - Add categories and expires_at columns to backup_history - Add Expires column to desktop and mobile backup UI - Add unit tests for classification logic (22 tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
107 lines
3.2 KiB
TypeScript
107 lines
3.2 KiB
TypeScript
/**
|
|
* @ai-summary Service for classifying backups into tiered retention categories
|
|
* @ai-context Pure functions for timestamp-based classification, no database dependencies
|
|
*/
|
|
|
|
import { BackupCategory, TIERED_RETENTION } from './backup.types';
|
|
|
|
/**
|
|
* Classifies a backup by its timestamp into retention categories.
|
|
* A backup can belong to multiple categories simultaneously.
|
|
*
|
|
* Categories:
|
|
* - hourly: All backups
|
|
* - daily: First backup at midnight UTC (hour = 0)
|
|
* - weekly: First backup on Sunday at midnight UTC
|
|
* - monthly: First backup on 1st of month at midnight UTC
|
|
*/
|
|
export function classifyBackup(timestamp: Date): BackupCategory[] {
|
|
const categories: BackupCategory[] = ['hourly'];
|
|
|
|
const utcHour = timestamp.getUTCHours();
|
|
const utcDay = timestamp.getUTCDate();
|
|
const utcDayOfWeek = timestamp.getUTCDay(); // 0 = Sunday
|
|
|
|
// Midnight UTC qualifies for daily
|
|
if (utcHour === 0) {
|
|
categories.push('daily');
|
|
|
|
// Sunday at midnight qualifies for weekly
|
|
if (utcDayOfWeek === 0) {
|
|
categories.push('weekly');
|
|
}
|
|
|
|
// 1st of month at midnight qualifies for monthly
|
|
if (utcDay === 1) {
|
|
categories.push('monthly');
|
|
}
|
|
}
|
|
|
|
return categories;
|
|
}
|
|
|
|
/**
|
|
* Calculates the expiration date based on the backup's categories.
|
|
* Uses the longest retention period among all applicable categories.
|
|
*
|
|
* Retention periods are count-based in the actual cleanup, but for display
|
|
* we estimate based on typical backup frequency:
|
|
* - hourly: 8 hours (8 backups * 1 hour)
|
|
* - daily: 7 days (7 backups * 1 day)
|
|
* - weekly: 4 weeks (4 backups * 1 week)
|
|
* - monthly: 12 months (12 backups * 1 month)
|
|
*/
|
|
export function calculateExpiration(
|
|
categories: BackupCategory[],
|
|
timestamp: Date
|
|
): Date {
|
|
const expirationDate = new Date(timestamp);
|
|
|
|
if (categories.includes('monthly')) {
|
|
expirationDate.setUTCMonth(expirationDate.getUTCMonth() + TIERED_RETENTION.monthly);
|
|
} else if (categories.includes('weekly')) {
|
|
expirationDate.setUTCDate(expirationDate.getUTCDate() + TIERED_RETENTION.weekly * 7);
|
|
} else if (categories.includes('daily')) {
|
|
expirationDate.setUTCDate(expirationDate.getUTCDate() + TIERED_RETENTION.daily);
|
|
} else {
|
|
// Hourly only - 8 hours
|
|
expirationDate.setUTCHours(expirationDate.getUTCHours() + TIERED_RETENTION.hourly);
|
|
}
|
|
|
|
return expirationDate;
|
|
}
|
|
|
|
/**
|
|
* Checks if a backup timestamp represents the first backup of the day (midnight UTC).
|
|
*/
|
|
export function isFirstBackupOfDay(timestamp: Date): boolean {
|
|
return timestamp.getUTCHours() === 0;
|
|
}
|
|
|
|
/**
|
|
* Checks if a timestamp falls on a Sunday.
|
|
*/
|
|
export function isSunday(timestamp: Date): boolean {
|
|
return timestamp.getUTCDay() === 0;
|
|
}
|
|
|
|
/**
|
|
* Checks if a timestamp falls on the first day of the month.
|
|
*/
|
|
export function isFirstDayOfMonth(timestamp: Date): boolean {
|
|
return timestamp.getUTCDate() === 1;
|
|
}
|
|
|
|
/**
|
|
* Classifies a backup and calculates its expiration in one call.
|
|
* Convenience function for backup creation flow.
|
|
*/
|
|
export function classifyAndCalculateExpiration(timestamp: Date): {
|
|
categories: BackupCategory[];
|
|
expiresAt: Date;
|
|
} {
|
|
const categories = classifyBackup(timestamp);
|
|
const expiresAt = calculateExpiration(categories, timestamp);
|
|
return { categories, expiresAt };
|
|
}
|