import crypto from 'node:crypto'; import { readFile, rename, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { startBackupForVm } from './routes/backup.js'; const schedulesPath = path.resolve(process.cwd(), 'schedules.json'); const checkIntervalMs = 30 * 1000; let schedules = []; let timer = null; export async function startScheduler() { await loadSchedules(); if (timer) return; timer = setInterval(runDueSchedules, checkIntervalMs); timer.unref?.(); runDueSchedules(); } export function listSchedules() { return schedules .map((schedule) => ({ ...schedule })) .sort((a, b) => a.vmName.localeCompare(b.vmName)); } export async function replaceSchedules(nextSchedules) { schedules = normalizeSchedules(nextSchedules); await saveSchedules(); return listSchedules(); } async function runDueSchedules() { const now = new Date(); let changed = false; for (const schedule of schedules) { if (!schedule.enabled) continue; if (!schedule.nextRunAt) { schedule.nextRunAt = nextRunFrom(now, schedule.intervalHours); changed = true; continue; } if (new Date(schedule.nextRunAt) > now) continue; try { await startBackupForVm(schedule.vmName); schedule.lastRunAt = now.toISOString(); schedule.lastError = ''; schedule.nextRunAt = nextRunFrom(now, schedule.intervalHours); } catch (error) { schedule.lastError = String(error.message || error); schedule.nextRunAt = nextRunFrom(new Date(now.getTime() + 5 * 60 * 1000), schedule.intervalHours); } changed = true; } if (changed) { await saveSchedules(); } } async function loadSchedules() { try { const content = await readFile(schedulesPath, 'utf8'); schedules = normalizeSchedules(JSON.parse(content)); } catch (error) { if (error.code === 'ENOENT') { schedules = []; return; } throw error; } } async function saveSchedules() { const tempPath = `${schedulesPath}.tmp`; await writeFile(tempPath, `${JSON.stringify(schedules, null, 2)}\n`, { mode: 0o600 }); await rename(tempPath, schedulesPath); } function normalizeSchedules(values) { const seen = new Set(); return (Array.isArray(values) ? values : []) .map((value) => { const vmName = String(value.vmName || '').trim(); const intervalHours = Math.max(1, Number(value.intervalHours) || 24); if (!vmName || seen.has(vmName)) return null; seen.add(vmName); return { id: value.id || `schedule_${crypto.randomBytes(8).toString('hex')}`, vmName, enabled: Boolean(value.enabled), intervalHours, nextRunAt: value.nextRunAt || nextRunFrom(new Date(), intervalHours), lastRunAt: value.lastRunAt || null, lastError: value.lastError || '', }; }) .filter(Boolean); } function nextRunFrom(date, intervalHours) { return new Date(date.getTime() + intervalHours * 60 * 60 * 1000).toISOString(); }