275 lines
7.1 KiB
JavaScript
275 lines
7.1 KiB
JavaScript
/**
|
|
* Database Backup and Recovery Utilities
|
|
* Provides automated backup and recovery functionality for SQLite database
|
|
*/
|
|
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const { createReadStream, createWriteStream } = require('fs');
|
|
const { pipeline } = require('stream/promises');
|
|
const logger = require('./logger');
|
|
const config = require('../config/production');
|
|
|
|
class BackupManager {
|
|
constructor() {
|
|
this.dbPath = config.database.path;
|
|
this.backupDir = config.database.backupPath;
|
|
this.retentionDays = config.backup.retentionDays;
|
|
}
|
|
|
|
/**
|
|
* Initialize backup directory
|
|
*/
|
|
async initialize() {
|
|
try {
|
|
await fs.mkdir(this.backupDir, { recursive: true });
|
|
logger.info('Backup directory initialized', { path: this.backupDir });
|
|
} catch (error) {
|
|
logger.error('Failed to initialize backup directory', {
|
|
error: error.message,
|
|
path: this.backupDir
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a backup of the database
|
|
*/
|
|
async createBackup() {
|
|
try {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const backupFileName = `inventory-backup-${timestamp}.db`;
|
|
const backupPath = path.join(this.backupDir, backupFileName);
|
|
|
|
// Check if source database exists
|
|
try {
|
|
await fs.access(this.dbPath);
|
|
} catch (error) {
|
|
throw new Error(`Source database not found: ${this.dbPath}`);
|
|
}
|
|
|
|
// Create backup using file copy
|
|
await pipeline(
|
|
createReadStream(this.dbPath),
|
|
createWriteStream(backupPath)
|
|
);
|
|
|
|
// Verify backup file
|
|
const stats = await fs.stat(backupPath);
|
|
if (stats.size === 0) {
|
|
throw new Error('Backup file is empty');
|
|
}
|
|
|
|
logger.info('Database backup created successfully', {
|
|
backupPath,
|
|
size: stats.size,
|
|
timestamp
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
backupPath,
|
|
size: stats.size,
|
|
timestamp
|
|
};
|
|
} catch (error) {
|
|
logger.error('Database backup failed', { error: error.message });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restore database from backup
|
|
*/
|
|
async restoreBackup(backupPath) {
|
|
try {
|
|
// Verify backup file exists
|
|
try {
|
|
await fs.access(backupPath);
|
|
} catch (error) {
|
|
throw new Error(`Backup file not found: ${backupPath}`);
|
|
}
|
|
|
|
// Create backup of current database before restore
|
|
const currentBackupPath = `${this.dbPath}.pre-restore-${Date.now()}.bak`;
|
|
try {
|
|
await pipeline(
|
|
createReadStream(this.dbPath),
|
|
createWriteStream(currentBackupPath)
|
|
);
|
|
logger.info('Current database backed up before restore', {
|
|
path: currentBackupPath
|
|
});
|
|
} catch (error) {
|
|
logger.warn('Could not backup current database before restore', {
|
|
error: error.message
|
|
});
|
|
}
|
|
|
|
// Restore from backup
|
|
await pipeline(
|
|
createReadStream(backupPath),
|
|
createWriteStream(this.dbPath)
|
|
);
|
|
|
|
// Verify restored database
|
|
const stats = await fs.stat(this.dbPath);
|
|
if (stats.size === 0) {
|
|
throw new Error('Restored database is empty');
|
|
}
|
|
|
|
logger.info('Database restored successfully', {
|
|
backupPath,
|
|
restoredSize: stats.size
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
restoredFrom: backupPath,
|
|
size: stats.size
|
|
};
|
|
} catch (error) {
|
|
logger.error('Database restore failed', {
|
|
error: error.message,
|
|
backupPath
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List available backups
|
|
*/
|
|
async listBackups() {
|
|
try {
|
|
const files = await fs.readdir(this.backupDir);
|
|
const backups = [];
|
|
|
|
for (const file of files) {
|
|
if (file.endsWith('.db')) {
|
|
const filePath = path.join(this.backupDir, file);
|
|
const stats = await fs.stat(filePath);
|
|
|
|
backups.push({
|
|
filename: file,
|
|
path: filePath,
|
|
size: stats.size,
|
|
created: stats.birthtime,
|
|
modified: stats.mtime
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort by creation date (newest first)
|
|
backups.sort((a, b) => b.created - a.created);
|
|
|
|
return backups;
|
|
} catch (error) {
|
|
logger.error('Failed to list backups', { error: error.message });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up old backups based on retention policy
|
|
*/
|
|
async cleanupOldBackups() {
|
|
try {
|
|
const backups = await this.listBackups();
|
|
const cutoffDate = new Date();
|
|
cutoffDate.setDate(cutoffDate.getDate() - this.retentionDays);
|
|
|
|
let deletedCount = 0;
|
|
let deletedSize = 0;
|
|
|
|
for (const backup of backups) {
|
|
if (backup.created < cutoffDate) {
|
|
try {
|
|
await fs.unlink(backup.path);
|
|
deletedCount++;
|
|
deletedSize += backup.size;
|
|
logger.info('Old backup deleted', {
|
|
filename: backup.filename,
|
|
age: Math.floor((Date.now() - backup.created) / (1000 * 60 * 60 * 24))
|
|
});
|
|
} catch (error) {
|
|
logger.warn('Failed to delete old backup', {
|
|
filename: backup.filename,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.info('Backup cleanup completed', {
|
|
deletedCount,
|
|
deletedSize,
|
|
retentionDays: this.retentionDays
|
|
});
|
|
|
|
return { deletedCount, deletedSize };
|
|
} catch (error) {
|
|
logger.error('Backup cleanup failed', { error: error.message });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule automatic backups
|
|
*/
|
|
scheduleBackups() {
|
|
const interval = config.database.backupInterval;
|
|
|
|
if (!config.backup.enabled) {
|
|
logger.info('Automatic backups disabled');
|
|
return;
|
|
}
|
|
|
|
logger.info('Scheduling automatic backups', {
|
|
intervalMs: interval,
|
|
intervalHours: interval / (1000 * 60 * 60)
|
|
});
|
|
|
|
setInterval(async () => {
|
|
try {
|
|
await this.createBackup();
|
|
await this.cleanupOldBackups();
|
|
} catch (error) {
|
|
logger.error('Scheduled backup failed', { error: error.message });
|
|
}
|
|
}, interval);
|
|
}
|
|
|
|
/**
|
|
* Get backup statistics
|
|
*/
|
|
async getBackupStats() {
|
|
try {
|
|
const backups = await this.listBackups();
|
|
const totalSize = backups.reduce((sum, backup) => sum + backup.size, 0);
|
|
const oldestBackup = backups.length > 0 ? backups[backups.length - 1] : null;
|
|
const newestBackup = backups.length > 0 ? backups[0] : null;
|
|
|
|
return {
|
|
count: backups.length,
|
|
totalSize,
|
|
oldestBackup: oldestBackup ? {
|
|
filename: oldestBackup.filename,
|
|
created: oldestBackup.created,
|
|
size: oldestBackup.size
|
|
} : null,
|
|
newestBackup: newestBackup ? {
|
|
filename: newestBackup.filename,
|
|
created: newestBackup.created,
|
|
size: newestBackup.size
|
|
} : null
|
|
};
|
|
} catch (error) {
|
|
logger.error('Failed to get backup statistics', { error: error.message });
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = BackupManager; |