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, };