pbx-backup-tool
Version:
A tool for backing up Vodia PBX systems
203 lines (166 loc) • 6.29 kB
JavaScript
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
};