Files
incus-backup-ui/backend/src/scheduler.js
T

104 lines
2.9 KiB
JavaScript

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();
}