UNPKG

homebridge-config-ui-x

Version:

A web based management, configuration and control platform for Homebridge.

362 lines • 16.2 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigEditorService = void 0; const node_path_1 = require("node:path"); const common_1 = require("@nestjs/common"); const dayjs_1 = __importDefault(require("dayjs")); const fs_extra_1 = require("fs-extra"); const semver_1 = require("semver"); const config_service_1 = require("../../core/config/config.service"); const logger_service_1 = require("../../core/logger/logger.service"); const scheduler_service_1 = require("../../core/scheduler/scheduler.service"); const plugins_service_1 = require("../plugins/plugins.service"); let ConfigEditorService = class ConfigEditorService { constructor(logger, configService, schedulerService, pluginsService) { this.logger = logger; this.configService = configService; this.schedulerService = schedulerService; this.pluginsService = pluginsService; this.start(); this.scheduleConfigBackupCleanup(); } async start() { await this.ensureBackupPathExists(); await this.migrateConfigBackups(); } scheduleConfigBackupCleanup() { const scheduleRule = new this.schedulerService.RecurrenceRule(); scheduleRule.hour = 1; scheduleRule.minute = 10; scheduleRule.second = Math.floor(Math.random() * 59) + 1; this.logger.debug(`Next config.json backup cleanup scheduled for ${scheduleRule.nextInvocationDate(new Date()).toString()}.`); this.schedulerService.scheduleJob('cleanup-config-backups', scheduleRule, () => { this.logger.log('Running job to cleanup config.json backup files older than 60 days...'); this.cleanupConfigBackups(); }); } async getConfigFile() { const config = await (0, fs_extra_1.readJson)(this.configService.configPath); if (!config.bridge || typeof config.bridge !== 'object') { config.bridge = {}; } if (!config.accessories || !Array.isArray(config.accessories)) { config.accessories = []; } if (!config.platforms || !Array.isArray(config.platforms)) { config.platforms = []; } return config; } async updateConfigFile(config) { const now = new Date(); if (!config) { config = {}; } if (!config.bridge) { config.bridge = {}; } if (typeof config.bridge.port === 'string') { config.bridge.port = Number.parseInt(config.bridge.port, 10); } if (!config.bridge.port || typeof config.bridge.port !== 'number' || config.bridge.port > 65533 || config.bridge.port < 1025) { config.bridge.port = Math.floor(Math.random() * (52000 - 51000 + 1) + 51000); } if (!config.bridge.username) { config.bridge.username = this.generateUsername(); } const usernamePattern = /^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$/i; if (!usernamePattern.test(config.bridge.username)) { if (usernamePattern.test(this.configService.homebridgeConfig.bridge.username)) { config.bridge.username = this.configService.homebridgeConfig.bridge.username; } else { config.bridge.username = this.generateUsername(); } } if (!config.bridge.pin) { config.bridge.pin = this.generatePin(); } const pinPattern = /^\d{3}-\d{2}-\d{3}$/; if (!pinPattern.test(config.bridge.pin)) { if (pinPattern.test(this.configService.homebridgeConfig.bridge.pin)) { config.bridge.pin = this.configService.homebridgeConfig.bridge.pin; } else { config.bridge.pin = this.generatePin(); } } if (!config.bridge.name || typeof config.bridge.name !== 'string') { config.bridge.name = `Homebridge ${config.bridge.username.substring(config.bridge.username.length - 5).replace(/:/g, '')}`; } if (!config.accessories || !Array.isArray(config.accessories)) { config.accessories = []; } if (!config.platforms || !Array.isArray(config.platforms)) { config.platforms = []; } if (config.plugins && Array.isArray(config.plugins)) { if (!config.plugins.length) { delete config.plugins; } } else if (config.plugins) { delete config.plugins; } if (config.mdns && typeof config.mdns !== 'object') { delete config.mdns; } if (config.disabledPlugins && !Array.isArray(config.disabledPlugins)) { delete config.disabledPlugins; } try { await (0, fs_extra_1.rename)(this.configService.configPath, (0, node_path_1.resolve)(this.configService.configBackupPath, `config.json.${now.getTime().toString()}`)); } catch (e) { if (e.code === 'ENOENT') { await this.ensureBackupPathExists(); } else { this.logger.warn(`Could not create a backup of the config.json file to ${this.configService.configBackupPath} as ${e.message}.`); } } (0, fs_extra_1.writeJsonSync)(this.configService.configPath, config, { spaces: 4 }); this.logger.log('Changes to config.json saved.'); const configCopy = JSON.parse(JSON.stringify(config)); this.configService.parseConfig(configCopy); return config; } async getConfigForPlugin(pluginName) { const [plugin, config] = await Promise.all([ await this.pluginsService.getPluginAlias(pluginName), await this.getConfigFile(), ]); if (!plugin.pluginAlias) { return new common_1.BadRequestException('Plugin alias could not be determined.'); } const arrayKey = plugin.pluginType === 'accessory' ? 'accessories' : 'platforms'; return config[arrayKey].filter((block) => { return block[plugin.pluginType] === plugin.pluginAlias || block[plugin.pluginType] === `${pluginName}.${plugin.pluginAlias}`; }); } async updateConfigForPlugin(pluginName, pluginConfig) { const [plugin, config] = await Promise.all([ await this.pluginsService.getPluginAlias(pluginName), await this.getConfigFile(), ]); if (!plugin.pluginAlias) { return new common_1.BadRequestException('Plugin alias could not be determined.'); } const arrayKey = plugin.pluginType === 'accessory' ? 'accessories' : 'platforms'; if (!Array.isArray(pluginConfig)) { throw new common_1.BadRequestException('Plugin Config must be an array.'); } for (const block of pluginConfig) { if (typeof block !== 'object' || Array.isArray(block)) { throw new common_1.BadRequestException('Plugin config must be an array of objects.'); } block[plugin.pluginType] = plugin.pluginAlias; } let positionIndices; config[arrayKey] = config[arrayKey].filter((block, index) => { if (block[plugin.pluginType] === plugin.pluginAlias || block[plugin.pluginType] === `${pluginName}.${plugin.pluginAlias}`) { positionIndices = index; return false; } else { return true; } }); pluginConfig.forEach((block) => { if (block._bridge) { const isEnvObjAllowed = (0, semver_1.gte)(this.configService.homebridgeVersion, '1.8.0'); Object.keys(block._bridge).forEach((key) => { if (key === 'env' && isEnvObjAllowed) { Object.keys(block._bridge.env).forEach((envKey) => { if (block._bridge.env[envKey] === undefined || typeof block._bridge.env[envKey] !== 'string' || block._bridge.env[envKey].trim() === '') { delete block._bridge.env[envKey]; } }); if (Object.keys(block._bridge.env).length === 0) { delete block._bridge.env; } } else { if (block._bridge[key] === undefined || (typeof block._bridge[key] === 'string' && block._bridge[key].trim() === '')) { delete block._bridge[key]; } } }); } }); if (positionIndices !== undefined) { config[arrayKey].splice(positionIndices, 0, ...pluginConfig); } else { config[arrayKey].push(...pluginConfig); } await this.updateConfigFile(config); return pluginConfig; } async setPropertyForUi(property, value) { if (property === 'platform') { throw new common_1.BadRequestException('Cannot update the platform property.'); } const config = await this.getConfigFile(); const pluginConfig = config.platforms.find(x => x.platform === 'config'); if (value === '' || value === null || value === undefined) { delete pluginConfig[property]; } else { pluginConfig[property] = value; } await this.updateConfigFile(config); } async disablePlugin(pluginName) { if (pluginName === this.configService.name) { throw new common_1.BadRequestException('Disabling this plugin is now allowed.'); } const config = await this.getConfigFile(); if (!Array.isArray(config.disabledPlugins)) { config.disabledPlugins = []; } config.disabledPlugins.push(pluginName); await this.updateConfigFile(config); return config.disabledPlugins; } async enablePlugin(pluginName) { const config = await this.getConfigFile(); if (!Array.isArray(config.disabledPlugins)) { config.disabledPlugins = []; } const idx = config.disabledPlugins.findIndex(x => x === pluginName); if (idx > -1) { config.disabledPlugins.splice(idx, 1); await this.updateConfigFile(config); } return config.disabledPlugins; } async listConfigBackups() { const dirContents = await (0, fs_extra_1.readdir)(this.configService.configBackupPath); return dirContents .filter(x => x.match(/^config.json.\d{09,15}/)) .sort() .reverse() .map((x) => { const ext = x.split('.'); if (ext.length === 3 && !Number.isNaN(ext[2])) { return { id: ext[2], timestamp: new Date(Number.parseInt(ext[2], 10)), file: x, }; } else { return null; } }) .filter(x => x && !Number.isNaN(x.timestamp.getTime())); } async getConfigBackup(backupId) { const requestedBackupPath = (0, node_path_1.resolve)(this.configService.configBackupPath, `config.json.${backupId}`); if (!await (0, fs_extra_1.pathExists)(requestedBackupPath)) { throw new common_1.NotFoundException(`Backup ${backupId} Not Found`); } return await (0, fs_extra_1.readFile)(requestedBackupPath); } async deleteAllConfigBackups() { const backups = await this.listConfigBackups(); backups.forEach(async (backupFile) => { await (0, fs_extra_1.unlink)((0, node_path_1.resolve)(this.configService.configBackupPath, backupFile.file)); }); } async ensureBackupPathExists() { try { await (0, fs_extra_1.ensureDir)(this.configService.configBackupPath); } catch (e) { this.logger.error(`Could not create directory for config backups ${this.configService.configBackupPath} as ${e.message}.`); this.logger.error(`Config backups will continue to use ${this.configService.storagePath}.`); this.configService.configBackupPath = this.configService.storagePath; } } async cleanupConfigBackups() { try { const backups = await this.listConfigBackups(); for (const backup of backups) { if ((0, dayjs_1.default)().diff((0, dayjs_1.default)(backup.timestamp), 'day') >= 60) { await (0, fs_extra_1.remove)((0, node_path_1.resolve)(this.configService.configBackupPath, backup.file)); } } } catch (e) { this.logger.warn(`Failed to cleanup old config.json backup files as ${e.message}`); } } async migrateConfigBackups() { try { if (this.configService.configBackupPath === this.configService.storagePath) { this.logger.error('Skipping migration of existing config.json backups...'); return; } const dirContents = await (0, fs_extra_1.readdir)(this.configService.storagePath); const backups = dirContents .filter(x => x.match(/^config.json.\d{09,15}/)) .sort() .reverse(); for (const backupFileName of backups.splice(0, 100)) { const sourcePath = (0, node_path_1.resolve)(this.configService.storagePath, backupFileName); const targetPath = (0, node_path_1.resolve)(this.configService.configBackupPath, backupFileName); await (0, fs_extra_1.move)(sourcePath, targetPath, { overwrite: true }); } for (const backupFileName of backups) { const sourcePath = (0, node_path_1.resolve)(this.configService.storagePath, backupFileName); await (0, fs_extra_1.remove)(sourcePath); } } catch (e) { this.logger.warn(`Migrating config.json backups to new location failed as ${e.message}.`); } } generatePin() { let code = `${Math.floor(10000000 + Math.random() * 90000000)}`; code = code.split(''); code.splice(3, 0, '-'); code.splice(6, 0, '-'); code = code.join(''); return code; } generateUsername() { const hexDigits = '0123456789ABCDEF'; let username = '0E:'; for (let i = 0; i < 5; i++) { username += hexDigits.charAt(Math.round(Math.random() * 15)); username += hexDigits.charAt(Math.round(Math.random() * 15)); if (i !== 4) { username += ':'; } } return username; } }; exports.ConfigEditorService = ConfigEditorService; exports.ConfigEditorService = ConfigEditorService = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [logger_service_1.Logger, config_service_1.ConfigService, scheduler_service_1.SchedulerService, plugins_service_1.PluginsService]) ], ConfigEditorService); //# sourceMappingURL=config-editor.service.js.map