Files

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;