/**
* 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;
}