yemot-backup
Version:
Automatic backup tool for Yemot HaMashiach call recordings to Amazon S3
816 lines (711 loc) • 27.9 kB
JavaScript
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();
}