#!/usr/bin/env node /** * Database Backup and Restore Script * Supports PostgreSQL database backup and restore operations */ require('dotenv').config(); const { execSync, spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); class DatabaseBackup { constructor() { this.dbConfig = { host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, database: process.env.DB_NAME || 'bookmark_manager', username: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD || 'password' }; this.backupDir = path.join(__dirname, '../backups'); this.ensureBackupDirectory(); } /** * Ensure backup directory exists */ ensureBackupDirectory() { if (!fs.existsSync(this.backupDir)) { fs.mkdirSync(this.backupDir, { recursive: true }); console.log(`๐Ÿ“ Created backup directory: ${this.backupDir}`); } } /** * Generate backup filename with timestamp * @param {string} type - Type of backup (full, schema, data) * @returns {string} Backup filename */ generateBackupFilename(type = 'full') { const timestamp = new Date().toISOString() .replace(/[:.]/g, '-') .replace('T', '_') .split('.')[0]; return `${this.dbConfig.database}_${type}_${timestamp}.sql`; } /** * Create full database backup * @param {string} filename - Optional custom filename * @returns {Promise} Path to backup file */ async createFullBackup(filename = null) { try { const backupFile = filename || this.generateBackupFilename('full'); const backupPath = path.join(this.backupDir, backupFile); console.log('๐Ÿ”„ Creating full database backup...'); console.log(` Database: ${this.dbConfig.database}`); console.log(` File: ${backupFile}`); const command = [ 'pg_dump', `--host=${this.dbConfig.host}`, `--port=${this.dbConfig.port}`, `--username=${this.dbConfig.username}`, '--verbose', '--clean', '--if-exists', '--create', '--format=plain', `--file=${backupPath}`, this.dbConfig.database ]; // Set password environment variable const env = { ...process.env, PGPASSWORD: this.dbConfig.password }; execSync(command.join(' '), { env, stdio: 'inherit' }); const stats = fs.statSync(backupPath); const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2); console.log('โœ… Full backup completed successfully'); console.log(` File: ${backupPath}`); console.log(` Size: ${fileSizeMB} MB`); return backupPath; } catch (error) { console.error('โŒ Full backup failed:', error.message); throw error; } } /** * Create schema-only backup * @param {string} filename - Optional custom filename * @returns {Promise} Path to backup file */ async createSchemaBackup(filename = null) { try { const backupFile = filename || this.generateBackupFilename('schema'); const backupPath = path.join(this.backupDir, backupFile); console.log('๐Ÿ”„ Creating schema-only backup...'); console.log(` Database: ${this.dbConfig.database}`); console.log(` File: ${backupFile}`); const command = [ 'pg_dump', `--host=${this.dbConfig.host}`, `--port=${this.dbConfig.port}`, `--username=${this.dbConfig.username}`, '--verbose', '--schema-only', '--clean', '--if-exists', '--create', '--format=plain', `--file=${backupPath}`, this.dbConfig.database ]; const env = { ...process.env, PGPASSWORD: this.dbConfig.password }; execSync(command.join(' '), { env, stdio: 'inherit' }); console.log('โœ… Schema backup completed successfully'); console.log(` File: ${backupPath}`); return backupPath; } catch (error) { console.error('โŒ Schema backup failed:', error.message); throw error; } } /** * Create data-only backup * @param {string} filename - Optional custom filename * @returns {Promise} Path to backup file */ async createDataBackup(filename = null) { try { const backupFile = filename || this.generateBackupFilename('data'); const backupPath = path.join(this.backupDir, backupFile); console.log('๐Ÿ”„ Creating data-only backup...'); console.log(` Database: ${this.dbConfig.database}`); console.log(` File: ${backupFile}`); const command = [ 'pg_dump', `--host=${this.dbConfig.host}`, `--port=${this.dbConfig.port}`, `--username=${this.dbConfig.username}`, '--verbose', '--data-only', '--format=plain', `--file=${backupPath}`, this.dbConfig.database ]; const env = { ...process.env, PGPASSWORD: this.dbConfig.password }; execSync(command.join(' '), { env, stdio: 'inherit' }); console.log('โœ… Data backup completed successfully'); console.log(` File: ${backupPath}`); return backupPath; } catch (error) { console.error('โŒ Data backup failed:', error.message); throw error; } } /** * Restore database from backup file * @param {string} backupPath - Path to backup file * @param {boolean} dropExisting - Whether to drop existing database * @returns {Promise} */ async restoreFromBackup(backupPath, dropExisting = false) { try { if (!fs.existsSync(backupPath)) { throw new Error(`Backup file not found: ${backupPath}`); } console.log('๐Ÿ”„ Restoring database from backup...'); console.log(` Backup file: ${backupPath}`); console.log(` Target database: ${this.dbConfig.database}`); if (dropExisting) { console.log('โš ๏ธ Dropping existing database...'); await this.dropDatabase(); } const command = [ 'psql', `--host=${this.dbConfig.host}`, `--port=${this.dbConfig.port}`, `--username=${this.dbConfig.username}`, '--verbose', `--file=${backupPath}` ]; const env = { ...process.env, PGPASSWORD: this.dbConfig.password }; execSync(command.join(' '), { env, stdio: 'inherit' }); console.log('โœ… Database restore completed successfully'); } catch (error) { console.error('โŒ Database restore failed:', error.message); throw error; } } /** * Drop existing database (use with caution!) * @returns {Promise} */ async dropDatabase() { try { console.log('โš ๏ธ Dropping database...'); const command = [ 'dropdb', `--host=${this.dbConfig.host}`, `--port=${this.dbConfig.port}`, `--username=${this.dbConfig.username}`, '--if-exists', this.dbConfig.database ]; const env = { ...process.env, PGPASSWORD: this.dbConfig.password }; execSync(command.join(' '), { env, stdio: 'inherit' }); console.log('โœ… Database dropped successfully'); } catch (error) { console.error('โŒ Failed to drop database:', error.message); throw error; } } /** * List available backup files * @returns {Array} List of backup files with details */ listBackups() { try { const files = fs.readdirSync(this.backupDir) .filter(file => file.endsWith('.sql')) .map(file => { const filePath = path.join(this.backupDir, file); const stats = fs.statSync(filePath); const sizeMB = (stats.size / (1024 * 1024)).toFixed(2); return { filename: file, path: filePath, size: `${sizeMB} MB`, created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString() }; }) .sort((a, b) => new Date(b.created) - new Date(a.created)); return files; } catch (error) { console.error('โŒ Failed to list backups:', error.message); return []; } } /** * Clean up old backup files * @param {number} keepDays - Number of days to keep backups * @returns {Promise} Number of files deleted */ async cleanupOldBackups(keepDays = 30) { try { console.log(`๐Ÿงน Cleaning up backups older than ${keepDays} days...`); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - keepDays); const files = fs.readdirSync(this.backupDir) .filter(file => file.endsWith('.sql')); let deletedCount = 0; for (const file of files) { const filePath = path.join(this.backupDir, file); const stats = fs.statSync(filePath); if (stats.birthtime < cutoffDate) { fs.unlinkSync(filePath); console.log(` Deleted: ${file}`); deletedCount++; } } console.log(`โœ… Cleanup completed: ${deletedCount} files deleted`); return deletedCount; } catch (error) { console.error('โŒ Cleanup failed:', error.message); throw error; } } /** * Verify backup file integrity * @param {string} backupPath - Path to backup file * @returns {Promise} True if backup is valid */ async verifyBackup(backupPath) { try { if (!fs.existsSync(backupPath)) { throw new Error(`Backup file not found: ${backupPath}`); } console.log('๐Ÿ” Verifying backup file...'); const content = fs.readFileSync(backupPath, 'utf8'); // Basic checks const hasCreateDatabase = content.includes('CREATE DATABASE'); const hasCreateTable = content.includes('CREATE TABLE'); const hasValidSQL = content.includes('--'); if (hasCreateDatabase || hasCreateTable || hasValidSQL) { console.log('โœ… Backup file appears to be valid'); return true; } else { console.log('โŒ Backup file appears to be invalid or corrupted'); return false; } } catch (error) { console.error('โŒ Backup verification failed:', error.message); return false; } } } // CLI Interface async function main() { const backup = new DatabaseBackup(); const command = process.argv[2]; const arg1 = process.argv[3]; const arg2 = process.argv[4]; try { switch (command) { case 'backup': case 'full': await backup.createFullBackup(arg1); break; case 'schema': await backup.createSchemaBackup(arg1); break; case 'data': await backup.createDataBackup(arg1); break; case 'restore': if (!arg1) { console.error('โŒ Please provide backup file path'); process.exit(1); } const dropExisting = arg2 === '--drop' || arg2 === '-d'; await backup.restoreFromBackup(arg1, dropExisting); break; case 'list': const backups = backup.listBackups(); console.log('\n๐Ÿ“‹ Available Backups:'); if (backups.length === 0) { console.log(' No backup files found'); } else { backups.forEach((file, index) => { console.log(`\n${index + 1}. ${file.filename}`); console.log(` Size: ${file.size}`); console.log(` Created: ${new Date(file.created).toLocaleString()}`); console.log(` Path: ${file.path}`); }); } break; case 'cleanup': const days = parseInt(arg1) || 30; await backup.cleanupOldBackups(days); break; case 'verify': if (!arg1) { console.error('โŒ Please provide backup file path'); process.exit(1); } await backup.verifyBackup(arg1); break; case 'help': default: console.log('๐Ÿ“– Database Backup & Restore Commands:'); console.log(''); console.log(' backup [filename] - Create full database backup'); console.log(' full [filename] - Create full database backup'); console.log(' schema [filename] - Create schema-only backup'); console.log(' data [filename] - Create data-only backup'); console.log(' restore [--drop] - Restore from backup file'); console.log(' list - List available backup files'); console.log(' cleanup [days] - Clean up old backups (default: 30 days)'); console.log(' verify - Verify backup file integrity'); console.log(' help - Show this help message'); console.log(''); console.log('Examples:'); console.log(' node scripts/db-backup.js backup'); console.log(' node scripts/db-backup.js restore backup.sql --drop'); console.log(' node scripts/db-backup.js cleanup 7'); break; } } catch (error) { console.error('โŒ Operation failed:', error.message); process.exit(1); } } // Run if called directly if (require.main === module) { main().catch(console.error); } module.exports = DatabaseBackup;