diff --git a/backup.js b/backup.js new file mode 100644 index 0000000..f65bf07 --- /dev/null +++ b/backup.js @@ -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 }; diff --git a/index.js b/index.js index 0961ae5..85157cb 100644 --- a/index.js +++ b/index.js @@ -356,6 +356,7 @@ if (require.main === module) { // Export functions for reuse in other scripts module.exports = { + createDatabaseBackup, downloadBackupFile, compressBackupFile, formatBytes, diff --git a/upload.js b/upload.js new file mode 100644 index 0000000..b2f31c6 --- /dev/null +++ b/upload.js @@ -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 };