BillTracker/services/backupScheduler.js

165 lines
4.6 KiB
JavaScript
Raw Normal View History

2026-05-03 19:51:57 -05:00
const cron = require('node-cron');
const { getSetting, setSetting } = require('../db/database');
const { applyScheduledRetention, createBackup } = require('./backupService');
let task = null;
let nextRunAt = null;
let running = false;
function parseBool(value) {
return value === true || value === 'true';
}
function validateScheduleSettings(input = {}) {
const enabled = parseBool(input.enabled);
const frequency = input.frequency || 'daily';
const time = input.time || '02:00';
const retentionCount = parseInt(input.retention_count ?? '14', 10);
if (!['daily', 'weekly'].includes(frequency)) {
const err = new Error('frequency must be daily or weekly');
err.status = 400;
throw err;
}
if (!/^\d{2}:\d{2}$/.test(time)) {
const err = new Error('time must use HH:MM format');
err.status = 400;
throw err;
}
const [hour, minute] = time.split(':').map(Number);
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
const err = new Error('time must be a valid 24-hour time');
err.status = 400;
throw err;
}
if (!Number.isInteger(retentionCount) || retentionCount < 1 || retentionCount > 365) {
const err = new Error('retention_count must be between 1 and 365');
err.status = 400;
throw err;
}
return { enabled, frequency, time, retention_count: retentionCount };
}
function readSettings() {
return validateScheduleSettings({
enabled: getSetting('backup_schedule_enabled') === 'true',
frequency: getSetting('backup_schedule_frequency') || 'daily',
time: getSetting('backup_schedule_time') || '02:00',
retention_count: getSetting('backup_schedule_retention_count') || '14',
});
}
function getLastRunAt() {
return getSetting('backup_schedule_last_run_at') || null;
}
function getLastError() {
return getSetting('backup_schedule_last_error') || null;
}
function computeNextRun(settings = readSettings(), from = new Date()) {
if (!settings.enabled) return null;
const [hour, minute] = settings.time.split(':').map(Number);
const next = new Date(from);
next.setHours(hour, minute, 0, 0);
if (settings.frequency === 'weekly') {
while (next <= from || next.getDay() !== 0) next.setDate(next.getDate() + 1);
return next.toISOString();
}
if (next <= from) next.setDate(next.getDate() + 1);
return next.toISOString();
}
function cronExpression(settings) {
const [hour, minute] = settings.time.split(':').map(Number);
return settings.frequency === 'weekly'
? `${minute} ${hour} * * 0`
: `${minute} ${hour} * * *`;
}
async function runScheduledBackupNow() {
if (running) {
const err = new Error('Scheduled backup is already running');
err.status = 409;
throw err;
}
running = true;
try {
const settings = readSettings();
const backup = await createBackup('scheduled-backup');
applyScheduledRetention(settings.retention_count);
setSetting('backup_schedule_last_run_at', new Date().toISOString());
setSetting('backup_schedule_last_error', '');
nextRunAt = computeNextRun(settings);
return backup;
} catch (err) {
setSetting('backup_schedule_last_error', err.message || 'Scheduled backup failed');
throw err;
} finally {
running = false;
}
}
function stop() {
if (task) task.stop();
task = null;
}
function reloadSchedule() {
stop();
const settings = readSettings();
nextRunAt = computeNextRun(settings);
if (!settings.enabled) return getScheduleStatus();
task = cron.schedule(cronExpression(settings), () => {
runScheduledBackupNow().catch(err => {
console.error('[backupScheduler] Scheduled backup failed:', err.message);
});
});
return getScheduleStatus();
}
function saveSettings(input) {
const settings = validateScheduleSettings(input);
setSetting('backup_schedule_enabled', settings.enabled ? 'true' : 'false');
setSetting('backup_schedule_frequency', settings.frequency);
setSetting('backup_schedule_time', settings.time);
setSetting('backup_schedule_retention_count', String(settings.retention_count));
return reloadSchedule();
}
function getScheduleStatus() {
const settings = readSettings();
return {
ok: !getLastError(),
enabled: settings.enabled,
running,
frequency: settings.frequency,
time: settings.time,
retention_count: settings.retention_count,
last_run_at: getLastRunAt(),
next_run_at: nextRunAt ?? computeNextRun(settings),
last_error: getLastError(),
};
}
function start() {
return reloadSchedule();
}
module.exports = {
getScheduleStatus,
reloadSchedule,
runScheduledBackupNow,
saveSettings,
start,
validateScheduleSettings,
};