WIP
This commit is contained in:
52
backend/scripts/clear-data.js
Normal file
52
backend/scripts/clear-data.js
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Clear all data from database tables while keeping the schema
|
||||
* This removes all users, bookmarks, and resets migrations
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const dbConnection = require('../src/database/connection');
|
||||
|
||||
async function clearAllData() {
|
||||
try {
|
||||
console.log('🧹 Clearing all data from database...');
|
||||
|
||||
// Connect to database
|
||||
await dbConnection.connect();
|
||||
|
||||
// Clear data in the correct order (respecting foreign key constraints)
|
||||
console.log('🗑️ Clearing bookmarks...');
|
||||
const bookmarksResult = await dbConnection.query('DELETE FROM bookmarks');
|
||||
console.log(` Removed ${bookmarksResult.rowCount} bookmarks`);
|
||||
|
||||
console.log('🗑️ Clearing users...');
|
||||
const usersResult = await dbConnection.query('DELETE FROM users');
|
||||
console.log(` Removed ${usersResult.rowCount} users`);
|
||||
|
||||
console.log('🗑️ Clearing migration history...');
|
||||
const migrationsResult = await dbConnection.query('DELETE FROM migrations');
|
||||
console.log(` Removed ${migrationsResult.rowCount} migration records`);
|
||||
|
||||
// Reset sequences to start from 1
|
||||
console.log('🔄 Resetting ID sequences...');
|
||||
await dbConnection.query('ALTER SEQUENCE users_id_seq RESTART WITH 1');
|
||||
await dbConnection.query('ALTER SEQUENCE bookmarks_id_seq RESTART WITH 1');
|
||||
await dbConnection.query('ALTER SEQUENCE migrations_id_seq RESTART WITH 1');
|
||||
|
||||
console.log('✅ All data cleared successfully');
|
||||
console.log('💡 Database schema is preserved - you can now add fresh data');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to clear data:', error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
await dbConnection.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the clear operation
|
||||
clearAllData().catch((error) => {
|
||||
console.error('❌ Clear data operation failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
463
backend/scripts/db-backup.js
Normal file
463
backend/scripts/db-backup.js
Normal 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;
|
||||
Reference in New Issue
Block a user