Software: Apache/2.4.41 (Ubuntu). PHP/8.0.30 uname -a: Linux apirnd 5.4.0-204-generic #224-Ubuntu SMP Thu Dec 5 13:38:28 UTC 2024 x86_64 uid=33(www-data) gid=33(www-data) groups=33(www-data) Safe-mode: OFF (not secure) /usr/local/lib/node_modules/homebridge-config-ui-x/dist/modules/backup/ drwxr-xr-x | |
| Viewing file: Select action/file-type: "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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BackupService = void 0;
const os = require("os");
const tar = require("tar");
const path = require("path");
const fs = require("fs-extra");
const color = require("bash-color");
const unzipper = require("unzipper");
const child_process = require("child_process");
const dayjs = require("dayjs");
const common_1 = require("@nestjs/common");
const plugins_service_1 = require("../plugins/plugins.service");
const scheduler_service_1 = require("../../core/scheduler/scheduler.service");
const config_service_1 = require("../../core/config/config.service");
const homebridge_ipc_service_1 = require("../..//core/homebridge-ipc/homebridge-ipc.service");
const logger_service_1 = require("../../core/logger/logger.service");
let BackupService = class BackupService {
constructor(configService, pluginsService, schedulerService, homebridgeIpcService, logger) {
this.configService = configService;
this.pluginsService = pluginsService;
this.schedulerService = schedulerService;
this.homebridgeIpcService = homebridgeIpcService;
this.logger = logger;
this.scheduleInstanceBackups();
}
scheduleInstanceBackups() {
if (this.configService.ui.scheduledBackupDisable === true) {
this.logger.debug('Scheduled backups disabled.');
return;
}
const scheduleRule = new this.schedulerService.RecurrenceRule();
scheduleRule.hour = Math.floor(Math.random() * 7);
scheduleRule.minute = Math.floor(Math.random() * 59);
scheduleRule.second = Math.floor(Math.random() * 59);
this.schedulerService.scheduleJob('instance-backup', scheduleRule, () => {
this.logger.log('Running scheduled instance backup...');
this.runScheduledBackupJob();
});
}
async createBackup() {
const instanceId = this.configService.homebridgeConfig.bridge.username.replace(/:/g, '');
const backupDir = await fs.mkdtemp(path.join(os.tmpdir(), 'homebridge-backup-'));
const backupFileName = 'homebridge-backup' + '-' + instanceId + '.tar.gz';
const backupPath = path.resolve(backupDir, backupFileName);
this.logger.log(`Creating temporary backup archive at ${backupPath}`);
await fs.copy(this.configService.storagePath, path.resolve(backupDir, 'storage'), {
filter: (filePath) => (![
'instance-backups',
'nssm.exe',
'homebridge.log',
'logs',
'node_modules',
'startup.sh',
'.docker.env',
'FFmpeg',
'fdk-aac',
'.git',
'recordings',
'.homebridge.sock',
].includes(path.basename(filePath))),
});
const installedPlugins = await this.pluginsService.getInstalledPlugins();
await fs.writeJSON(path.resolve(backupDir, 'plugins.json'), installedPlugins);
await fs.writeJson(path.resolve(backupDir, 'info.json'), {
timestamp: new Date().toISOString(),
platform: os.platform(),
uix: this.configService.package.version,
node: process.version,
});
await tar.c({
portable: true,
gzip: true,
file: backupPath,
cwd: backupDir,
filter: (filePath, stat) => {
if (stat.size > 1e+7) {
this.logger.warn(`Backup is skipping "${filePath}" because it is larger than 10MB.`);
return false;
}
return true;
},
}, [
'storage', 'plugins.json', 'info.json',
]);
return {
instanceId,
backupDir,
backupPath,
backupFileName,
};
}
async ensureScheduledBackupPath() {
if (this.configService.ui.scheduledBackupPath) {
if (!await fs.pathExists(this.configService.instanceBackupPath)) {
throw new Error(`Custom instance backup path does not exists: ${this.configService.instanceBackupPath}`);
}
try {
await fs.access(this.configService.instanceBackupPath, fs.constants.W_OK | fs.constants.R_OK);
}
catch (e) {
throw new Error(`Custom instance backup path is not writable / readable by service: ${e.message}`);
}
}
else {
return await fs.ensureDir(this.configService.instanceBackupPath);
}
}
async runScheduledBackupJob() {
try {
await this.ensureScheduledBackupPath();
}
catch (e) {
this.logger.warn('Could not run scheduled backup:', e.message);
return;
}
try {
const { backupDir, backupPath, instanceId } = await this.createBackup();
await fs.copy(backupPath, path.resolve(this.configService.instanceBackupPath, 'homebridge-backup-' + instanceId + '.' + new Date().getTime().toString() + '.tar.gz'));
await fs.remove(path.resolve(backupDir));
}
catch (e) {
this.logger.warn('Failed to create scheduled instance backup:', e.message);
}
try {
const backups = await this.listScheduledBackups();
for (const backup of backups) {
if (dayjs().diff(dayjs(backup.timestamp), 'day') >= 7) {
await fs.remove(path.resolve(this.configService.instanceBackupPath, backup.fileName));
}
}
}
catch (e) {
this.logger.warn('Failed to remove old backups:', e.message);
}
}
async getNextBackupTime() {
var _a;
if (this.configService.ui.scheduledBackupDisable === true) {
return {
next: false
};
}
else {
return {
next: ((_a = this.schedulerService.scheduledJobs['instance-backup']) === null || _a === void 0 ? void 0 : _a.nextInvocation()) || false,
};
}
}
async listScheduledBackups() {
try {
await this.ensureScheduledBackupPath();
}
catch (e) {
this.logger.warn('Could get scheduled backups:', e.message);
throw new common_1.InternalServerErrorException(e.message);
}
const dirContents = await fs.readdir(this.configService.instanceBackupPath, { withFileTypes: true });
return dirContents
.filter(x => x.isFile() && x.name.match(/^homebridge-backup-[0-9A-Za-z]{12}.[0-9]{09,15}.tar.gz/))
.map(x => {
const split = x.name.split('.');
const instanceId = split[0].split('-')[2];
if (split.length === 4 && !isNaN(split[1])) {
return {
id: instanceId + '.' + split[1],
instanceId: split[0].split('-')[2],
timestamp: new Date(parseInt(split[1], 10)),
fileName: x.name,
};
}
else {
return null;
}
})
.filter((x => x !== null))
.sort((a, b) => {
if (a.id > b.id) {
return -1;
}
else if (a.id < b.id) {
return -2;
}
else {
return 0;
}
});
}
async getScheduledBackup(backupId) {
const backupPath = path.resolve(this.configService.instanceBackupPath, 'homebridge-backup-' + backupId + '.tar.gz');
if (!await fs.pathExists(backupPath)) {
return new common_1.NotFoundException();
}
return fs.createReadStream(backupPath);
}
async downloadBackup(reply) {
const { backupDir, backupPath, backupFileName } = await this.createBackup();
async function cleanup() {
await fs.remove(path.resolve(backupDir));
this.logger.log(`Backup complete, removing ${backupDir}`);
}
reply.raw.setHeader('Content-type', 'application/octet-stream');
reply.raw.setHeader('Content-disposition', 'attachment; filename=' + backupFileName);
reply.raw.setHeader('File-Name', backupFileName);
if (reply.request.hostname === 'localhost:8080') {
reply.raw.setHeader('access-control-allow-origin', 'http://localhost:4200');
}
fs.createReadStream(backupPath)
.on('close', cleanup.bind(this))
.pipe(reply.raw);
}
async uploadBackupRestore(file) {
this.restoreDirectory = undefined;
const backupDir = await fs.mkdtemp(path.join(os.tmpdir(), 'homebridge-backup-'));
file.pipe(tar.x({
cwd: backupDir,
}).on('error', (err) => {
this.logger.error(err);
}));
file.on('end', () => {
this.restoreDirectory = backupDir;
});
}
async removeRestoreDirectory() {
if (this.restoreDirectory) {
return await fs.remove(this.restoreDirectory);
}
}
async restoreFromBackup(client) {
if (!this.restoreDirectory) {
throw new common_1.BadRequestException();
}
console.log(this.restoreDirectory);
if (!await fs.pathExists(path.resolve(this.restoreDirectory, 'info.json'))) {
await this.removeRestoreDirectory();
throw new Error('Uploaded file is not a valid Homebridge Backup Archive.');
}
if (!await fs.pathExists(path.resolve(this.restoreDirectory, 'plugins.json'))) {
await this.removeRestoreDirectory();
throw new Error('Uploaded file is not a valid Homebridge Backup Archive.');
}
if (!await fs.pathExists(path.resolve(this.restoreDirectory, 'storage'))) {
await this.removeRestoreDirectory();
throw new Error('Uploaded file is not a valid Homebridge Backup Archive.');
}
const backupInfo = await fs.readJson(path.resolve(this.restoreDirectory, 'info.json'));
client.emit('stdout', color.cyan('Backup Archive Information\r\n'));
client.emit('stdout', `Source Node.js Version: ${backupInfo.node}\r\n`);
client.emit('stdout', `Source Homebridge Config UI X Version: v${backupInfo.uix}\r\n`);
client.emit('stdout', `Source Platform: ${backupInfo.platform}\r\n`);
client.emit('stdout', `Created: ${backupInfo.timestamp}\r\n`);
this.logger.warn('Starting backup restore...');
client.emit('stdout', color.cyan('\r\nRestoring backup...\r\n\r\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
client.emit('stdout', color.yellow(`Restoring Homebridge storage to ${this.configService.storagePath}\r\n`));
await new Promise(resolve => setTimeout(resolve, 100));
await fs.copy(path.resolve(this.restoreDirectory, 'storage'), this.configService.storagePath, {
filter: (filePath) => {
client.emit('stdout', `Restoring ${path.basename(filePath)}\r\n`);
return true;
},
});
client.emit('stdout', color.yellow('File restore complete.\r\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
client.emit('stdout', color.cyan('\r\nRestoring plugins...\r\n'));
const plugins = (await fs.readJson(path.resolve(this.restoreDirectory, 'plugins.json')))
.filter((x) => ![
'homebridge-config-ui-x',
].includes(x.name) && x.publicPackage);
for (const plugin of plugins) {
try {
client.emit('stdout', color.yellow(`\r\nInstalling ${plugin.name}...\r\n`));
await this.pluginsService.installPlugin({ name: plugin.name, version: plugin.installedVersion }, client);
}
catch (e) {
client.emit('stdout', color.red(`Failed to install ${plugin.name}.\r\n`));
}
}
const restoredConfig = await fs.readJson(this.configService.configPath);
if (restoredConfig.bridge) {
restoredConfig.bridge.port = this.configService.homebridgeConfig.bridge.port;
}
if (restoredConfig.bridge.bind) {
this.checkBridgeBindConfig(restoredConfig);
}
if (!Array.isArray(restoredConfig.platforms)) {
restoredConfig.platforms = [];
}
const uiConfigBlock = restoredConfig.platforms.find((x) => x.platform === 'config');
if (uiConfigBlock) {
uiConfigBlock.port = this.configService.ui.port;
if (this.configService.serviceMode || this.configService.runningInDocker) {
delete uiConfigBlock.restart;
delete uiConfigBlock.sudo;
delete uiConfigBlock.log;
}
}
else {
restoredConfig.platforms.push({
name: 'Config',
port: this.configService.ui.port,
platform: 'config',
});
}
await fs.writeJson(this.configService.configPath, restoredConfig, { spaces: 4 });
await this.removeRestoreDirectory();
client.emit('stdout', color.green('\r\nRestore Complete!\r\n'));
this.configService.hbServiceUiRestartRequired = true;
return { status: 0 };
}
async uploadHbfxRestore(file) {
this.restoreDirectory = undefined;
const backupDir = await fs.mkdtemp(path.join(os.tmpdir(), 'homebridge-backup-'));
this.logger.log(`Extracting .hbfx file to ${backupDir}`);
file.pipe(unzipper.Extract({
path: backupDir,
}));
file.on('end', () => {
this.restoreDirectory = backupDir;
});
}
async restoreHbfxBackup(client) {
var _a, _b, _c;
if (!this.restoreDirectory) {
throw new common_1.BadRequestException();
}
if (!await fs.pathExists(path.resolve(this.restoreDirectory, 'package.json'))) {
await this.removeRestoreDirectory();
throw new Error('Uploaded file is not a valid HBFX Backup Archive.');
}
if (!await fs.pathExists(path.resolve(this.restoreDirectory, 'etc', 'config.json'))) {
await this.removeRestoreDirectory();
throw new Error('Uploaded file is not a valid HBFX Backup Archive.');
}
const backupInfo = await fs.readJson(path.resolve(this.restoreDirectory, 'package.json'));
client.emit('stdout', color.cyan('Backup Archive Information\r\n'));
client.emit('stdout', `Backup Source: ${backupInfo.name}\r\n`);
client.emit('stdout', `Version: v${backupInfo.version}\r\n`);
this.logger.warn('Starting hbfx restore...');
client.emit('stdout', color.cyan('\r\nRestoring hbfx backup...\r\n\r\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
client.emit('stdout', color.yellow(`Restoring Homebridge storage to ${this.configService.storagePath}\r\n`));
await fs.copy(path.resolve(this.restoreDirectory, 'etc'), path.resolve(this.configService.storagePath), {
filter: (filePath) => {
if ([
'access.json',
'dashboard.json',
'layout.json',
'config.json',
].includes(path.basename(filePath))) {
return false;
}
client.emit('stdout', `Restoring ${path.basename(filePath)}\r\n`);
return true;
},
});
const sourceAccessoriesPath = path.resolve(this.restoreDirectory, 'etc', 'accessories');
const targeAccessoriestPath = path.resolve(this.configService.storagePath, 'accessories');
if (await fs.pathExists(sourceAccessoriesPath)) {
await fs.copy(sourceAccessoriesPath, targeAccessoriestPath, {
filter: (filePath) => {
client.emit('stdout', `Restoring ${path.basename(filePath)}\r\n`);
return true;
},
});
}
const sourceConfig = await fs.readJson(path.resolve(this.restoreDirectory, 'etc', 'config.json'));
const pluginMap = {
'hue': 'homebridge-hue',
'chamberlain': 'homebridge-chamberlain',
'google-home': 'homebridge-gsh',
'ikea-tradfri': 'homebridge-ikea-tradfri-gateway',
'nest': 'homebridge-nest',
'ring': 'homebridge-ring',
'roborock': 'homebridge-roborock',
'shelly': 'homebridge-shelly',
'wink': 'homebridge-wink3',
'homebridge-tuya-web': '@milo526/homebridge-tuya-web',
};
if ((_a = sourceConfig.plugins) === null || _a === void 0 ? void 0 : _a.length) {
for (let plugin of sourceConfig.plugins) {
if (plugin in pluginMap) {
plugin = pluginMap[plugin];
}
try {
client.emit('stdout', color.yellow(`\r\nInstalling ${plugin}...\r\n`));
await this.pluginsService.installPlugin({ name: plugin, version: 'latest' }, client);
}
catch (e) {
client.emit('stdout', color.red(`Failed to install ${plugin}.\r\n`));
}
}
}
const targetConfig = JSON.parse(JSON.stringify({
bridge: sourceConfig.bridge,
accessories: ((_b = sourceConfig.accessories) === null || _b === void 0 ? void 0 : _b.map((x) => {
delete x.plugin_map;
return x;
})) || [],
platforms: ((_c = sourceConfig.platforms) === null || _c === void 0 ? void 0 : _c.map((x) => {
if (x.platform === 'google-home') {
x.platform = 'google-smarthome';
x.notice = 'Keep your token a secret!';
}
delete x.plugin_map;
return x;
})) || [],
}));
targetConfig.bridge.name = 'Homebridge ' + targetConfig.bridge.username.substr(targetConfig.bridge.username.length - 5).replace(/:/g, '');
if (targetConfig.bridge.bind) {
this.checkBridgeBindConfig(targetConfig);
}
targetConfig.platforms.push(this.configService.ui);
await fs.writeJson(this.configService.configPath, targetConfig, { spaces: 4 });
await this.removeRestoreDirectory();
client.emit('stdout', color.green('\r\nRestore Complete!\r\n'));
this.configService.hbServiceUiRestartRequired = true;
return { status: 0 };
}
postBackupRestoreRestart() {
setTimeout(() => {
if (this.configService.serviceMode) {
this.homebridgeIpcService.killHomebridge();
setTimeout(() => {
process.kill(process.pid, 'SIGKILL');
}, 500);
return;
}
if (this.configService.runningInDocker) {
try {
return child_process.execSync('killall -9 homebridge; kill -9 $(pidof homebridge-config-ui-x);');
}
catch (e) {
this.logger.error(e);
this.logger.error('Failed to restart Homebridge');
}
}
if (process.connected) {
process.kill(process.ppid, 'SIGKILL');
process.kill(process.pid, 'SIGKILL');
}
if (this.configService.ui.noFork) {
return process.kill(process.pid, 'SIGKILL');
}
if (os.platform() === 'linux' && this.configService.ui.standalone) {
try {
const getPidByPort = (port) => {
try {
return parseInt(child_process.execSync(`fuser ${port}/tcp 2>/dev/null`).toString('utf8').trim(), 10);
}
catch (e) {
return null;
}
};
const getPidByName = () => {
try {
return parseInt(child_process.execSync('pidof homebridge').toString('utf8').trim(), 10);
}
catch (e) {
return null;
}
};
const homebridgePid = getPidByPort(this.configService.homebridgeConfig.bridge.port) || getPidByName();
if (homebridgePid) {
process.kill(homebridgePid, 'SIGKILL');
return process.kill(process.pid, 'SIGKILL');
}
}
catch (e) {
}
}
if (this.configService.ui.restart) {
return child_process.exec(this.configService.ui.restart, (err) => {
if (err) {
this.logger.log('Restart command exited with an error. Failed to restart Homebridge.');
}
});
}
return process.kill(process.pid, 'SIGKILL');
}, 500);
return { status: 0 };
}
checkBridgeBindConfig(restoredConfig) {
if (restoredConfig.bridge.bind) {
if (typeof restoredConfig.bridge.bind === 'string') {
restoredConfig.bridge.bind = [restoredConfig.bridge.bind];
}
if (!Array.isArray(restoredConfig.bridge.bind)) {
delete restoredConfig.bridge.bind;
return;
}
const networkInterfaces = os.networkInterfaces();
restoredConfig.bridge.bind = restoredConfig.bridge.bind.filter((x) => networkInterfaces[x]);
if (!restoredConfig.bridge.bind) {
delete restoredConfig.bridge.bind;
}
}
}
};
BackupService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [config_service_1.ConfigService,
plugins_service_1.PluginsService,
scheduler_service_1.SchedulerService,
homebridge_ipc_service_1.HomebridgeIpcService,
logger_service_1.Logger])
], BackupService);
exports.BackupService = BackupService;
//# sourceMappingURL=backup.service.js.map |
:: Command execute :: | |
--[ c99shell v. 2.5 [PHP 8 Update] [24.05.2025] | Generation time: 0.0062 ]-- |