From 83b490a9684334e018fee7379065c6bf4555cf2c Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Tue, 14 Jun 2022 20:14:01 +0200 Subject: [PATCH] Add zigbee2mqtt/bridge/request/backup (#12797) * Add backup call * Updates --- lib/extension/bridge.ts | 14 +++++ lib/util/utils.ts | 14 +++++ package-lock.json | 115 ++++++++++++++++++++++++++++++++++++++++ package.json | 1 + test/bridge.test.js | 38 +++++++++++++ 5 files changed, 182 insertions(+) diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index a3e6f8f0..c9be0354 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -10,6 +10,9 @@ import {detailedDiff} from 'deep-object-diff'; import Extension from './extension'; import Device from '../model/device'; import Group from '../model/group'; +import data from '../util/data'; +import JSZip from 'jszip'; +import fs from 'fs'; const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`); @@ -38,6 +41,7 @@ export default class Bridge extends Extension { 'group/rename': this.groupRename, 'permit_join': this.permitJoin, 'restart': this.restart, + 'backup': this.backup, 'touchlink/factory_reset': this.touchlinkFactoryReset, 'touchlink/identify': this.touchlinkIdentify, 'touchlink/scan': this.touchlinkScan, @@ -233,6 +237,16 @@ export default class Bridge extends Extension { return utils.getResponse(message, {}, null); } + @bind async backup(message: string | KeyValue): Promise { + const dataPath = data.getPath(); + const files = utils.getAllFiles(dataPath).map((f) => [f, f.substring(dataPath.length + 1)]) + .filter((f) => !f[1].startsWith('log')); + const zip = new JSZip(); + files.forEach((f) => zip.file(f[1], fs.readFileSync(f[0]))); + const base64Zip = await zip.generateAsync({type: 'base64'}); + return utils.getResponse(message, {zip: base64Zip}, null); + } + @bind async permitJoin(message: KeyValue | string): Promise { if (typeof message === 'object' && !message.hasOwnProperty('value')) { throw new Error('Invalid payload'); diff --git a/lib/util/utils.ts b/lib/util/utils.ts index b8a16578..535147d8 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -234,6 +234,19 @@ function containsControlCharacter(str: string): boolean { return false; } +function getAllFiles(path_: string): string[] { + const result = []; + for (let item of fs.readdirSync(path_)) { + item = path.join(path_, item); + if (fs.lstatSync(item).isFile()) { + result.push(item); + } else { + result.push(...getAllFiles(item)); + } + } + return result; +} + function validateFriendlyName(name: string, throwFirstError=false): string[] { const errors = []; for (const endpointName of endpointNames) { @@ -335,4 +348,5 @@ export default { getExternalConvertersDefinitions, removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase, parseEntityID, isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep, sanitizeImageParameter, isAvailabilityEnabledForEntity, publishLastSeen, availabilityPayload, + getAllFiles, }; diff --git a/package-lock.json b/package-lock.json index 2589532a..3e6ed9bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "humanize-duration": "^3.27.2", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", + "jszip": "^3.10.0", "mkdir-recursive": "^0.4.0", "moment": "^2.29.3", "mqtt": "4.3.7", @@ -5497,6 +5498,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -7695,6 +7701,39 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.0.tgz", + "integrity": "sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7731,6 +7770,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8243,6 +8290,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8847,6 +8899,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -13758,6 +13815,11 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -15405,6 +15467,41 @@ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "dev": true }, + "jszip": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.0.tgz", + "integrity": "sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -15432,6 +15529,14 @@ "type-check": "~0.4.0" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -15821,6 +15926,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16265,6 +16375,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", diff --git a/package.json b/package.json index a60a5aff..67d4061b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "humanize-duration": "^3.27.2", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", + "jszip": "^3.10.0", "mkdir-recursive": "^0.4.0", "moment": "^2.29.3", "mqtt": "4.3.7", diff --git a/test/bridge.test.js b/test/bridge.test.js index fd52bf29..c2399a5e 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -4,9 +4,23 @@ const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); const MQTT = require('./stub/mqtt'); const settings = require('../lib/util/settings'); const Controller = require('../lib/controller'); +const fs = require('fs'); +const path = require('path'); const flushPromises = require('./lib/flushPromises'); const stringify = require('json-stable-stringify-without-jsonify'); +const mockJSZipFile = jest.fn(); +const mockJSZipGenerateAsync = jest.fn().mockReturnValue("THISISBASE64"); + +jest.mock("jszip", () => + jest.fn().mockImplementation((path) => { + return { + file: mockJSZipFile, + generateAsync: mockJSZipGenerateAsync, + } + }) +); + const {coordinator, bulb, unsupported, WXKG11LM, remote, ZNCZ02LM, bulb_color_2, WSDCGQ11LM} = zigbeeHerdsman.devices; zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr); @@ -995,6 +1009,30 @@ describe('Bridge', () => { ); }); + it('Should allow to create a backup', async () => { + fs.mkdirSync(path.join(data.mockDir, 'ext_converters')); + fs.writeFileSync(path.join(data.mockDir, 'ext_converters', 'afile.js'), 'test123') + fs.mkdirSync(path.join(data.mockDir, 'log')); + fs.writeFileSync(path.join(data.mockDir, 'log', 'log.txt'), 'test123') + fs.mkdirSync(path.join(data.mockDir, 'ext_converters', '123')); + fs.writeFileSync(path.join(data.mockDir, 'ext_converters', '123', 'myfile.js'), 'test123') + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/request/backup', ''); + await flushPromises(); + expect(mockJSZipFile).toHaveBeenCalledTimes(4); + expect(mockJSZipFile).toHaveBeenNthCalledWith(1, 'configuration.yaml', expect.any(Object)); + expect(mockJSZipFile).toHaveBeenNthCalledWith(2, 'ext_converters/123/myfile.js', expect.any(Object)); + expect(mockJSZipFile).toHaveBeenNthCalledWith(3, 'ext_converters/afile.js', expect.any(Object)); + expect(mockJSZipFile).toHaveBeenNthCalledWith(4, 'state.json', expect.any(Object)); + expect(mockJSZipGenerateAsync).toHaveBeenCalledTimes(1); + expect(mockJSZipGenerateAsync).toHaveBeenNthCalledWith(1, {type: 'base64'}); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/backup', + stringify({"data":{"zip":"THISISBASE64"},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + it('Should allow to restart', async () => { zigbeeHerdsman.permitJoin.mockClear(); MQTT.publish.mockClear();