104 lines
2.9 KiB
JavaScript
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();
|
|
}
|