UNPKG

pbx-backup-tool

Version:

A tool for backing up Vodia PBX systems

203 lines (166 loc) 6.29 kB
const fs = require('fs-extra'); const path = require('path'); const axios = require('axios'); const chalk = require('chalk'); const { URL } = require('url'); const ProgressBar = require('progress'); const config = require('./config'); const EXCLUDED_DIRS = ['astatd', 'astath', 'astatm', 'astatw', 'billcdr', 'billcfg', 'billdata', 'blocklist', 'cdre', 'cdri', 'cdrt', 'connections', 'emails', 'gstatd', 'gstath', 'gstatm', 'gstatw', 'pcap', 'registrations']; class PBXBackup { constructor() { this.config = config.loadConfig(); this.auth = { username: this.config.username, password: this.config.password }; this.baseUrl = `${this.config.server}/rest/system/textedit?path=`; this.concurrentDownloads = 4; this.activeDownloads = 0; this.queue = []; } async run() { if (!this.validateConfig()) { throw new Error('Invalid configuration'); } await fs.ensureDir(this.config.pbx_target_dir); console.log(chalk.green(`Starting backup to: ${this.config.pbx_target_dir}`)); await this.fetchDirectory(this.config.pbx_work_dir); await this.processQueue(); // Make pbxctrl files executable const pbxCtrlFiles = await fs.readdir(this.config.pbx_target_dir); for (const file of pbxCtrlFiles) { if (file.startsWith('pbxctrl')) { const filePath = path.join(this.config.pbx_target_dir, file); await fs.chmod(filePath, 0o755); console.log(chalk.yellow(`Made executable: ${filePath}`)); } } console.log(chalk.green(`Backup complete! Files saved in ${this.config.pbx_target_dir}`)); } async fetchDirectory(dirPath) { const encodedPath = encodeURIComponent(dirPath); const url = `${this.baseUrl}${encodedPath}`; try { const response = await axios.get(url, { auth: this.auth, headers: { 'Content-Type': 'application/json; charset=utf-8' } }); // Parse the directory listing const lines = response.data.split('\n'); for (const line of lines) { if (!line.trim()) continue; // Parse line format: "D name size" for directories or "name size" for files const parts = line.trim().split(/\s+/); if (parts.length < 2) continue; let type, name, size; if (parts[0] === 'D') { // Directory line: "D name size" type = 'D'; name = parts[1]; size = parts[2]; } else { // File line: "name size" type = 'F'; name = parts[0]; size = parts[1]; } if (!name || name === '.' || name === '..') continue; if (type === 'D') { if (EXCLUDED_DIRS.includes(name)) { console.log(chalk.gray(`Skipping excluded directory: ${name}`)); continue; } const remotePath = `${dirPath}/${name}`; const localPath = path.join(this.config.pbx_target_dir, remotePath.replace(this.config.pbx_work_dir, '')); await fs.ensureDir(localPath); console.log(chalk.blue(`Created directory: ${localPath}`)); await this.fetchDirectory(remotePath); } else if (type === 'F') { const remotePath = `${dirPath}/${name}`; const localPath = path.join(this.config.pbx_target_dir, remotePath.replace(this.config.pbx_work_dir, '')); const folderName = path.basename(path.dirname(remotePath)); // Check for incremental backup conditions if ((folderName === 'cdr' || folderName === 'recordings') && await fs.pathExists(localPath)) { console.log(chalk.gray(`Skipping existing file (incremental): ${localPath}`)); continue; } this.addToDownloadQueue(remotePath, localPath); } } } catch (error) { console.error(chalk.red(`Error fetching directory ${dirPath}:`, error.message)); } } addToDownloadQueue(remotePath, localPath) { this.queue.push({ remotePath, localPath }); } async processQueue() { if (this.queue.length === 0) { console.log(chalk.yellow('No files to download')); return; } const bar = new ProgressBar('Downloading [:bar] :percent :etas', { complete: '=', incomplete: ' ', width: 20, total: this.queue.length }); const downloadNext = async () => { if (this.queue.length === 0) { if (this.activeDownloads === 0) return; return; } if (this.activeDownloads >= this.concurrentDownloads) return; this.activeDownloads++; const { remotePath, localPath } = this.queue.shift(); try { const encodedPath = encodeURIComponent(remotePath); const url = `${this.baseUrl}${encodedPath}`; const response = await axios.get(url, { auth: this.auth, responseType: 'stream', headers: { 'Content-Type': 'application/json; charset=utf-8' } }); await fs.ensureDir(path.dirname(localPath)); const writer = fs.createWriteStream(localPath); response.data.pipe(writer); await new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); console.log(chalk.green(`Downloaded: ${localPath}`)); bar.tick(); } catch (error) { console.error(chalk.red(`Error downloading ${remotePath}:`, error.message)); } finally { this.activeDownloads--; await downloadNext(); } }; // Start initial downloads const initialDownloads = Math.min(this.concurrentDownloads, this.queue.length); const downloadPromises = []; for (let i = 0; i < initialDownloads; i++) { downloadPromises.push(downloadNext()); } await Promise.all(downloadPromises); } validateConfig() { const required = ['username', 'password', 'server', 'pbx_work_dir', 'pbx_target_dir']; for (const field of required) { if (!this.config[field]) { console.error(chalk.red(`Missing required configuration: ${field}`)); return false; } } return true; } } async function runBackup() { const backup = new PBXBackup(); await backup.run(); } module.exports = { PBXBackup, runBackup };