Files
oikos/server/services/backup-scheduler.js
Ulas Kalayci 9b29d1847c feat: automatische geplante Backups mit Rotation
Phase 1.3 - Automatische Backups:
- Cron-basierter Scheduler (Standard: täglich 2 Uhr)
- Konfigurierbar über .env (Zeitplan, Verzeichnis, Anzahl)
- Automatische Rotation: behält nur letzte N Backups (Standard: 7)
- UI in Settings → Backup: Status-Anzeige und manueller Trigger
- Tests: 7 erfolgreiche Tests für Scheduler-Funktionalität

Neue Umgebungsvariablen:
- BACKUP_ENABLED (Standard: true)
- BACKUP_SCHEDULE (Standard: 0 2 * * *)
- BACKUP_DIR (Standard: ./backups)
- BACKUP_KEEP (Standard: 7)
- TZ (für Zeitzone)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 07:02:38 +02:00

190 lines
4.4 KiB
JavaScript

/**
* Module: Backup Scheduler
* Purpose: Automated scheduled database backups with rotation
* Dependencies: node-cron, fs/promises, path, server/db.js
*/
import cron from 'node-cron';
import fs from 'node:fs/promises';
import path from 'node:path';
import { backupToFile } from '../db.js';
import { createLogger } from '../logger.js';
const log = createLogger('BackupScheduler');
// Configuration from environment variables
const BACKUP_SCHEDULE = process.env.BACKUP_SCHEDULE || '0 2 * * *'; // Default: 2 AM daily
const BACKUP_DIR = process.env.BACKUP_DIR || './backups';
const BACKUP_KEEP = parseInt(process.env.BACKUP_KEEP || '7', 10); // Default: keep last 7 backups
const BACKUP_ENABLED = process.env.BACKUP_ENABLED !== 'false'; // Default: enabled
let scheduledTask = null;
let lastBackup = null;
let lastError = null;
/**
* Generate timestamped backup filename
*/
function backupFileName() {
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
return `oikos-backup-${stamp}.db`;
}
/**
* Ensure backup directory exists
*/
async function ensureBackupDir() {
try {
await fs.mkdir(BACKUP_DIR, { recursive: true });
} catch (err) {
log.error('Failed to create backup directory:', err);
throw err;
}
}
/**
* Get all backup files sorted by modification time (newest first)
*/
async function getBackupFiles() {
try {
const files = await fs.readdir(BACKUP_DIR);
const backupFiles = files.filter((f) => f.startsWith('oikos-backup-') && f.endsWith('.db'));
// Get file stats and sort by modification time
const filesWithStats = await Promise.all(
backupFiles.map(async (file) => {
const filePath = path.join(BACKUP_DIR, file);
const stats = await fs.stat(filePath);
return { file, mtime: stats.mtime, path: filePath };
})
);
return filesWithStats.sort((a, b) => b.mtime - a.mtime);
} catch (err) {
if (err.code === 'ENOENT') {
return [];
}
throw err;
}
}
/**
* Rotate backups - keep only the last N backups
*/
async function rotateBackups() {
try {
const files = await getBackupFiles();
if (files.length <= BACKUP_KEEP) {
return; // Nothing to delete
}
const filesToDelete = files.slice(BACKUP_KEEP);
for (const { file, path: filePath } of filesToDelete) {
try {
await fs.unlink(filePath);
log.info(`Rotated old backup: ${file}`);
} catch (err) {
log.error(`Failed to delete old backup ${file}:`, err);
}
}
} catch (err) {
log.error('Backup rotation failed:', err);
}
}
/**
* Perform automated backup
*/
async function performBackup() {
try {
log.info('Starting scheduled backup...');
await ensureBackupDir();
const fileName = backupFileName();
const filePath = path.join(BACKUP_DIR, fileName);
await backupToFile(filePath);
log.info(`Backup created: ${fileName}`);
// Rotate old backups
await rotateBackups();
lastBackup = {
timestamp: new Date().toISOString(),
file: fileName,
success: true,
};
lastError = null;
} catch (err) {
log.error('Scheduled backup failed:', err);
lastError = {
timestamp: new Date().toISOString(),
message: err.message,
};
lastBackup = {
timestamp: new Date().toISOString(),
success: false,
error: err.message,
};
}
}
/**
* Start the backup scheduler
*/
export function startScheduler() {
if (!BACKUP_ENABLED) {
log.info('Automated backups are disabled (BACKUP_ENABLED=false)');
return;
}
if (!cron.validate(BACKUP_SCHEDULE)) {
log.error(`Invalid cron schedule: ${BACKUP_SCHEDULE}`);
return;
}
scheduledTask = cron.schedule(BACKUP_SCHEDULE, performBackup, {
timezone: process.env.TZ || 'UTC',
});
log.info(`Backup scheduler started: ${BACKUP_SCHEDULE} (keeping last ${BACKUP_KEEP} backups)`);
}
/**
* Stop the backup scheduler
*/
export function stopScheduler() {
if (scheduledTask) {
scheduledTask.stop();
scheduledTask = null;
log.info('Backup scheduler stopped');
}
}
/**
* Get scheduler status
*/
export function getStatus() {
return {
enabled: BACKUP_ENABLED,
schedule: BACKUP_SCHEDULE,
backupDir: BACKUP_DIR,
keepCount: BACKUP_KEEP,
running: scheduledTask !== null,
lastBackup,
lastError,
};
}
/**
* Trigger an immediate backup (for manual/testing purposes)
*/
export async function triggerBackup() {
await performBackup();
return lastBackup;
}