Skip to main content
Glama
backup-manager.jsโ€ข10.9 kB
/** * Backup Manager for MCP SSH Manager * Handles creation, listing, restoration, and scheduling of backups * Supports databases (MySQL, PostgreSQL, MongoDB) and file backups */ import path from 'path'; import crypto from 'crypto'; import { logger } from './logger.js'; // Backup types export const BACKUP_TYPES = { MYSQL: 'mysql', POSTGRESQL: 'postgresql', MONGODB: 'mongodb', FILES: 'files', FULL: 'full' }; // Default backup directory export const DEFAULT_BACKUP_DIR = '/var/backups/ssh-manager'; /** * Generate unique backup ID */ export function generateBackupId(type, name) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const random = crypto.randomBytes(4).toString('hex'); return `${type}_${name}_${timestamp}_${random}`; } /** * Get backup metadata file path */ export function getBackupMetadataPath(backupId, backupDir = DEFAULT_BACKUP_DIR) { return path.join(backupDir, `${backupId}.meta.json`); } /** * Get backup file path */ export function getBackupFilePath(backupId, backupDir = DEFAULT_BACKUP_DIR, extension = '.gz') { return path.join(backupDir, `${backupId}${extension}`); } /** * Build MySQL dump command */ export function buildMySQLDumpCommand(options) { const { database, user, password, host = 'localhost', port = 3306, outputFile, singleTransaction = true, compress = true } = options; let command = 'mysqldump'; // Connection parameters if (user) command += ` -u${user}`; if (password) command += ` -p'${password}'`; if (host) command += ` -h ${host}`; if (port) command += ` -P ${port}`; // Dump options if (singleTransaction) command += ' --single-transaction'; command += ' --routines --triggers'; // Database name command += ` ${database}`; // Output handling if (compress) { command += ` | gzip > "${outputFile}"`; } else { command += ` > "${outputFile}"`; } return command; } /** * Build PostgreSQL dump command */ export function buildPostgreSQLDumpCommand(options) { const { database, user, password, host = 'localhost', port = 5432, outputFile, compress = true } = options; // PostgreSQL uses PGPASSWORD environment variable let command = ''; if (password) { command = `PGPASSWORD='${password}' `; } command += 'pg_dump'; // Connection parameters if (user) command += ` -U ${user}`; if (host) command += ` -h ${host}`; if (port) command += ` -p ${port}`; // Dump options command += ' --format=custom --clean --if-exists'; // Database name command += ` ${database}`; // Output handling if (compress) { command += ` | gzip > "${outputFile}"`; } else { command += ` > "${outputFile}"`; } return command; } /** * Build MongoDB dump command */ export function buildMongoDBDumpCommand(options) { const { database, user, password, host = 'localhost', port = 27017, outputDir, compress = true } = options; let command = 'mongodump'; // Connection parameters if (host) command += ` --host ${host}`; if (port) command += ` --port ${port}`; if (user) command += ` --username ${user}`; if (password) command += ` --password '${password}'`; // Database selection if (database) command += ` --db ${database}`; // Output directory command += ` --out "${outputDir}"`; // Compress the output directory if (compress) { const archiveName = `${outputDir}.tar.gz`; command += ` && tar -czf "${archiveName}" -C "$(dirname ${outputDir})" "$(basename ${outputDir})"`; command += ` && rm -rf "${outputDir}"`; } return command; } /** * Build files backup command (tar + gzip) */ export function buildFilesBackupCommand(options) { const { paths, outputFile, exclude = [], compress = true } = options; if (!Array.isArray(paths) || paths.length === 0) { throw new Error('paths must be a non-empty array'); } let command = 'tar'; // Compression flag if (compress) { command += ' -czf'; } else { command += ' -cf'; } // Output file command += ` "${outputFile}"`; // Exclude patterns for (const pattern of exclude) { command += ` --exclude="${pattern}"`; } // Paths to backup command += ` ${paths.map(p => `"${p}"`).join(' ')}`; return command; } /** * Build backup restore command based on type */ export function buildRestoreCommand(backupType, backupFile, options = {}) { switch (backupType) { case BACKUP_TYPES.MYSQL: return buildMySQLRestoreCommand(backupFile, options); case BACKUP_TYPES.POSTGRESQL: return buildPostgreSQLRestoreCommand(backupFile, options); case BACKUP_TYPES.MONGODB: return buildMongoDBRestoreCommand(backupFile, options); case BACKUP_TYPES.FILES: return buildFilesRestoreCommand(backupFile, options); default: throw new Error(`Unknown backup type: ${backupType}`); } } /** * Build MySQL restore command */ function buildMySQLRestoreCommand(backupFile, options) { const { database, user, password, host = 'localhost', port = 3306 } = options; let command = ''; // Decompress if needed if (backupFile.endsWith('.gz')) { command = `gunzip -c "${backupFile}" | `; } else { command = `cat "${backupFile}" | `; } command += 'mysql'; // Connection parameters if (user) command += ` -u${user}`; if (password) command += ` -p'${password}'`; if (host) command += ` -h ${host}`; if (port) command += ` -P ${port}`; if (database) command += ` ${database}`; return command; } /** * Build PostgreSQL restore command */ function buildPostgreSQLRestoreCommand(backupFile, options) { const { database, user, password, host = 'localhost', port = 5432 } = options; let command = ''; if (password) { command = `PGPASSWORD='${password}' `; } command += 'pg_restore'; // Connection parameters if (user) command += ` -U ${user}`; if (host) command += ` -h ${host}`; if (port) command += ` -p ${port}`; if (database) command += ` -d ${database}`; // Restore options command += ' --clean --if-exists'; // Handle compressed files if (backupFile.endsWith('.gz')) { command = `gunzip -c "${backupFile}" | ${command}`; } else { command += ` "${backupFile}"`; } return command; } /** * Build MongoDB restore command */ function buildMongoDBRestoreCommand(backupFile, options) { const { database, user, password, host = 'localhost', port = 27017, drop = true } = options; let command = ''; // Extract if compressed if (backupFile.endsWith('.tar.gz')) { const extractDir = backupFile.replace('.tar.gz', ''); command = `tar -xzf "${backupFile}" -C "$(dirname ${backupFile})" && `; command += 'mongorestore'; if (drop) command += ' --drop'; if (host) command += ` --host ${host}`; if (port) command += ` --port ${port}`; if (user) command += ` --username ${user}`; if (password) command += ` --password '${password}'`; command += ` "${extractDir}"`; command += ` && rm -rf "${extractDir}"`; } else { command = 'mongorestore'; if (drop) command += ' --drop'; if (host) command += ` --host ${host}`; if (port) command += ` --port ${port}`; if (user) command += ` --username ${user}`; if (password) command += ` --password '${password}'`; command += ` "${backupFile}"`; } return command; } /** * Build files restore command */ function buildFilesRestoreCommand(backupFile, options) { const { targetPath = '/' } = options; let command = 'tar'; // Auto-detect compression if (backupFile.endsWith('.gz') || backupFile.endsWith('.tgz')) { command += ' -xzf'; } else { command += ' -xf'; } command += ` "${backupFile}"`; command += ` -C "${targetPath}"`; return command; } /** * Create backup metadata object */ export function createBackupMetadata(backupId, type, options = {}) { return { id: backupId, type, created_at: new Date().toISOString(), server: options.server || 'unknown', database: options.database || null, paths: options.paths || [], size: null, // Will be filled after backup compressed: options.compress !== false, retention: options.retention || 7, // days status: 'pending', error: null }; } /** * Build command to save metadata to remote server */ export function buildSaveMetadataCommand(metadata, metadataPath) { const jsonData = JSON.stringify(metadata, null, 2); // Escape single quotes in JSON for shell const escapedJson = jsonData.replace(/'/g, '\'\\\'\''); return `echo '${escapedJson}' > "${metadataPath}"`; } /** * Build command to list backups from remote server */ export function buildListBackupsCommand(backupDir = DEFAULT_BACKUP_DIR, type = null) { let command = `find "${backupDir}" -name "*.meta.json" -type f`; if (type) { command += ` | grep "${type}_"`; } // Read and parse each metadata file command += ' | while read -r file; do cat "$file"; echo "---"; done'; return command; } /** * Parse list backups output */ export function parseBackupsList(output) { if (!output || !output.trim()) { return []; } const backups = []; const metadataBlocks = output.split('---').filter(b => b.trim()); for (const block of metadataBlocks) { try { const metadata = JSON.parse(block.trim()); backups.push(metadata); } catch (error) { logger.warn('Failed to parse backup metadata', { error: error.message, block }); } } // Sort by created_at descending return backups.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); } /** * Build cleanup old backups command (based on retention) */ export function buildCleanupCommand(backupDir = DEFAULT_BACKUP_DIR, retentionDays = 7) { // Find backup files older than retention period and delete them return `find "${backupDir}" -name "*_*_*" -type f -mtime +${retentionDays} -delete`; } /** * Build cron schedule command */ export function buildCronScheduleCommand(schedule, backupCommand, cronComment) { // Add cron job with comment const cronLine = `${schedule} ${backupCommand} # ${cronComment}`; return `(crontab -l 2>/dev/null; echo '${cronLine}') | crontab -`; } /** * Parse cron list output */ export function parseCronJobs(output) { if (!output || !output.trim()) { return []; } const jobs = []; const lines = output.split('\n'); for (const line of lines) { if (line.trim() && !line.startsWith('#') && line.includes('ssh-manager-backup')) { const parts = line.split('#'); const schedule = parts[0].trim(); const comment = parts[1] ? parts[1].trim() : ''; jobs.push({ schedule, comment, command: schedule.split(/\s+/).slice(5).join(' ') }); } } return jobs; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bvisible/mcp-ssh-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server