/** * 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;