Files
hotell777_260507/modules/backup/index.js
2026-05-11 19:41:49 +05:00

453 lines
16 KiB
JavaScript

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