const path = require('path'); const fs = require('fs'); const cron = require('node-cron'); let db; let backupDir; let dbPath; let cronJob = null; const DEFAULT_SETTINGS = { backup_auto_enabled: 'true', backup_auto_time: '03:00', backup_retention_days: '30' }; function init(database, mainDbPath) { db = database; dbPath = mainDbPath; backupDir = path.join(path.dirname(mainDbPath), 'backups'); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } initTable(); initDefaultSettings(); startScheduler(); } function initTable() { db.run(`CREATE TABLE IF NOT EXISTS backups ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, size INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, type TEXT DEFAULT 'manual', restored_at DATETIME )`); } function initDefaultSettings() { Object.entries(DEFAULT_SETTINGS).forEach(([key, value]) => { db.run(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, [key, value], (err) => { if (err) console.error('Backup settings init error:', err); }); }); } function createBackupFilename() { const now = new Date(); const y = now.getFullYear(); const m = String(now.getMonth() + 1).padStart(2, '0'); const d = String(now.getDate()).padStart(2, '0'); const h = String(now.getHours()).padStart(2, '0'); const min = String(now.getMinutes()).padStart(2, '0'); const s = String(now.getSeconds()).padStart(2, '0'); return `hotel_${y}-${m}-${d}_${h}-${min}-${s}.db`; } function createBackup(type = 'manual') { return new Promise((resolve, reject) => { const filename = createBackupFilename(); const backupPath = path.join(backupDir, filename); try { fs.copyFileSync(dbPath, backupPath); const stats = fs.statSync(backupPath); const size = stats.size; db.run( `INSERT INTO backups (filename, size, type) VALUES (?, ?, ?)`, [filename, size, type], function(err) { if (err) { console.error('[BACKUP] Database insert error:', err); reject(err); return; } console.log(`[BACKUP] Created backup: ${filename} (${formatBytes(size)}) [${type}]`); cleanupOldBackups((cleanupErr) => { if (cleanupErr) { console.warn('[BACKUP] Cleanup warning:', cleanupErr.message); } }); resolve({ id: this.lastID, filename, size, type, created_at: new Date().toISOString() }); } ); } catch (err) { console.error('[BACKUP] Create backup error:', err); reject(err); } }); } function listBackups(callback) { db.all(`SELECT * FROM backups ORDER BY created_at DESC`, [], (err, rows) => { if (err) { console.error('[BACKUP] List backups error:', err); return callback(err); } callback(null, rows); }); } function getBackupById(id, callback) { db.get(`SELECT * FROM backups WHERE id = ?`, [id], (err, row) => { if (err) return callback(err); if (!row) return callback(new Error('Backup not found')); callback(null, row); }); } function deleteBackup(id) { return new Promise((resolve, reject) => { getBackupById(id, (err, backup) => { if (err) return reject(err); const backupPath = path.join(backupDir, backup.filename); try { if (fs.existsSync(backupPath)) { fs.unlinkSync(backupPath); } db.run(`DELETE FROM backups WHERE id = ?`, [id], (err) => { if (err) { console.error('[BACKUP] Delete from DB error:', err); return reject(err); } console.log(`[BACKUP] Deleted backup: ${backup.filename}`); resolve(); }); } catch (fsErr) { console.error('[BACKUP] Delete file error:', fsErr); reject(fsErr); } }); }); } function restoreBackup(id) { return new Promise((resolve, reject) => { getBackupById(id, (err, backup) => { if (err) return reject(err); const backupPath = path.join(backupDir, backup.filename); if (!fs.existsSync(backupPath)) { return reject(new Error('Backup file not found')); } console.log(`[BACKUP] Starting restore from: ${backup.filename}`); createBackup('emergency_before_restore') .then((emergencyBackup) => { console.log(`[BACKUP] Emergency backup created: ${emergencyBackup.filename}`); try { const tempPath = dbPath + '.tmp'; if (fs.existsSync(dbPath)) { fs.copyFileSync(dbPath, tempPath); } fs.copyFileSync(backupPath, dbPath); if (fs.existsSync(tempPath)) { fs.unlinkSync(tempPath); } db.run( `UPDATE backups SET restored_at = CURRENT_TIMESTAMP WHERE id = ?`, [id], (updateErr) => { if (updateErr) { console.warn('[BACKUP] Update restored_at error:', updateErr); } } ); console.log(`[BACKUP] Restore completed successfully`); resolve({ message: 'Backup restored successfully', restored_backup: backup.filename, emergency_backup: emergencyBackup.filename }); } catch (copyErr) { console.error('[BACKUP] Restore copy error:', copyErr); const tempPath = dbPath + '.tmp'; if (fs.existsSync(tempPath)) { try { fs.copyFileSync(tempPath, dbPath); fs.unlinkSync(tempPath); } catch (restoreErr) { console.error('[BACKUP] Rollback failed:', restoreErr); } } reject(copyErr); } }) .catch((emergencyErr) => { console.error('[BACKUP] Emergency backup failed, aborting restore:', emergencyErr); reject(new Error('Failed to create emergency backup, restore aborted')); }); }); }); } function cleanupOldBackups(callback) { db.get(`SELECT value FROM settings WHERE key = 'backup_retention_days'`, [], (err, row) => { if (err || !row) { return callback(err || new Error('Retention days not set')); } const retentionDays = parseInt(row.value, 10) || 30; const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); const cutoffStr = cutoffDate.toISOString(); db.all(`SELECT * FROM backups WHERE created_at < ?`, [cutoffStr], (err, oldBackups) => { if (err) return callback(err); let deleted = 0; if (oldBackups.length === 0) return callback(null, 0); oldBackups.forEach((backup) => { const backupPath = path.join(backupDir, backup.filename); try { if (fs.existsSync(backupPath)) { fs.unlinkSync(backupPath); deleted++; } } catch (e) { console.warn(`[BACKUP] Could not delete ${backup.filename}:`, e.message); } db.run(`DELETE FROM backups WHERE id = ?`, [backup.id], (delErr) => { if (delErr) console.warn(`[BACKUP] DB delete error for ${backup.id}:`, delErr); }); }); if (deleted > 0) { console.log(`[BACKUP] Cleaned up ${deleted} old backup(s)`); } callback(null, deleted); }); }); } function startScheduler() { db.get(`SELECT value FROM settings WHERE key = 'backup_auto_enabled'`, [], (err, row) => { const enabled = !err && row && row.value === 'true'; if (!enabled) { console.log('[BACKUP] Auto backup is disabled'); return; } scheduleBackup(); }); } function scheduleBackup() { if (cronJob) { cronJob.stop(); cronJob = null; } db.get(`SELECT value FROM settings WHERE key = 'backup_auto_time'`, [], (err, row) => { const time = (!err && row) ? row.value : '03:00'; const [hours, minutes] = time.split(':'); const cronExpression = `${minutes} ${hours} * * *`; cronJob = cron.schedule(cronExpression, async () => { console.log('[BACKUP] Running scheduled backup...'); try { const result = await createBackup('auto'); console.log(`[BACKUP] Scheduled backup completed: ${result.filename}`); } catch (err) { console.error('[BACKUP] Scheduled backup failed:', err); } }); console.log(`[BACKUP] Scheduler started: daily at ${time}`); }); } function updateSchedulerSettings() { db.get(`SELECT value FROM settings WHERE key = 'backup_auto_enabled'`, [], (err, row) => { const enabled = !err && row && row.value === 'true'; if (!enabled) { if (cronJob) { cronJob.stop(); cronJob = null; console.log('[BACKUP] Scheduler stopped (disabled)'); } return; } scheduleBackup(); }); } function getSettings(callback) { db.all(`SELECT key, value FROM settings WHERE key LIKE 'backup_%'`, [], (err, rows) => { if (err) return callback(err); const settings = {}; rows.forEach(row => { settings[row.key] = row.value; }); callback(null, settings); }); } function updateSettings(settings, callback) { const entries = Object.entries(settings); let completed = 0; let hasError = false; entries.forEach(([key, value]) => { if (hasError) return; db.run( `INSERT INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP`, [key, value, value], (err) => { if (err) { console.error('[BACKUP] Settings update error:', err); hasError = true; callback(err); return; } completed++; if (completed === entries.length) { updateSchedulerSettings(); callback(null); } } ); }); } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function setupRoutes(app, authenticateToken, requireAdmin) { app.post('/api/admin/backup', authenticateToken, requireAdmin, async (req, res) => { try { const backup = await createBackup('manual'); res.json({ success: true, backup }); } catch (err) { console.error('[BACKUP] API create backup error:', err); res.status(500).json({ error: 'Failed to create backup' }); } }); app.get('/api/admin/backups', authenticateToken, requireAdmin, (req, res) => { listBackups((err, backups) => { if (err) { console.error('[BACKUP] API list backups error:', err); return res.status(500).json({ error: 'Failed to list backups' }); } res.json({ backups }); }); }); app.delete('/api/admin/backups/:id', authenticateToken, requireAdmin, async (req, res) => { try { await deleteBackup(parseInt(req.params.id, 10)); res.json({ success: true }); } catch (err) { console.error('[BACKUP] API delete backup error:', err); res.status(500).json({ error: err.message || 'Failed to delete backup' }); } }); app.post('/api/admin/backups/:id/restore', authenticateToken, requireAdmin, async (req, res) => { try { const result = await restoreBackup(parseInt(req.params.id, 10)); res.json({ success: true, ...result }); } catch (err) { console.error('[BACKUP] API restore backup error:', err); res.status(500).json({ error: err.message || 'Failed to restore backup' }); } }); app.get('/api/admin/backups/:id/download', authenticateToken, requireAdmin, (req, res) => { const id = parseInt(req.params.id, 10); getBackupById(id, (err, backup) => { if (err) return res.status(404).json({ error: 'Backup not found' }); const backupPath = path.join(backupDir, backup.filename); if (!fs.existsSync(backupPath)) { return res.status(404).json({ error: 'Backup file not found' }); } res.download(backupPath, backup.filename); }); }); app.get('/api/admin/backup/settings', authenticateToken, requireAdmin, (req, res) => { getSettings((err, settings) => { if (err) return res.status(500).json({ error: 'Failed to get settings' }); res.json(settings); }); }); app.put('/api/admin/backup/settings', authenticateToken, requireAdmin, (req, res) => { const { backup_auto_enabled, backup_auto_time, backup_retention_days } = req.body; const settings = {}; if (backup_auto_enabled !== undefined) settings.backup_auto_enabled = String(backup_auto_enabled); if (backup_auto_time !== undefined) settings.backup_auto_time = backup_auto_time; if (backup_retention_days !== undefined) settings.backup_retention_days = String(backup_retention_days); if (Object.keys(settings).length === 0) { return res.status(400).json({ error: 'No settings provided' }); } updateSettings(settings, (err) => { if (err) return res.status(500).json({ error: 'Failed to update settings' }); res.json({ success: true }); }); }); } module.exports = { init, createBackup, listBackups, deleteBackup, restoreBackup, getSettings, updateSettings, setupRoutes };