/** * Automated Backup & Restore Manager * Handles scheduled backups with local storage */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const crypto = require('crypto'); const EventEmitter = require('events'); const BACKUP_CONFIG_FILE = process.env.BACKUP_CONFIG_FILE || path.join(__dirname, 'backup-config.json'); const BACKUP_HISTORY_FILE = process.env.BACKUP_HISTORY_FILE || path.join(__dirname, 'backup-history.json'); const DEFAULT_BACKUP_DIR = process.env.BACKUP_DIR || path.join(__dirname, 'backups'); class BackupManager extends EventEmitter { constructor() { super(); this.config = this.loadConfig(); this.history = this.loadHistory(); this.scheduledJobs = new Map(); this.running = false; } /** * Start backup scheduler */ start() { if (this.running) return; console.log('[BackupManager] Starting backup scheduler'); this.running = true; // Schedule all configured backups for (const [name, backup] of Object.entries(this.config.backups || {})) { if (backup.enabled && backup.schedule) { this.scheduleBackup(name, backup); } } } /** * Stop backup scheduler */ stop() { if (!this.running) return; console.log('[BackupManager] Stopping backup scheduler'); this.running = false; // Clear all scheduled jobs for (const [name, job] of this.scheduledJobs.entries()) { clearInterval(job); } this.scheduledJobs.clear(); } /** * Schedule a backup job */ scheduleBackup(name, backup) { // Parse schedule (cron-like: daily, weekly, monthly, or interval in minutes) let intervalMs; switch (backup.schedule) { case 'hourly': intervalMs = 60 * 60 * 1000; break; case 'daily': intervalMs = 24 * 60 * 60 * 1000; break; case 'weekly': intervalMs = 7 * 24 * 60 * 60 * 1000; break; case 'monthly': intervalMs = 30 * 24 * 60 * 60 * 1000; break; default: { // Custom interval in minutes const minutes = parseInt(backup.schedule, 10); if (!isNaN(minutes) && minutes > 0) { intervalMs = minutes * 60 * 1000; } else { console.error(`[BackupManager] Invalid schedule for ${name}: ${backup.schedule}`); return; } } } // Schedule the job const job = setInterval(() => { this.executeBackup(name, backup).catch(error => { console.error(`[BackupManager] Scheduled backup ${name} failed:`, error.message); }); }, intervalMs); this.scheduledJobs.set(name, job); console.log(`[BackupManager] Scheduled backup '${name}' every ${backup.schedule}`); // Run immediately if configured if (backup.runImmediately) { this.executeBackup(name, backup).catch(error => { console.error(`[BackupManager] Initial backup ${name} failed:`, error.message); }); } } /** * Execute a backup */ async executeBackup(name, backup) { const startTime = Date.now(); const backupId = `${name}-${Date.now()}`; console.log(`[BackupManager] Starting backup: ${name}`); this.emit('backup-start', { name, backupId, timestamp: new Date().toISOString() }); try { // Create backup data const backupData = await this.createBackupData(backup.include || ['all']); // Compress backup const compressed = await this.compressBackup(backupData); // Encrypt if configured let finalData = compressed; if (backup.encrypt && backup.encryptionKey) { finalData = await this.encryptBackup(compressed, backup.encryptionKey); } // Calculate checksum const checksum = this.calculateChecksum(finalData); // Save to destinations const destinations = backup.destinations || [{ type: 'local' }]; const savedLocations = []; for (const dest of destinations) { try { const location = await this.saveToDestination(finalData, dest, backupId); savedLocations.push(location); } catch (error) { console.error(`[BackupManager] Failed to save to ${dest.type}:`, error.message); } } if (savedLocations.length === 0) { throw new Error('Failed to save backup to any destination'); } // Verify backup if (backup.verify !== false) { await this.verifyBackup(savedLocations[0], checksum); } // Record in history const duration = Date.now() - startTime; const historyEntry = { id: backupId, name, timestamp: new Date().toISOString(), duration, size: finalData.length, checksum, locations: savedLocations, encrypted: !!backup.encrypt, compressed: true, status: 'success' }; this.addToHistory(historyEntry); // Cleanup old backups if (backup.retention) { await this.cleanupOldBackups(name, backup.retention); } this.emit('backup-complete', historyEntry); console.log(`[BackupManager] Backup ${name} completed in ${duration}ms`); return historyEntry; } catch (error) { const duration = Date.now() - startTime; const historyEntry = { id: backupId, name, timestamp: new Date().toISOString(), duration, status: 'failed', error: error.message }; this.addToHistory(historyEntry); this.emit('backup-failed', historyEntry); throw error; } } /** * Create backup data from specified sources */ async createBackupData(include) { const data = { version: '1.0', timestamp: new Date().toISOString(), hostname: require('os').hostname(), data: {} }; for (const source of include) { switch (source) { case 'all': data.data.services = this.backupServices(); data.data.config = this.backupConfig(); data.data.credentials = this.backupCredentials(); data.data.stats = this.backupStats(); break; case 'services': data.data.services = this.backupServices(); break; case 'config': data.data.config = this.backupConfig(); break; case 'credentials': data.data.credentials = this.backupCredentials(); break; case 'stats': data.data.stats = this.backupStats(); break; case 'volumes': data.data.volumes = await this.backupVolumes(); break; } } return data; } /** * Backup services configuration */ backupServices() { try { const servicesFile = process.env.SERVICES_FILE || path.join(__dirname, 'services.json'); if (fs.existsSync(servicesFile)) { return JSON.parse(fs.readFileSync(servicesFile, 'utf8')); } } catch (error) { console.error('[BackupManager] Error backing up services:', error.message); } return null; } /** * Backup configuration files */ backupConfig() { try { const configFile = process.env.CONFIG_FILE || path.join(__dirname, 'config.json'); if (fs.existsSync(configFile)) { return JSON.parse(fs.readFileSync(configFile, 'utf8')); } } catch (error) { console.error('[BackupManager] Error backing up config:', error.message); } return null; } /** * Backup credentials (encrypted) */ backupCredentials() { try { const credentialManager = require('./credential-manager'); return credentialManager.exportBackup(); } catch (error) { console.error('[BackupManager] Error backing up credentials:', error.message); } return null; } /** * Backup resource stats */ backupStats() { try { const resourceMonitor = require('./resource-monitor'); return resourceMonitor.exportStats(); } catch (error) { console.error('[BackupManager] Error backing up stats:', error.message); } return null; } /** * Backup Docker volumes * Creates tar archives of Docker volumes for backup * @returns {Object|null} Volume backup metadata or null on failure */ async backupVolumes() { try { const Docker = require('dockerode'); const docker = new Docker(); // Get list of all volumes const volumeData = await docker.listVolumes(); const volumes = volumeData.Volumes || []; if (volumes.length === 0) { return { volumes: [], message: 'No volumes found' }; } const backupDir = path.join(DEFAULT_BACKUP_DIR, 'volumes'); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } const timestamp = Date.now(); const backupResults = []; for (const volume of volumes) { try { const volumeName = volume.Name; const backupFile = path.join(backupDir, `${volumeName}-${timestamp}.tar.gz`); // Create a temporary container to backup the volume // Using alpine with tar to create the archive const container = await docker.createContainer({ Image: 'alpine:latest', Cmd: ['tar', 'czf', '/backup/volume.tar.gz', '-C', '/volume', '.'], HostConfig: { Binds: [ `${volumeName}:/volume:ro`, `${backupDir}:/backup` ], AutoRemove: true } }); // Start and wait for completion await container.start(); await container.wait(); // Rename the backup file to include volume name const tempFile = path.join(backupDir, 'volume.tar.gz'); if (fs.existsSync(tempFile)) { fs.renameSync(tempFile, backupFile); const stats = fs.statSync(backupFile); backupResults.push({ name: volumeName, driver: volume.Driver, path: backupFile, size: stats.size, timestamp: new Date().toISOString(), status: 'success' }); } } catch (volumeError) { console.error(`[BackupManager] Error backing up volume ${volume.Name}:`, volumeError.message); backupResults.push({ name: volume.Name, status: 'failed', error: volumeError.message }); } } return { timestamp: new Date().toISOString(), totalVolumes: volumes.length, successCount: backupResults.filter(r => r.status === 'success').length, volumes: backupResults }; } catch (error) { console.error('[BackupManager] Error backing up volumes:', error.message); return null; } } /** * Restore Docker volumes from backup * @param {Object} volumeBackup - Volume backup metadata from backupVolumes() * @returns {Object} Restore results */ async restoreVolumes(volumeBackup) { if (!volumeBackup || !volumeBackup.volumes) { throw new Error('Invalid volume backup data'); } const Docker = require('dockerode'); const docker = new Docker(); const restoreResults = []; for (const volBackup of volumeBackup.volumes) { if (volBackup.status !== 'success' || !volBackup.path) { continue; } try { // Check if backup file exists if (!fs.existsSync(volBackup.path)) { throw new Error(`Backup file not found: ${volBackup.path}`); } const volumeName = volBackup.name; const backupDir = path.dirname(volBackup.path); // Create volume if it doesn't exist try { await docker.createVolume({ Name: volumeName }); } catch (e) { // Volume might already exist, that's OK } // Copy backup file to a temp name for mounting const tempBackupFile = path.join(backupDir, 'restore-volume.tar.gz'); fs.copyFileSync(volBackup.path, tempBackupFile); // Create container to restore the volume const container = await docker.createContainer({ Image: 'alpine:latest', Cmd: ['sh', '-c', 'rm -rf /volume/* && tar xzf /backup/restore-volume.tar.gz -C /volume'], HostConfig: { Binds: [ `${volumeName}:/volume`, `${backupDir}:/backup:ro` ], AutoRemove: true } }); await container.start(); await container.wait(); // Clean up temp file if (fs.existsSync(tempBackupFile)) { fs.unlinkSync(tempBackupFile); } restoreResults.push({ name: volumeName, status: 'success', timestamp: new Date().toISOString() }); console.log(`[BackupManager] Volume ${volumeName} restored successfully`); } catch (restoreError) { console.error(`[BackupManager] Error restoring volume ${volBackup.name}:`, restoreError.message); restoreResults.push({ name: volBackup.name, status: 'failed', error: restoreError.message }); } } return { timestamp: new Date().toISOString(), results: restoreResults, successCount: restoreResults.filter(r => r.status === 'success').length, failedCount: restoreResults.filter(r => r.status === 'failed').length }; } /** * Compress backup data */ async compressBackup(data) { const zlib = require('zlib'); const json = JSON.stringify(data); return zlib.gzipSync(json); } /** * Decompress backup data */ async decompressBackup(compressed) { const zlib = require('zlib'); const json = zlib.gunzipSync(compressed).toString(); return JSON.parse(json); } /** * Encrypt backup data */ async encryptBackup(data, key) { const algorithm = 'aes-256-gcm'; const keyBuffer = Buffer.from(key, 'hex'); const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, keyBuffer, iv); let encrypted = cipher.update(data); encrypted = Buffer.concat([encrypted, cipher.final()]); const authTag = cipher.getAuthTag(); // Return: iv:authTag:encrypted (all base64) return Buffer.from( iv.toString('base64') + ':' + authTag.toString('base64') + ':' + encrypted.toString('base64') ); } /** * Decrypt backup data */ async decryptBackup(encrypted, key) { const algorithm = 'aes-256-gcm'; const keyBuffer = Buffer.from(key, 'hex'); // Parse format: iv:authTag:encrypted const parts = encrypted.toString().split(':'); if (parts.length < 3) { throw new Error('Invalid encrypted backup format'); } const iv = Buffer.from(parts[0], 'base64'); const authTag = Buffer.from(parts[1], 'base64'); const ciphertext = Buffer.from(parts.slice(2).join(':'), 'base64'); const decipher = crypto.createDecipheriv(algorithm, keyBuffer, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(ciphertext); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted; } /** * Calculate checksum for backup */ calculateChecksum(data) { return crypto.createHash('sha256').update(data).digest('hex'); } /** * Save backup to destination */ async saveToDestination(data, destination, backupId) { switch (destination.type) { case 'local': return await this.saveToLocal(data, destination, backupId); case 'dropbox': return await this.saveToDropbox(data, destination, backupId); case 'webdav': return await this.saveToWebDAV(data, destination, backupId); case 'sftp': return await this.saveToSFTP(data, destination, backupId); default: throw new Error(`Unsupported destination type: ${destination.type}`); } } /** * Load encrypted backup blob from a destination location. * Returns a Buffer that can be passed to decryptBackup/decompressBackup. */ async loadFromDestination(location) { switch (location.type) { case 'local': return fs.readFileSync(location.path); case 'dropbox': return await this.loadFromDropbox(location); case 'webdav': return await this.loadFromWebDAV(location); case 'sftp': return await this.loadFromSFTP(location); default: throw new Error(`Unsupported destination type: ${location.type}`); } } /** * Save to local filesystem */ async saveToLocal(data, destination, backupId) { const backupDir = destination.path || DEFAULT_BACKUP_DIR; // Ensure directory exists if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } const filename = `${backupId}.backup`; const filepath = path.join(backupDir, filename); fs.writeFileSync(filepath, data); return { type: 'local', path: filepath, size: data.length }; } // ==================== CLOUD DESTINATIONS ==================== /** * Resolve credentials for a given provider via the credentialManager. * Throws if required fields are missing. */ async _getCloudCredentials(provider) { const credentialManager = require('./credential-manager'); const creds = {}; if (provider === 'dropbox') { creds.token = await credentialManager.retrieve('backup.dropbox.token'); if (!creds.token) throw new Error('Dropbox token not configured'); } else if (provider === 'webdav') { creds.url = await credentialManager.retrieve('backup.webdav.url'); creds.username = await credentialManager.retrieve('backup.webdav.username'); creds.password = await credentialManager.retrieve('backup.webdav.password'); if (!creds.url || !creds.username || !creds.password) { throw new Error('WebDAV credentials incomplete (need url, username, password)'); } } else if (provider === 'sftp') { creds.host = await credentialManager.retrieve('backup.sftp.host'); const portStr = await credentialManager.retrieve('backup.sftp.port'); creds.port = parseInt(portStr || '22', 10); creds.username = await credentialManager.retrieve('backup.sftp.username'); creds.password = await credentialManager.retrieve('backup.sftp.password'); creds.privateKey = await credentialManager.retrieve('backup.sftp.privateKey'); if (!creds.host || !creds.username || (!creds.password && !creds.privateKey)) { throw new Error('SFTP credentials incomplete (need host, username, and either password or privateKey)'); } } return creds; } // ----- Dropbox ----- async saveToDropbox(data, destination, backupId) { const { Dropbox } = require('dropbox'); const creds = await this._getCloudCredentials('dropbox'); const dbx = new Dropbox({ accessToken: creds.token }); const folder = (destination.path || '/dashcaddy-backups').replace(/\/+$/, ''); const remotePath = `${folder}/${backupId}.backup`; await dbx.filesUpload({ path: remotePath, contents: data, mode: { '.tag': 'overwrite' }, autorename: false, mute: true }); return { type: 'dropbox', path: remotePath, size: data.length }; } async loadFromDropbox(location) { const { Dropbox } = require('dropbox'); const creds = await this._getCloudCredentials('dropbox'); const dbx = new Dropbox({ accessToken: creds.token }); const result = await dbx.filesDownload({ path: location.path }); // Node SDK returns fileBinary on the result const fileBinary = result.result.fileBinary || result.result.fileBlob; if (Buffer.isBuffer(fileBinary)) return fileBinary; return Buffer.from(fileBinary); } // ----- WebDAV ----- async saveToWebDAV(data, destination, backupId) { const { createClient } = require('webdav'); const creds = await this._getCloudCredentials('webdav'); const client = createClient(creds.url, { username: creds.username, password: creds.password }); const folder = (destination.path || '/dashcaddy-backups').replace(/\/+$/, ''); // Ensure folder exists try { const exists = await client.exists(folder); if (!exists) await client.createDirectory(folder, { recursive: true }); } catch (_) { // best-effort } const remotePath = `${folder}/${backupId}.backup`; await client.putFileContents(remotePath, data, { overwrite: true }); return { type: 'webdav', path: remotePath, size: data.length }; } async loadFromWebDAV(location) { const { createClient } = require('webdav'); const creds = await this._getCloudCredentials('webdav'); const client = createClient(creds.url, { username: creds.username, password: creds.password }); const data = await client.getFileContents(location.path); return Buffer.isBuffer(data) ? data : Buffer.from(data); } // ----- SFTP ----- async saveToSFTP(data, destination, backupId) { const SftpClient = require('ssh2-sftp-client'); const creds = await this._getCloudCredentials('sftp'); const client = new SftpClient(); try { await client.connect({ host: creds.host, port: creds.port, username: creds.username, password: creds.password || undefined, privateKey: creds.privateKey || undefined }); const folder = (destination.path || '/dashcaddy-backups').replace(/\/+$/, ''); // Ensure remote dir exists try { const exists = await client.exists(folder); if (!exists) await client.mkdir(folder, true); } catch (_) { // best-effort } const remotePath = `${folder}/${backupId}.backup`; await client.put(Buffer.from(data), remotePath); return { type: 'sftp', path: remotePath, size: data.length }; } finally { try { await client.end(); } catch (_) {} } } async loadFromSFTP(location) { const SftpClient = require('ssh2-sftp-client'); const creds = await this._getCloudCredentials('sftp'); const client = new SftpClient(); try { await client.connect({ host: creds.host, port: creds.port, username: creds.username, password: creds.password || undefined, privateKey: creds.privateKey || undefined }); const buffer = await client.get(location.path); return Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer); } finally { try { await client.end(); } catch (_) {} } } /** * Test that a destination is reachable + writable + deletable. * Performs a small write/read/delete probe. */ async testDestination(destination) { const probeId = `test-${Date.now()}`; const probeData = Buffer.from(`dashcaddy-test-${probeId}`); const start = Date.now(); try { const location = await this.saveToDestination(probeData, destination, probeId); // Read it back let readBack = null; try { readBack = await this.loadFromDestination(location); } catch (_) { // Some providers (e.g. local) we already trust the file system; skip } // Delete the probe try { await this._deleteFromDestination(location); } catch (_) {} const elapsed = Date.now() - start; return { success: true, type: destination.type, elapsedMs: elapsed, verified: readBack ? readBack.equals(probeData) : null }; } catch (error) { return { success: false, type: destination.type, error: error.message, elapsedMs: Date.now() - start }; } } /** * Delete a backup from a destination location */ async _deleteFromDestination(location) { if (location.type === 'local') { if (fs.existsSync(location.path)) fs.unlinkSync(location.path); return; } if (location.type === 'dropbox') { const { Dropbox } = require('dropbox'); const creds = await this._getCloudCredentials('dropbox'); const dbx = new Dropbox({ accessToken: creds.token }); try { await dbx.filesDeleteV2({ path: location.path }); } catch (_) {} return; } if (location.type === 'webdav') { const { createClient } = require('webdav'); const creds = await this._getCloudCredentials('webdav'); const client = createClient(creds.url, { username: creds.username, password: creds.password }); try { await client.deleteFile(location.path); } catch (_) {} return; } if (location.type === 'sftp') { const SftpClient = require('ssh2-sftp-client'); const creds = await this._getCloudCredentials('sftp'); const client = new SftpClient(); try { await client.connect({ host: creds.host, port: creds.port, username: creds.username, password: creds.password || undefined, privateKey: creds.privateKey || undefined }); try { await client.delete(location.path); } catch (_) {} } finally { try { await client.end(); } catch (_) {} } return; } } /** * Verify backup integrity */ async verifyBackup(location, expectedChecksum) { if (location.type === 'local') { const data = fs.readFileSync(location.path); const checksum = this.calculateChecksum(data); if (checksum !== expectedChecksum) { throw new Error('Backup verification failed: checksum mismatch'); } console.log('[BackupManager] Backup verified successfully'); return true; } return true; } /** * Restore from backup */ async restoreBackup(backupId, options = {}) { console.log(`[BackupManager] Starting restore from backup: ${backupId}`); this.emit('restore-start', { backupId, timestamp: new Date().toISOString() }); try { // Find backup in history const backup = this.history.find(b => b.id === backupId); if (!backup) { throw new Error(`Backup not found: ${backupId}`); } // Load backup data — try each destination location until one succeeds const location = backup.locations[0]; // Primary location let data; try { data = await this.loadFromDestination(location); } catch (loadErr) { // Fall back to other locations if available let recovered = false; for (let i = 1; i < backup.locations.length; i++) { try { data = await this.loadFromDestination(backup.locations[i]); recovered = true; console.log(`[BackupManager] Loaded backup from fallback location ${backup.locations[i].type}`); break; } catch (_) {} } if (!recovered) throw loadErr; } // Decrypt if needed if (backup.encrypted && options.encryptionKey) { data = await this.decryptBackup(data, options.encryptionKey); } // Decompress const backupData = await this.decompressBackup(data); // Verify version compatibility if (backupData.version !== '1.0') { throw new Error(`Unsupported backup version: ${backupData.version}`); } // Restore data const restored = {}; if (backupData.data.services && options.restoreServices !== false) { this.restoreServices(backupData.data.services); restored.services = true; } if (backupData.data.config && options.restoreConfig !== false) { this.restoreConfig(backupData.data.config); restored.config = true; } if (backupData.data.credentials && options.restoreCredentials !== false) { this.restoreCredentials(backupData.data.credentials); restored.credentials = true; } if (backupData.data.stats && options.restoreStats !== false) { this.restoreStats(backupData.data.stats); restored.stats = true; } if (backupData.data.volumes && options.restoreVolumes !== false) { const volumeResult = await this.restoreVolumes(backupData.data.volumes); restored.volumes = volumeResult; } this.emit('restore-complete', { backupId, restored, timestamp: new Date().toISOString() }); console.log('[BackupManager] Restore completed successfully'); return { success: true, restored }; } catch (error) { this.emit('restore-failed', { backupId, error: error.message, timestamp: new Date().toISOString() }); throw error; } } /** * Restore services configuration */ restoreServices(services) { const servicesFile = process.env.SERVICES_FILE || path.join(__dirname, 'services.json'); fs.writeFileSync(servicesFile, JSON.stringify(services, null, 2)); console.log('[BackupManager] Services restored'); } /** * Restore configuration */ restoreConfig(config) { const configFile = process.env.CONFIG_FILE || path.join(__dirname, 'config.json'); fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); console.log('[BackupManager] Config restored'); } /** * Restore credentials */ restoreCredentials(credentials) { const credentialManager = require('./credential-manager'); credentialManager.importBackup(credentials); console.log('[BackupManager] Credentials restored'); } /** * Restore stats */ restoreStats(stats) { const resourceMonitor = require('./resource-monitor'); resourceMonitor.importStats(stats); console.log('[BackupManager] Stats restored'); } /** * Cleanup old backups based on retention policy */ async cleanupOldBackups(name, retention) { const backups = this.history.filter(b => b.name === name && b.status === 'success'); // Sort by timestamp (newest first) backups.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); // Keep only the specified number of backups const toDelete = backups.slice(retention.keep || 7); for (const backup of toDelete) { try { // Delete from all locations (local + cloud) for (const location of backup.locations) { try { await this._deleteFromDestination(location); } catch (delErr) { console.warn(`[BackupManager] Could not delete ${location.type} location for ${backup.id}:`, delErr.message); } } // Remove from history this.history = this.history.filter(b => b.id !== backup.id); console.log(`[BackupManager] Deleted old backup: ${backup.id}`); } catch (error) { console.error(`[BackupManager] Error deleting backup ${backup.id}:`, error.message); } } this.saveHistory(); } /** * Add entry to backup history */ addToHistory(entry) { this.history.push(entry); // Keep only last 100 entries if (this.history.length > 100) { this.history = this.history.slice(-100); } this.saveHistory(); } /** * Get backup history */ getHistory(limit = 50) { return this.history.slice(-limit).reverse(); } /** * Get backup configuration */ getConfig() { return this.config; } /** * Update backup configuration */ updateConfig(config) { this.config = { ...this.config, ...config }; this.saveConfig(); // Restart scheduler to apply changes this.stop(); this.start(); } /** * Load configuration from disk */ loadConfig() { try { if (fs.existsSync(BACKUP_CONFIG_FILE)) { return JSON.parse(fs.readFileSync(BACKUP_CONFIG_FILE, 'utf8')); } } catch (error) { console.error('[BackupManager] Error loading config:', error.message); } return { backups: {}, defaultRetention: { keep: 7 } }; } /** * Save configuration to disk */ saveConfig() { try { fs.writeFileSync(BACKUP_CONFIG_FILE, JSON.stringify(this.config, null, 2)); } catch (error) { console.error('[BackupManager] Error saving config:', error.message); } } /** * Load history from disk */ loadHistory() { try { if (fs.existsSync(BACKUP_HISTORY_FILE)) { return JSON.parse(fs.readFileSync(BACKUP_HISTORY_FILE, 'utf8')); } } catch (error) { console.error('[BackupManager] Error loading history:', error.message); } return []; } /** * Save history to disk */ saveHistory() { try { fs.writeFileSync(BACKUP_HISTORY_FILE, JSON.stringify(this.history, null, 2)); } catch (error) { console.error('[BackupManager] Error saving history:', error.message); } } } // Export singleton instance module.exports = new BackupManager();