463 lines
16 KiB
JavaScript
463 lines
16 KiB
JavaScript
#!/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<string>} 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<string>} 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<string>} 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<void>}
|
|
*/
|
|
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<void>}
|
|
*/
|
|
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>} 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<boolean>} 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 <file> [--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 <file> - 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; |