Initial commit
Dieser Commit ist enthalten in:
183
backend/utils/backup.js
Normale Datei
183
backend/utils/backup.js
Normale Datei
@ -0,0 +1,183 @@
|
||||
/**
|
||||
* TASKMATE - Backup System
|
||||
* ========================
|
||||
* Automatische Datenbank-Backups
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const logger = require('./logger');
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data');
|
||||
const BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, '..', 'backups');
|
||||
const DB_FILE = path.join(DATA_DIR, 'taskmate.db');
|
||||
|
||||
// Backup-Verzeichnis erstellen falls nicht vorhanden
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup erstellen
|
||||
*/
|
||||
function createBackup() {
|
||||
try {
|
||||
if (!fs.existsSync(DB_FILE)) {
|
||||
logger.warn('Keine Datenbank zum Sichern gefunden');
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupName = `backup_${timestamp}.db`;
|
||||
const backupPath = path.join(BACKUP_DIR, backupName);
|
||||
|
||||
// Datenbank kopieren
|
||||
fs.copyFileSync(DB_FILE, backupPath);
|
||||
|
||||
// WAL-Datei auch sichern falls vorhanden
|
||||
const walFile = DB_FILE + '-wal';
|
||||
if (fs.existsSync(walFile)) {
|
||||
fs.copyFileSync(walFile, backupPath + '-wal');
|
||||
}
|
||||
|
||||
logger.info(`Backup erstellt: ${backupName}`);
|
||||
|
||||
// Alte Backups aufräumen (behalte nur die letzten 30)
|
||||
cleanupOldBackups(30);
|
||||
|
||||
return backupPath;
|
||||
} catch (error) {
|
||||
logger.error('Backup-Fehler:', { error: error.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alte Backups löschen
|
||||
*/
|
||||
function cleanupOldBackups(keepCount = 30) {
|
||||
try {
|
||||
const files = fs.readdirSync(BACKUP_DIR)
|
||||
.filter(f => f.startsWith('backup_') && f.endsWith('.db'))
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
const toDelete = files.slice(keepCount);
|
||||
|
||||
toDelete.forEach(file => {
|
||||
const filePath = path.join(BACKUP_DIR, file);
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
// WAL-Datei auch löschen falls vorhanden
|
||||
const walPath = filePath + '-wal';
|
||||
if (fs.existsSync(walPath)) {
|
||||
fs.unlinkSync(walPath);
|
||||
}
|
||||
|
||||
logger.info(`Altes Backup gelöscht: ${file}`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Aufräumen alter Backups:', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup wiederherstellen
|
||||
*/
|
||||
function restoreBackup(backupName) {
|
||||
try {
|
||||
const backupPath = path.join(BACKUP_DIR, backupName);
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
throw new Error(`Backup nicht gefunden: ${backupName}`);
|
||||
}
|
||||
|
||||
// Aktuelles DB sichern bevor überschrieben wird
|
||||
if (fs.existsSync(DB_FILE)) {
|
||||
const safetyBackup = DB_FILE + '.before-restore';
|
||||
fs.copyFileSync(DB_FILE, safetyBackup);
|
||||
}
|
||||
|
||||
// Backup wiederherstellen
|
||||
fs.copyFileSync(backupPath, DB_FILE);
|
||||
|
||||
// WAL-Datei auch wiederherstellen falls vorhanden
|
||||
const walBackup = backupPath + '-wal';
|
||||
if (fs.existsSync(walBackup)) {
|
||||
fs.copyFileSync(walBackup, DB_FILE + '-wal');
|
||||
}
|
||||
|
||||
logger.info(`Backup wiederhergestellt: ${backupName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Restore-Fehler:', { error: error.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste aller Backups
|
||||
*/
|
||||
function listBackups() {
|
||||
try {
|
||||
const files = fs.readdirSync(BACKUP_DIR)
|
||||
.filter(f => f.startsWith('backup_') && f.endsWith('.db'))
|
||||
.map(f => {
|
||||
const filePath = path.join(BACKUP_DIR, f);
|
||||
const stats = fs.statSync(filePath);
|
||||
return {
|
||||
name: f,
|
||||
size: stats.size,
|
||||
created: stats.birthtime
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.created - a.created);
|
||||
|
||||
return files;
|
||||
} catch (error) {
|
||||
logger.error('Fehler beim Auflisten der Backups:', { error: error.message });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup-Scheduler starten
|
||||
*/
|
||||
let schedulerInterval = null;
|
||||
|
||||
function startScheduler() {
|
||||
const intervalHours = parseInt(process.env.BACKUP_INTERVAL_HOURS) || 24;
|
||||
const intervalMs = intervalHours * 60 * 60 * 1000;
|
||||
|
||||
// Erstes Backup nach 1 Minute
|
||||
setTimeout(() => {
|
||||
createBackup();
|
||||
}, 60 * 1000);
|
||||
|
||||
// Regelmäßige Backups
|
||||
schedulerInterval = setInterval(() => {
|
||||
createBackup();
|
||||
}, intervalMs);
|
||||
|
||||
logger.info(`Backup-Scheduler gestartet (alle ${intervalHours} Stunden)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup-Scheduler stoppen
|
||||
*/
|
||||
function stopScheduler() {
|
||||
if (schedulerInterval) {
|
||||
clearInterval(schedulerInterval);
|
||||
schedulerInterval = null;
|
||||
logger.info('Backup-Scheduler gestoppt');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createBackup,
|
||||
restoreBackup,
|
||||
listBackups,
|
||||
startScheduler,
|
||||
stopScheduler,
|
||||
cleanupOldBackups
|
||||
};
|
||||
94
backend/utils/logger.js
Normale Datei
94
backend/utils/logger.js
Normale Datei
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* TASKMATE - Logger
|
||||
* =================
|
||||
* Einfaches Logging mit Datei-Ausgabe
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const LOG_DIR = process.env.LOG_DIR || path.join(__dirname, '..', 'logs');
|
||||
const LOG_FILE = path.join(LOG_DIR, 'app.log');
|
||||
|
||||
// Log-Verzeichnis erstellen falls nicht vorhanden
|
||||
if (!fs.existsSync(LOG_DIR)) {
|
||||
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-Level
|
||||
*/
|
||||
const LEVELS = {
|
||||
ERROR: 'ERROR',
|
||||
WARN: 'WARN',
|
||||
INFO: 'INFO',
|
||||
DEBUG: 'DEBUG'
|
||||
};
|
||||
|
||||
/**
|
||||
* Timestamp formatieren
|
||||
*/
|
||||
function getTimestamp() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-Nachricht schreiben
|
||||
*/
|
||||
function log(level, message, meta = {}) {
|
||||
const timestamp = getTimestamp();
|
||||
const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
|
||||
const logLine = `[${timestamp}] [${level}] ${message}${metaStr}`;
|
||||
|
||||
// Konsole
|
||||
if (level === LEVELS.ERROR) {
|
||||
console.error(logLine);
|
||||
} else if (level === LEVELS.WARN) {
|
||||
console.warn(logLine);
|
||||
} else {
|
||||
console.log(logLine);
|
||||
}
|
||||
|
||||
// Datei (async, non-blocking)
|
||||
fs.appendFile(LOG_FILE, logLine + '\n', (err) => {
|
||||
if (err) console.error('Log-Datei Fehler:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-Rotation (alte Logs löschen)
|
||||
*/
|
||||
function rotateLogsIfNeeded() {
|
||||
try {
|
||||
const stats = fs.statSync(LOG_FILE);
|
||||
const maxSize = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
if (stats.size > maxSize) {
|
||||
const archiveName = `app.${Date.now()}.log`;
|
||||
fs.renameSync(LOG_FILE, path.join(LOG_DIR, archiveName));
|
||||
|
||||
// Alte Archive löschen (behalte nur die letzten 5)
|
||||
const files = fs.readdirSync(LOG_DIR)
|
||||
.filter(f => f.startsWith('app.') && f.endsWith('.log') && f !== 'app.log')
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
files.slice(5).forEach(f => {
|
||||
fs.unlinkSync(path.join(LOG_DIR, f));
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// Datei existiert noch nicht, ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Log-Rotation beim Start und alle 6 Stunden prüfen
|
||||
rotateLogsIfNeeded();
|
||||
setInterval(rotateLogsIfNeeded, 6 * 60 * 60 * 1000);
|
||||
|
||||
module.exports = {
|
||||
error: (message, meta) => log(LEVELS.ERROR, message, meta),
|
||||
warn: (message, meta) => log(LEVELS.WARN, message, meta),
|
||||
info: (message, meta) => log(LEVELS.INFO, message, meta),
|
||||
debug: (message, meta) => log(LEVELS.DEBUG, message, meta)
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren