Initial commit: Inventory Barcode System
This commit is contained in:
275
utils/backup.js
Normal file
275
utils/backup.js
Normal file
@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user