mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
Add configurable backup schedule with admin UI
- Make backup frequency (6h/12h/1d/7d), time, and day-of-week configurable via settings, stored in appSettings key-value table - Add rescheduleBackup() to dynamically update cron job at runtime - Differentiate manual (sofa-manual-*) vs scheduled (sofa-scheduled-*) backup filenames with source icons and tooltips in the backup list - Pruning now only targets scheduled backups, leaving manual ones intact - Merge backup-actions.ts into actions.ts, use shadcn DropdownMenu for all selectors (consistent with dashboard stats cards) - Fix hydration mismatch on relative backup timestamps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+45
-2
@@ -29,6 +29,8 @@ import {
|
||||
import { getSetting } from "@/lib/services/settings";
|
||||
import { getTvDetails } from "@/lib/tmdb/client";
|
||||
|
||||
export type BackupFrequency = "6h" | "12h" | "1d" | "7d";
|
||||
|
||||
const log = createLogger("cron");
|
||||
|
||||
const DAY = 24 * 60 * 60 * 1000;
|
||||
@@ -229,17 +231,58 @@ async function scheduledBackupJob() {
|
||||
}
|
||||
|
||||
await ensureBackupDir();
|
||||
await createBackup();
|
||||
await createBackup("sofa-scheduled");
|
||||
|
||||
const maxStr = getSetting("maxBackupRetention");
|
||||
const max = maxStr ? Number.parseInt(maxStr, 10) : 7;
|
||||
await pruneBackups(max);
|
||||
}
|
||||
|
||||
export function buildBackupCron(
|
||||
frequency: BackupFrequency = "1d",
|
||||
time = "02:00",
|
||||
dayOfWeek = 0,
|
||||
): string {
|
||||
const [hour, minute] = time.split(":").map(Number);
|
||||
const h = Number.isFinite(hour) ? hour : 2;
|
||||
const m = Number.isFinite(minute) ? minute : 0;
|
||||
const dow = dayOfWeek >= 0 && dayOfWeek <= 6 ? dayOfWeek : 0;
|
||||
|
||||
switch (frequency) {
|
||||
case "6h":
|
||||
return `${m} */6 * * *`;
|
||||
case "12h":
|
||||
return `${m} ${h},${(h + 12) % 24} * * *`;
|
||||
case "7d":
|
||||
return `${m} ${h} * * ${dow}`;
|
||||
default:
|
||||
return `${m} ${h} * * *`;
|
||||
}
|
||||
}
|
||||
|
||||
function getBackupCronFromSettings(): string {
|
||||
const frequency = (getSetting("backupScheduleFrequency") ??
|
||||
"1d") as BackupFrequency;
|
||||
const time = getSetting("backupScheduleTime") ?? "02:00";
|
||||
const dayOfWeek = Number.parseInt(getSetting("backupScheduleDow") ?? "0", 10);
|
||||
return buildBackupCron(frequency, time, dayOfWeek);
|
||||
}
|
||||
|
||||
export function rescheduleBackup() {
|
||||
const existing = jobs.get("scheduledBackup");
|
||||
if (existing) {
|
||||
existing.stop();
|
||||
jobs.delete("scheduledBackup");
|
||||
}
|
||||
const cron = getBackupCronFromSettings();
|
||||
log.info(`Rescheduling backup job with cron: ${cron}`);
|
||||
schedule("scheduledBackup", cron, scheduledBackupJob);
|
||||
}
|
||||
|
||||
export function startJobs() {
|
||||
if (jobs.size > 0) return;
|
||||
|
||||
schedule("scheduledBackup", "0 2 * * *", scheduledBackupJob);
|
||||
schedule("scheduledBackup", getBackupCronFromSettings(), scheduledBackupJob);
|
||||
schedule("nightlyRefreshLibrary", "0 3 * * *", nightlyRefreshLibrary);
|
||||
schedule("refreshAvailability", "0 */6 * * *", refreshAvailabilityJob);
|
||||
schedule("refreshRecommendations", "0 */12 * * *", refreshRecommendationsJob);
|
||||
|
||||
+25
-9
@@ -13,13 +13,31 @@ const DATABASE_URL =
|
||||
process.env.DATABASE_URL || path.join(DATA_DIR, "sqlite.db");
|
||||
const BACKUP_DIR = path.join(DATA_DIR, "backups");
|
||||
|
||||
const BACKUP_PATTERN = /^sofa-backup-\d{4}-\d{2}-\d{2}-\d{6}\.db$/;
|
||||
const MANUAL_PATTERN = /^sofa-manual-\d{4}-\d{2}-\d{2}-\d{6}\.db$/;
|
||||
const SCHEDULED_PATTERN = /^sofa-scheduled-\d{4}-\d{2}-\d{2}-\d{6}\.db$/;
|
||||
const PRE_RESTORE_PATTERN = /^pre-restore-\d{4}-\d{2}-\d{2}-\d{6}\.db$/;
|
||||
|
||||
export type BackupSource = "manual" | "scheduled" | "pre-restore";
|
||||
|
||||
export interface BackupInfo {
|
||||
filename: string;
|
||||
sizeBytes: number;
|
||||
createdAt: string;
|
||||
source: BackupSource;
|
||||
}
|
||||
|
||||
function getBackupSource(filename: string): BackupSource {
|
||||
if (SCHEDULED_PATTERN.test(filename)) return "scheduled";
|
||||
if (PRE_RESTORE_PATTERN.test(filename)) return "pre-restore";
|
||||
return "manual";
|
||||
}
|
||||
|
||||
function isKnownBackup(filename: string): boolean {
|
||||
return (
|
||||
MANUAL_PATTERN.test(filename) ||
|
||||
SCHEDULED_PATTERN.test(filename) ||
|
||||
PRE_RESTORE_PATTERN.test(filename)
|
||||
);
|
||||
}
|
||||
|
||||
export async function ensureBackupDir() {
|
||||
@@ -29,14 +47,12 @@ export async function ensureBackupDir() {
|
||||
function isValidBackupFilename(filename: string): boolean {
|
||||
const base = path.basename(filename);
|
||||
return (
|
||||
base === filename &&
|
||||
!filename.includes("..") &&
|
||||
(BACKUP_PATTERN.test(filename) || PRE_RESTORE_PATTERN.test(filename))
|
||||
base === filename && !filename.includes("..") && isKnownBackup(filename)
|
||||
);
|
||||
}
|
||||
|
||||
export async function createBackup(
|
||||
prefix = "sofa-backup",
|
||||
prefix = "sofa-manual",
|
||||
): Promise<BackupInfo> {
|
||||
await ensureBackupDir();
|
||||
|
||||
@@ -54,15 +70,14 @@ export async function createBackup(
|
||||
filename,
|
||||
sizeBytes: s.size,
|
||||
createdAt: s.mtime.toISOString(),
|
||||
source: getBackupSource(filename),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listBackups(): Promise<BackupInfo[]> {
|
||||
await ensureBackupDir();
|
||||
|
||||
const files = (await readdir(BACKUP_DIR)).filter(
|
||||
(f) => BACKUP_PATTERN.test(f) || PRE_RESTORE_PATTERN.test(f),
|
||||
);
|
||||
const files = (await readdir(BACKUP_DIR)).filter((f) => isKnownBackup(f));
|
||||
|
||||
const results: BackupInfo[] = [];
|
||||
for (const filename of files) {
|
||||
@@ -71,6 +86,7 @@ export async function listBackups(): Promise<BackupInfo[]> {
|
||||
filename,
|
||||
sizeBytes: s.size,
|
||||
createdAt: s.mtime.toISOString(),
|
||||
source: getBackupSource(filename),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,7 +180,7 @@ export async function restoreFromBackup(buffer: Buffer): Promise<void> {
|
||||
|
||||
export async function pruneBackups(maxKeep: number): Promise<void> {
|
||||
const backups = (await listBackups()).filter((b) =>
|
||||
BACKUP_PATTERN.test(b.filename),
|
||||
SCHEDULED_PATTERN.test(b.filename),
|
||||
);
|
||||
|
||||
if (backups.length <= maxKeep) return;
|
||||
|
||||
Reference in New Issue
Block a user