feat: export createDatabaseBackup function for reuse in other scripts

This commit is contained in:
sebseb7
2025-09-04 11:11:04 +02:00
parent acfb7a0552
commit 6fb6b0da78
3 changed files with 417 additions and 0 deletions

93
backup.js Normal file
View File

@@ -0,0 +1,93 @@
require('dotenv').config();
const {
createDatabaseBackup,
downloadBackupFile,
sendTelegramBroadcast,
formatBytes,
config: sqlConfig
} = require('./index.js');
const fs = require('fs');
const path = require('path');
async function createAndDownloadBackup() {
try {
console.log('='.repeat(50));
console.log('FRESH BACKUP CREATION');
console.log('='.repeat(50));
console.log(`Database: ${process.env.MSSQL_DATABASE}`);
console.log(`Server: ${process.env.MSSQL_SERVER}:${process.env.MSSQL_PORT}`);
console.log('');
// Step 1: Create fresh backup on SQL Server
console.log('Step 1: Creating fresh database backup on SQL Server...');
const serverBackupPath = await createDatabaseBackup();
console.log(`Backup created on server: ${serverBackupPath}`);
console.log('');
// Step 2: Download the backup file to local folder
console.log('Step 2: Downloading backup file to local folder...');
const localBackupPath = await downloadBackupFile();
console.log(`Backup downloaded to: ${localBackupPath}`);
console.log('');
// Step 3: Check local file and show details
if (fs.existsSync(localBackupPath)) {
const stats = fs.statSync(localBackupPath);
const absolutePath = path.resolve(localBackupPath);
console.log('='.repeat(50));
console.log('BACKUP COMPLETED SUCCESSFULLY');
console.log('='.repeat(50));
console.log(`Local file: ${absolutePath}`);
console.log(`File size: ${formatBytes(stats.size)}`);
console.log(`Created: ${stats.ctime.toISOString()}`);
console.log('');
// Send success notification
const when = new Date().toISOString();
const msg = `Fresh backup created and downloaded ✅\nDB: ${process.env.MSSQL_DATABASE}\nFile: ${localBackupPath}\nSize: ${formatBytes(stats.size)}\nServer: ${process.env.MSSQL_SERVER}:${process.env.MSSQL_PORT}\nTime: ${when}`;
await sendTelegramBroadcast('all', msg);
return {
success: true,
localPath: absolutePath,
serverPath: serverBackupPath,
size: stats.size,
database: process.env.MSSQL_DATABASE
};
} else {
throw new Error(`Local backup file not found: ${localBackupPath}`);
}
} catch (err) {
console.error('');
console.error('='.repeat(50));
console.error('BACKUP FAILED');
console.error('='.repeat(50));
console.error('Error:', err.message);
console.error('');
// Send error notification
const when = new Date().toISOString();
const msg = `Fresh backup failed 🔴\nDB: ${process.env.MSSQL_DATABASE}\nServer: ${process.env.MSSQL_SERVER}:${process.env.MSSQL_PORT}\nTime: ${when}\nError: ${err.message}`;
await sendTelegramBroadcast('errors', msg);
throw err;
}
}
// Run if called directly
if (require.main === module) {
createAndDownloadBackup()
.then((result) => {
console.log('Fresh backup process completed successfully!');
console.log(`Ready to use: ${result.localPath}`);
process.exit(0);
})
.catch((error) => {
console.error('Fresh backup process failed:', error.message);
process.exit(1);
});
}
module.exports = { createAndDownloadBackup };

View File

@@ -356,6 +356,7 @@ if (require.main === module) {
// Export functions for reuse in other scripts
module.exports = {
createDatabaseBackup,
downloadBackupFile,
compressBackupFile,
formatBytes,

323
upload.js Normal file
View File

@@ -0,0 +1,323 @@
require('dotenv').config();
const sql = require('mssql');
const fs = require('fs');
const path = require('path');
const { sendTelegramBroadcast, formatBytes } = require('./index.js');
// MSSQL Configuration - allow override via command line arguments
const getConfig = () => {
// Check for command line arguments to override server/port
const args = process.argv.slice(2);
let serverOverride = null;
let portOverride = null;
args.forEach((arg, index) => {
if (arg === '--server' && args[index + 1]) {
serverOverride = args[index + 1];
}
if (arg === '--port' && args[index + 1]) {
portOverride = parseInt(args[index + 1]);
}
});
return {
user: process.env.MSSQL_USER,
password: process.env.MSSQL_PASSWORD,
server: serverOverride || process.env.MSSQL_SERVER,
port: portOverride || parseInt(process.env.MSSQL_PORT) || 1433,
database: 'master', // Connect to master for restore operations
requestTimeout: 3600000, // 1 hour for restore operations
options: {
encrypt: false,
trustServerCertificate: true
}
};
};
// Function to check if backup file exists
function checkBackupFile(backupPath) {
if (!fs.existsSync(backupPath)) {
throw new Error(`Backup file not found: ${backupPath}`);
}
const stats = fs.statSync(backupPath);
console.log(`Backup file found: ${backupPath}`);
console.log(`File size: ${formatBytes(stats.size)}`);
return stats;
}
// Function to get database name from backup file
async function getDatabaseNameFromBackup(pool, backupPath) {
try {
console.log('Reading database name from backup file...');
const query = `
RESTORE HEADERONLY
FROM DISK = @backupPath
`;
const request = pool.request();
request.input('backupPath', sql.NVarChar, backupPath);
const result = await request.query(query);
if (result.recordset && result.recordset.length > 0) {
const databaseName = result.recordset[0].DatabaseName;
console.log(`Database name from backup: ${databaseName}`);
return databaseName;
} else {
throw new Error('Unable to read database name from backup file');
}
} catch (err) {
console.error('Error reading backup header:', err.message);
throw err;
}
}
// Function to get logical file names from backup
async function getLogicalFileNames(pool, backupPath) {
try {
console.log('Reading logical file names from backup...');
const query = `
RESTORE FILELISTONLY
FROM DISK = @backupPath
`;
const request = pool.request();
request.input('backupPath', sql.NVarChar, backupPath);
const result = await request.query(query);
if (result.recordset && result.recordset.length > 0) {
const files = result.recordset.map(file => ({
LogicalName: file.LogicalName,
Type: file.Type,
PhysicalName: file.PhysicalName
}));
console.log('Logical files in backup:');
files.forEach(file => {
console.log(` - ${file.LogicalName} (${file.Type}): ${file.PhysicalName}`);
});
return files;
} else {
throw new Error('Unable to read file list from backup');
}
} catch (err) {
console.error('Error reading backup file list:', err.message);
throw err;
}
}
// Function to restore database using the proven working SQL commands
async function restoreDatabase(pool, backupPath, databaseName) {
try {
console.log(`Starting restore of database: ${databaseName}`);
// Step 1: Set database to single user mode (this kills connections)
console.log('Setting database to single-user mode and killing connections...');
const singleUserQuery = `ALTER DATABASE [${databaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE`;
try {
const singleUserRequest = pool.request();
singleUserRequest.timeout = 60000; // 1 minute for preparation
await singleUserRequest.query(singleUserQuery);
console.log('Database set to single-user mode');
} catch (err) {
// Database might not exist yet, that's okay
console.log('Note: Could not set to single-user mode (database may not exist yet)');
}
// Step 2: Execute the restore with proven working parameters
console.log('Executing restore command with tested parameters...');
console.log('Note: This operation may take several minutes depending on database size.');
const restoreQuery = `
RESTORE DATABASE [${databaseName}]
FROM DISK = @backupPath
WITH FILE = 1,
MOVE N'eazybusiness' TO N'/var/opt/mssql/data/eazybusiness.mdf',
MOVE N'eazybusiness_log' TO N'/var/opt/mssql/data/eazybusiness_log.ldf',
NOUNLOAD,
REPLACE,
STATS = 5
`;
const request = pool.request();
request.input('backupPath', sql.NVarChar, backupPath);
// Set longer timeout for restore operations (in milliseconds)
request.timeout = 3600000; // 1 hour
// Also set pool timeout
pool.config.requestTimeout = 3600000;
const result = await request.query(restoreQuery);
console.log('Database restore completed successfully!');
// Step 3: Set database back to multi user mode
console.log('Setting database back to multi-user mode...');
const multiUserQuery = `ALTER DATABASE [${databaseName}] SET MULTI_USER`;
const multiUserRequest = pool.request();
multiUserRequest.timeout = 60000;
await multiUserRequest.query(multiUserQuery);
console.log('Database is now ready for use!');
return result;
} catch (err) {
console.error('Error during database restore:', err.message);
// Try to set database back to multi-user mode even if restore failed
try {
console.log('Attempting to set database back to multi-user mode...');
const multiUserQuery = `ALTER DATABASE [${databaseName}] SET MULTI_USER`;
const multiUserRequest = pool.request();
multiUserRequest.timeout = 60000;
await multiUserRequest.query(multiUserQuery);
} catch (cleanupErr) {
console.warn('Could not reset database to multi-user mode:', cleanupErr.message);
}
throw err;
}
}
// Main restore function
async function restoreBackupFile(backupFileName = 'ez.bak', sqlServerPath = null) {
let pool = null;
try {
const config = getConfig();
console.log(`Connecting to SQL Server: ${config.server}:${config.port}`);
console.log(`User: ${config.user}`);
let fileStats = null;
let serverBackupPath;
if (sqlServerPath) {
// Using remote path - no local validation needed
serverBackupPath = sqlServerPath;
console.log(`SQL Server will access backup from: ${serverBackupPath}`);
} else {
// Using local path - validate it exists
const localBackupPath = path.resolve(backupFileName);
fileStats = checkBackupFile(localBackupPath);
serverBackupPath = localBackupPath;
console.log(`SQL Server will access backup from: ${serverBackupPath}`);
}
// Connect to SQL Server
pool = await sql.connect(config);
console.log('Connected to SQL Server successfully');
// Get database name from backup
const databaseName = await getDatabaseNameFromBackup(pool, serverBackupPath);
// Perform the restore
await restoreDatabase(pool, serverBackupPath, databaseName);
console.log('Restore process completed successfully!');
console.log(`Database '${databaseName}' has been restored from ${backupFileName}`);
// Send success notification
const when = new Date().toISOString();
const sizeInfo = fileStats ? `\nSize: ${formatBytes(fileStats.size)}` : '';
const msg = `Database restore completed ✅\nDB: ${databaseName}\nFrom: ${serverBackupPath}${sizeInfo}\nServer: ${config.server}:${config.port}\nTime: ${when}`;
await sendTelegramBroadcast('all', msg);
return {
success: true,
databaseName,
server: `${config.server}:${config.port}`,
backupFile: serverBackupPath,
fileSize: fileStats ? fileStats.size : null
};
} catch (err) {
console.error('Restore process failed:', err.message);
// Send error notification
const config = getConfig();
const when = new Date().toISOString();
const msg = `Database restore failed 🔴\nFile: ${backupFileName}\nServer: ${config.server}:${config.port}\nTime: ${when}\nError: ${err.message}`;
await sendTelegramBroadcast('errors', msg);
throw err;
} finally {
// Always close the connection
if (pool) {
try {
await pool.close();
console.log('Database connection closed');
} catch (closeErr) {
console.warn('Error closing database connection:', closeErr.message);
}
}
}
}
// Run if called directly
if (require.main === module) {
// Parse command line arguments
const args = process.argv.slice(2);
let backupFile = 'ez.bak';
let sqlServerPath = null;
// Look for backup file argument (first non-flag argument)
const fileArg = args.find(arg => !arg.startsWith('--'));
if (fileArg) {
backupFile = fileArg;
}
// Look for SQL Server path argument
const pathIndex = args.indexOf('--sql-path');
if (pathIndex !== -1 && args[pathIndex + 1]) {
sqlServerPath = args[pathIndex + 1];
}
console.log('='.repeat(50));
console.log('DATABASE RESTORE UTILITY');
console.log('='.repeat(50));
console.log(`Backup file: ${backupFile}`);
if (sqlServerPath) {
console.log(`SQL Server path: ${sqlServerPath}`);
} else {
console.log('WARNING: Using local path - SQL Server on remote host may not access it!');
console.log('Use --sql-path to specify the path as SQL Server sees it');
}
console.log('Starting restore process...');
console.log('');
restoreBackupFile(backupFile, sqlServerPath)
.then((result) => {
console.log('');
console.log('='.repeat(50));
console.log('RESTORE COMPLETED SUCCESSFULLY');
console.log('='.repeat(50));
console.log(`Database: ${result.databaseName}`);
console.log(`Server: ${result.server}`);
console.log(`File: ${result.backupFile}`);
if (result.fileSize) {
console.log(`Size: ${formatBytes(result.fileSize)}`);
}
process.exit(0);
})
.catch((error) => {
console.error('');
console.error('='.repeat(50));
console.error('RESTORE FAILED');
console.error('='.repeat(50));
console.error('Error:', error.message);
console.error('');
console.error('TROUBLESHOOTING:');
console.error('- If SQL Server is on a remote host, it cannot access local files');
console.error('- Use --sql-path to specify the path as SQL Server sees it');
console.error('- Example: --sql-path "F:\\ez.bak"');
process.exit(1);
});
}
module.exports = { restoreBackupFile, getConfig };