This commit is contained in:
2025-07-20 20:43:06 +02:00
parent 0abee5b794
commit 29592c7fc8
93 changed files with 23400 additions and 131 deletions

View File

@ -0,0 +1,463 @@
#!/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;