UNPKG

yemot-backup

Version:

Automatic backup tool for Yemot HaMashiach call recordings to Amazon S3

816 lines (711 loc) 27.9 kB
const cron = require('node-cron'); const { Command } = require('commander'); const logger = require('./logger'); const config = require('./config'); const BackupService = require('./backupService'); const YemotApi = require('./yemotApi'); const path = require('path'); const fs = require('fs'); // Load system configuration config.loadSystems(); // For background process management - global scheduler const PM2_PROCESS_NAME = 'yemot-backup-scheduler'; // Format for per-system schedulers const PM2_SYSTEM_PROCESS_FORMAT = 'yemot-backup-system-{id}'; /** * Get PM2 process name for a specific system * @param {string} systemId - System ID * @returns {string} - PM2 process name */ function getSystemProcessName(systemId) { return PM2_SYSTEM_PROCESS_FORMAT.replace('{id}', systemId); } /** * Check if a system scheduler is running * @param {string} systemId - System ID to check * @returns {Promise<boolean>} True if running */ async function isSystemSchedulerRunning(systemId) { const processName = getSystemProcessName(systemId); return new Promise((resolve) => { const { exec } = require('child_process'); exec(`pm2 id ${processName}`, (error, stdout) => { if (error || stdout.includes('[]')) { resolve(false); } else { resolve(true); } }); }); } /** * Stop system scheduler if it exists * @param {string} systemId - System ID to stop scheduler for * @returns {Promise<boolean>} True if stopped, false if wasn't running */ async function stopSystemScheduler(systemId) { const processName = getSystemProcessName(systemId); const isRunning = await isSystemSchedulerRunning(systemId); if (!isRunning) { return false; } return new Promise((resolve, reject) => { const { exec } = require('child_process'); exec(`pm2 delete ${processName}`, (error) => { if (error) { reject(error); } else { resolve(true); } }); }); } /** * Run backup for all enabled systems * @param {string} yemotPath - Optional custom path to backup */ async function runBackup(yemotPath) { try { logger.info('Starting backup job'); // Check if AWS is configured if (!config.isAwsConfigured()) { logger.error('AWS is not configured. Use aws:configure to set up AWS credentials.'); return; } // Filter enabled systems const enabledSystems = config.systems.filter(system => system.enabled); if (enabledSystems.length === 0) { logger.warn('No enabled systems found. Please check your configuration.'); return; } logger.info(`Found ${enabledSystems.length} enabled systems to backup`); // Run backup for each system for (const system of enabledSystems) { const backupService = new BackupService(system); await backupService.runBackup(yemotPath); } logger.info('Backup job completed'); } catch (error) { logger.error(`Backup job failed: ${error.message}`); } } /** * Verify Yemot credentials by testing login * @param {string} username - Yemot username * @param {string} password - Yemot password * @returns {Promise<boolean>} True if credentials are valid */ async function verifyYemotCredentials(username, password) { try { const api = new YemotApi(username, password); await api.getDirectoryContents('/'); return true; } catch (error) { return false; } } /** * Check if the PM2 process is running * @returns {Promise<boolean>} True if running */ async function isSchedulerRunning() { return new Promise((resolve) => { const { exec } = require('child_process'); exec(`pm2 id ${PM2_PROCESS_NAME}`, (error, stdout) => { if (error || stdout.includes('[]')) { resolve(false); } else { resolve(true); } }); }); } // Create CLI const program = new Command(); program .name('yemot-backup') .description('Backup Yemot HaMashiach recordings to Amazon S3') .version('1.0.0'); program .command('run') .description('Run backup job once immediately') .option('-p, --yemot-path <path>', 'Specific path to backup from Yemot (default: root "/")') .action(async (options) => { try { // Check configuration status if (!config.isFullyConfigured()) { if (!config.hasConfiguredSystems()) { logger.error('No systems configured. Please add a system first with systems:add command.'); } if (!config.isAwsConfigured()) { logger.error('AWS is not configured. Please configure AWS with aws:configure command.'); } process.exit(1); } await runBackup(options.yemotPath); process.exit(0); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('schedule') .description('Schedule backup job to run at specified time') .option('-s, --schedule <cron>', 'Cron schedule expression (default: 0 0 * * *)') .option('-p, --yemot-path <path>', 'Specific path to backup from Yemot (default: root "/")') .action((options) => { // Check configuration status if (!config.isFullyConfigured()) { if (!config.hasConfiguredSystems()) { logger.error('No systems configured. Please add a system first with systems:add command.'); } if (!config.isAwsConfigured()) { logger.error('AWS is not configured. Please configure AWS with aws:configure command.'); } process.exit(1); } const schedule = options.schedule || config.schedule; const yemotPath = options.yemotPath; logger.info(`Scheduling backup job with cron: ${schedule}`); cron.schedule(schedule, () => { runBackup(yemotPath).catch(error => { logger.error(`Scheduled backup failed: ${error.message}`); }); }); logger.info('Backup scheduler started. Press Ctrl+C to exit.'); }); program .command('schedule:start') .description('Start backup scheduler as background process using PM2') .option('-s, --schedule <cron>', 'Cron schedule expression (default: 0 0 * * *)') .option('-p, --yemot-path <path>', 'Specific path to backup from Yemot (default: root "/")') .action(async (options) => { try { // Check configuration status if (!config.isFullyConfigured()) { if (!config.hasConfiguredSystems()) { logger.error('No systems configured. Please add a system first with systems:add command.'); } if (!config.isAwsConfigured()) { logger.error('AWS is not configured. Please configure AWS with aws:configure command.'); } process.exit(1); } // Check if already running const isRunning = await isSchedulerRunning(); if (isRunning) { logger.info('Scheduler is already running. Use schedule:stop to stop it first.'); process.exit(0); } // Create a script to run the scheduler const schedule = options.schedule || config.schedule; const yemotPath = options.yemotPath || ''; const schedulerScriptPath = path.join(__dirname, 'scheduler.js'); const schedulerScript = ` const { runBackup } = require('./module'); const cron = require('node-cron'); const logger = require('./logger'); // Cron schedule const schedule = '${schedule}'; const yemotPath = ${yemotPath ? `'${yemotPath}'` : 'null'}; logger.info(\`Starting backup scheduler with cron: \${schedule}\`); ${yemotPath ? `logger.info(\`Backup path: ${yemotPath}\`);` : ''} cron.schedule(schedule, () => { runBackup(yemotPath).catch(error => { logger.error(\`Scheduled backup failed: \${error.message}\`); }); }); logger.info('Backup scheduler is running in the background.'); `; fs.writeFileSync(schedulerScriptPath, schedulerScript); // Start with PM2 const { exec } = require('child_process'); exec(`pm2 start ${schedulerScriptPath} --name ${PM2_PROCESS_NAME}`, (error, stdout) => { if (error) { logger.error(`Failed to start scheduler: ${error.message}`); if (error.message.includes('not found')) { logger.error('PM2 is not installed. Install it with: npm install -g pm2'); } process.exit(1); } logger.info(`Backup scheduler started in background with PM2 (ID: ${PM2_PROCESS_NAME})`); logger.info('Use schedule:stop to stop the scheduler.'); process.exit(0); }); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('schedule:stop') .description('Stop the backup scheduler running in the background') .action(async () => { try { const isRunning = await isSchedulerRunning(); if (!isRunning) { logger.info('Scheduler is not running.'); process.exit(0); } const { exec } = require('child_process'); exec(`pm2 delete ${PM2_PROCESS_NAME}`, (error) => { if (error) { logger.error(`Failed to stop scheduler: ${error.message}`); process.exit(1); } logger.info('Backup scheduler stopped successfully.'); process.exit(0); }); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('schedule:status') .description('Check if the backup scheduler is running') .action(async () => { try { const isRunning = await isSchedulerRunning(); if (isRunning) { logger.info('Backup scheduler is running in the background.'); // Get more details using PM2 const { exec } = require('child_process'); exec(`pm2 show ${PM2_PROCESS_NAME}`, (error, stdout) => { if (!error) { console.log(stdout); } process.exit(0); }); } else { logger.info('Backup scheduler is not running.'); process.exit(0); } } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('schedule:list') .description('List all active schedulers (both global and system-specific)') .action(async () => { try { // Check if the global scheduler is running const isGlobalRunning = await isSchedulerRunning(); // Get all systems const systems = config.systems; // Check which systems have active schedulers const systemSchedulers = []; for (const system of systems) { const isSystemRunning = await isSystemSchedulerRunning(system.id); if (isSystemRunning) { systemSchedulers.push(system.id); } } // Output results console.log('Active schedulers:'); if (isGlobalRunning) { console.log(`- Global scheduler: ACTIVE (process name: ${PM2_PROCESS_NAME})`); } else { console.log('- Global scheduler: NOT ACTIVE'); } if (systemSchedulers.length > 0) { console.log('\nSystem-specific schedulers:'); for (const systemId of systemSchedulers) { const processName = getSystemProcessName(systemId); console.log(`- System "${systemId}": ACTIVE (process name: ${processName})`); } } else { console.log('\nNo system-specific schedulers are running.'); } console.log('\nUse the following commands to get more details:'); console.log('- Global scheduler: yemot-backup schedule:status'); console.log('- System scheduler: yemot-backup system:schedule:status -i SYSTEM_ID'); process.exit(0); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('test-connection') .description('Test connection to Yemot API for all systems') .action(async () => { try { // Check if systems are configured if (!config.hasConfiguredSystems()) { logger.error('No systems configured. Please add a system first with systems:add command.'); process.exit(1); } logger.info('Testing system connections...'); for (const system of config.systems) { try { const backupService = new BackupService(system); const directoryContents = await backupService.yemotApi.getDirectoryContents('/'); logger.info(`✓ System ${system.id} (${system.username}): Connection successful`); } catch (error) { logger.error(`✗ System ${system.id} (${system.username}): Connection failed - ${error.message}`); } } process.exit(0); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); // Add system management commands program .command('systems:list') .description('List all configured systems') .action(() => { try { if (config.systems.length === 0) { console.log('No systems configured. Use systems:add to add a new system.'); return; } console.log('Configured systems:'); config.systems.forEach(system => { const status = system.enabled ? 'Enabled' : 'Disabled'; console.log(`- ${system.id} (${system.username}): ${status}`); }); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('systems:add') .description('Add a new Yemot system') .requiredOption('-i, --id <id>', 'System ID') .requiredOption('-u, --username <username>', 'Yemot username/system number') .requiredOption('-p, --password <password>', 'Yemot password') .option('-e, --enabled <boolean>', 'Enable this system', 'true') .option('--skip-verification', 'Skip credentials verification') .action(async (options) => { try { // Verify credentials if not skipped if (!options.skipVerification) { logger.info(`Verifying credentials for ${options.username}...`); const isValid = await verifyYemotCredentials(options.username, options.password); if (!isValid) { logger.error(`Credential verification failed for ${options.username}. Invalid username or password.`); logger.info('Use --skip-verification to add anyway.'); process.exit(1); } logger.info('Credentials verified successfully.'); } const newSystem = { id: options.id, username: options.username, password: options.password, enabled: options.enabled === 'true' }; // Add to existing systems const systems = [...config.systems]; // Check if system with same ID already exists const existingIndex = systems.findIndex(s => s.id === newSystem.id); if (existingIndex >= 0) { logger.error(`System with ID "${newSystem.id}" already exists. Use systems:update instead.`); process.exit(1); } systems.push(newSystem); // Save to file const systemsPath = path.join(__dirname, 'systems.json'); fs.writeFileSync(systemsPath, JSON.stringify(systems, null, 2)); logger.info(`System "${newSystem.id}" added successfully.`); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('systems:update') .description('Update an existing Yemot system') .requiredOption('-i, --id <id>', 'System ID to update') .option('-u, --username <username>', 'Yemot username/system number') .option('-p, --password <password>', 'Yemot password') .option('-e, --enabled <boolean>', 'Enable or disable this system') .option('--skip-verification', 'Skip credentials verification') .action(async (options) => { try { // Get existing systems const systems = [...config.systems]; // Find system to update const systemIndex = systems.findIndex(s => s.id === options.id); if (systemIndex < 0) { logger.error(`System with ID "${options.id}" not found.`); process.exit(1); } // Verify credentials if both username and password are provided and verification not skipped if (options.username && options.password && !options.skipVerification) { logger.info(`Verifying credentials for ${options.username}...`); const isValid = await verifyYemotCredentials(options.username, options.password); if (!isValid) { logger.error(`Credential verification failed for ${options.username}. Invalid username or password.`); logger.info('Use --skip-verification to update anyway.'); process.exit(1); } logger.info('Credentials verified successfully.'); } // Update fields if (options.username) systems[systemIndex].username = options.username; if (options.password) systems[systemIndex].password = options.password; if (options.enabled !== undefined) systems[systemIndex].enabled = options.enabled === 'true'; // Save to file const systemsPath = path.join(__dirname, 'systems.json'); fs.writeFileSync(systemsPath, JSON.stringify(systems, null, 2)); logger.info(`System "${options.id}" updated successfully.`); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('systems:remove') .description('Remove a Yemot system') .requiredOption('-i, --id <id>', 'System ID to remove') .action(async (options) => { try { // Get existing systems const systems = [...config.systems]; // Find system to remove const systemIndex = systems.findIndex(s => s.id === options.id); if (systemIndex < 0) { logger.error(`System with ID "${options.id}" not found.`); process.exit(1); } // Check if system has a running scheduler and stop it try { const isRunning = await isSystemSchedulerRunning(options.id); if (isRunning) { logger.info(`Stopping scheduler for system "${options.id}"...`); await stopSystemScheduler(options.id); logger.info(`Scheduler for system "${options.id}" stopped.`); } } catch (err) { logger.warn(`Could not check/stop scheduler for system "${options.id}": ${err.message}`); } // Remove system systems.splice(systemIndex, 1); // Save to file const systemsPath = path.join(__dirname, 'systems.json'); fs.writeFileSync(systemsPath, JSON.stringify(systems, null, 2)); logger.info(`System "${options.id}" removed successfully.`); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('systems:show') .description('Show details for a specific system') .requiredOption('-i, --id <id>', 'System ID to show') .action(async (options) => { try { // Find system const system = config.systems.find(s => s.id === options.id); if (!system) { logger.error(`System with ID "${options.id}" not found.`); process.exit(1); } console.log('System details:'); console.log(`- ID: ${system.id}`); console.log(`- Username: ${system.username}`); console.log(`- Password: ${system.password.substring(0, 3)}${'*'.repeat(system.password.length - 3)}`); console.log(`- Enabled: ${system.enabled ? 'Yes' : 'No'}`); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); // AWS configuration commands program .command('aws:configure') .description('Configure AWS settings') .requiredOption('-k, --key <key>', 'AWS access key ID') .requiredOption('-s, --secret <secret>', 'AWS secret access key') .requiredOption('-r, --region <region>', 'AWS region') .requiredOption('-b, --bucket <bucket>', 'S3 bucket name') .action(async (options) => { try { // Update .env file const dotenv = require('dotenv'); const envPath = path.join(process.cwd(), '.env'); // Read existing .env if it exists let envConfig = {}; try { const existingEnv = fs.readFileSync(envPath, 'utf8'); envConfig = dotenv.parse(existingEnv); } catch (error) { // File doesn't exist, start with empty config } // Update values envConfig.AWS_ACCESS_KEY_ID = options.key; envConfig.AWS_SECRET_ACCESS_KEY = options.secret; envConfig.AWS_REGION = options.region; envConfig.AWS_S3_BUCKET = options.bucket; // Write back to .env const envContent = Object.entries(envConfig) .map(([key, value]) => `${key}=${value}`) .join('\n'); fs.writeFileSync(envPath, envContent); logger.info('AWS credentials saved successfully.'); // Update current config config.aws = { accessKeyId: options.key, secretAccessKey: options.secret, region: options.region, bucket: options.bucket }; } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('aws:status') .description('Show current AWS configuration') .action(() => { console.log('Current AWS Configuration:'); if (!config.isAwsConfigured()) { console.log('AWS is not configured. Use aws:configure to set up AWS credentials.'); return; } console.log(`- Access Key: ${config.aws.accessKeyId.substring(0, 4)}${'*'.repeat(16)}`); console.log(`- Secret Key: ${'*'.repeat(8)}`); console.log(`- Region: ${config.aws.region}`); console.log(`- S3 Bucket: ${config.aws.bucket}`); }); // Add system-specific scheduler commands program .command('system:schedule') .description('Start backup scheduler for a specific system') .requiredOption('-i, --id <id>', 'System ID') .option('-s, --schedule <cron>', 'Cron schedule expression (default: 0 0 * * *)') .option('-p, --yemot-path <path>', 'Specific path to backup from Yemot (default: root "/")') .action(async (options) => { try { // Check if the system exists const system = config.systems.find(s => s.id === options.id); if (!system) { logger.error(`System with ID "${options.id}" not found.`); process.exit(1); } // Check if AWS is configured if (!config.isAwsConfigured()) { logger.error('AWS is not configured. Please configure AWS with aws:configure command.'); process.exit(1); } // Check if already running const isRunning = await isSystemSchedulerRunning(options.id); if (isRunning) { logger.info(`Scheduler for system "${options.id}" is already running. Use system:schedule:stop to stop it first.`); process.exit(0); } // Create a script to run the scheduler for this system only const schedule = options.schedule || config.schedule; const yemotPath = options.yemotPath || ''; const processName = getSystemProcessName(options.id); const schedulerScriptPath = path.join(__dirname, `scheduler-${options.id}.js`); const schedulerScript = ` const { runSystemBackup } = require('./module'); const cron = require('node-cron'); const logger = require('./logger'); // Cron schedule const schedule = '${schedule}'; const systemId = '${options.id}'; const yemotPath = ${yemotPath ? `'${yemotPath}'` : 'null'}; logger.info(\`Starting backup scheduler for system \${systemId} with cron: \${schedule}\`); ${yemotPath ? `logger.info(\`Backup path: ${yemotPath}\`);` : ''} cron.schedule(schedule, () => { runSystemBackup(systemId, yemotPath).catch(error => { logger.error(\`Scheduled backup failed for system \${systemId}: \${error.message}\`); }); }); logger.info(\`Backup scheduler for system \${systemId} is running in the background.\`); `; fs.writeFileSync(schedulerScriptPath, schedulerScript); // Start with PM2 const { exec } = require('child_process'); exec(`pm2 start ${schedulerScriptPath} --name ${processName}`, (error, stdout) => { if (error) { logger.error(`Failed to start scheduler: ${error.message}`); if (error.message.includes('not found')) { logger.error('PM2 is not installed. Install it with: npm install -g pm2'); } process.exit(1); } logger.info(`Backup scheduler for system "${options.id}" started in background with PM2 (ID: ${processName})`); logger.info(`Use system:schedule:stop -i ${options.id} to stop the scheduler.`); process.exit(0); }); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('system:schedule:stop') .description('Stop the backup scheduler for a specific system') .requiredOption('-i, --id <id>', 'System ID') .action(async (options) => { try { // Check if the system exists const system = config.systems.find(s => s.id === options.id); if (!system) { logger.error(`System with ID "${options.id}" not found.`); process.exit(1); } const isRunning = await isSystemSchedulerRunning(options.id); if (!isRunning) { logger.info(`Scheduler for system "${options.id}" is not running.`); process.exit(0); } await stopSystemScheduler(options.id); logger.info(`Backup scheduler for system "${options.id}" stopped successfully.`); process.exit(0); } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program .command('system:schedule:status') .description('Check if the backup scheduler for a specific system is running') .requiredOption('-i, --id <id>', 'System ID') .action(async (options) => { try { // Check if the system exists const system = config.systems.find(s => s.id === options.id); if (!system) { logger.error(`System with ID "${options.id}" not found.`); process.exit(1); } const isRunning = await isSystemSchedulerRunning(options.id); if (isRunning) { logger.info(`Backup scheduler for system "${options.id}" is running in the background.`); // Get more details using PM2 const processName = getSystemProcessName(options.id); const { exec } = require('child_process'); exec(`pm2 show ${processName}`, (error, stdout) => { if (!error) { console.log(stdout); } process.exit(0); }); } else { logger.info(`Backup scheduler for system "${options.id}" is not running.`); process.exit(0); } } catch (error) { logger.error(`Error: ${error.message}`); process.exit(1); } }); program.parse(process.argv); // If no command specified, show help if (!process.argv.slice(2).length) { program.outputHelp(); }