feat: export createDatabaseBackup function for reuse in other scripts
This commit is contained in:
93
backup.js
Normal file
93
backup.js
Normal 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 };
|
||||
1
index.js
1
index.js
@@ -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
323
upload.js
Normal 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 };
|
||||
Reference in New Issue
Block a user