@iobroker/js-controller-cli 4.0.21 → 4.1.0-alpha.0-20220819-74ceeddc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +89 -0
- package/{index.js → build/index.js} +7 -1
- package/build/lib/cli/cliCert.d.ts +16 -0
- package/build/lib/cli/cliCert.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliCert.js +5 -9
- package/build/lib/cli/cliCert.js.map +1 -0
- package/build/lib/cli/cliCommand.d.ts +24 -0
- package/build/lib/cli/cliCommand.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliCommand.js +2 -5
- package/build/lib/cli/cliCommand.js.map +1 -0
- package/build/lib/cli/cliCompact.d.ts +26 -0
- package/build/lib/cli/cliCompact.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliCompact.js +23 -43
- package/build/lib/cli/cliCompact.js.map +1 -0
- package/build/lib/cli/cliDebug.d.ts +19 -0
- package/build/lib/cli/cliDebug.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliDebug.js +6 -13
- package/build/lib/cli/cliDebug.js.map +1 -0
- package/build/lib/cli/cliHost.d.ts +36 -0
- package/build/lib/cli/cliHost.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliHost.js +22 -47
- package/build/lib/cli/cliHost.js.map +1 -0
- package/build/lib/cli/cliLogs.d.ts +58 -0
- package/build/lib/cli/cliLogs.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliLogs.js +16 -19
- package/build/lib/cli/cliLogs.js.map +1 -0
- package/build/lib/cli/cliMessage.d.ts +10 -0
- package/build/lib/cli/cliMessage.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliMessage.js +5 -7
- package/build/lib/cli/cliMessage.js.map +1 -0
- package/build/lib/cli/cliObjects.d.ts +105 -0
- package/build/lib/cli/cliObjects.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliObjects.js +69 -110
- package/build/lib/cli/cliObjects.js.map +1 -0
- package/build/lib/cli/cliPlugin.d.ts +23 -0
- package/build/lib/cli/cliPlugin.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliPlugin.js +24 -50
- package/build/lib/cli/cliPlugin.js.map +1 -0
- package/build/lib/cli/cliProcess.d.ts +50 -0
- package/build/lib/cli/cliProcess.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliProcess.js +50 -62
- package/build/lib/cli/cliProcess.js.map +1 -0
- package/build/lib/cli/cliStates.d.ts +51 -0
- package/build/lib/cli/cliStates.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliStates.js +64 -86
- package/build/lib/cli/cliStates.js.map +1 -0
- package/build/lib/cli/cliTools.d.ts +64 -0
- package/build/lib/cli/cliTools.d.ts.map +1 -0
- package/{lib → build/lib}/cli/cliTools.js +1 -12
- package/build/lib/cli/cliTools.js.map +1 -0
- package/build/lib/cli/messages.d.ts +53 -0
- package/build/lib/cli/messages.d.ts.map +1 -0
- package/{lib → build/lib}/cli/messages.js +27 -66
- package/build/lib/cli/messages.js.map +1 -0
- package/build/lib/setup/setupBackup.d.ts +126 -0
- package/build/lib/setup/setupBackup.d.ts.map +1 -0
- package/build/lib/setup/setupBackup.js +1031 -0
- package/build/lib/setup/setupBackup.js.map +1 -0
- package/build/lib/setup/setupInstall.d.ts +23 -0
- package/build/lib/setup/setupInstall.d.ts.map +1 -0
- package/build/lib/setup/setupInstall.js +1620 -0
- package/build/lib/setup/setupInstall.js.map +1 -0
- package/build/lib/setup/setupList.d.ts +49 -0
- package/build/lib/setup/setupList.d.ts.map +1 -0
- package/build/lib/setup/setupList.js +722 -0
- package/build/lib/setup/setupList.js.map +1 -0
- package/build/lib/setup/setupPacketManager.d.ts +65 -0
- package/build/lib/setup/setupPacketManager.d.ts.map +1 -0
- package/build/lib/setup/setupPacketManager.js +216 -0
- package/build/lib/setup/setupPacketManager.js.map +1 -0
- package/build/lib/setup/setupUpload.d.ts +72 -0
- package/build/lib/setup/setupUpload.d.ts.map +1 -0
- package/build/lib/setup/setupUpload.js +784 -0
- package/build/lib/setup/setupUpload.js.map +1 -0
- package/package.json +18 -11
- package/lib/setup/setupList.js +0 -840
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Backup
|
|
4
|
+
*
|
|
5
|
+
* Copyright 2013-2022 bluefox <dogafox@gmail.com>
|
|
6
|
+
*
|
|
7
|
+
* MIT License
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.BackupRestore = void 0;
|
|
15
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
16
|
+
const js_controller_common_1 = require("@iobroker/js-controller-common");
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const setupUpload_1 = require("./setupUpload");
|
|
19
|
+
const js_controller_common_2 = require("@iobroker/js-controller-common");
|
|
20
|
+
const promisify_child_process_1 = __importDefault(require("promisify-child-process"));
|
|
21
|
+
const tar_1 = __importDefault(require("tar"));
|
|
22
|
+
const hostname = js_controller_common_1.tools.getHostName();
|
|
23
|
+
const controllerDir = js_controller_common_1.tools.getControllerDir();
|
|
24
|
+
const tmpDir = path_1.default.normalize(path_1.default.join(controllerDir, 'tmp'));
|
|
25
|
+
const bkpDir = path_1.default.normalize(path_1.default.join(controllerDir, 'backups'));
|
|
26
|
+
class BackupRestore {
|
|
27
|
+
constructor(options) {
|
|
28
|
+
options = options || {};
|
|
29
|
+
if (!options.states) {
|
|
30
|
+
throw new Error('Invalid arguments: states is missing');
|
|
31
|
+
}
|
|
32
|
+
if (!options.objects) {
|
|
33
|
+
throw new Error('Invalid arguments: objects is missing');
|
|
34
|
+
}
|
|
35
|
+
if (!options.processExit) {
|
|
36
|
+
throw new Error('Invalid arguments: processExit is missing');
|
|
37
|
+
}
|
|
38
|
+
if (!options.cleanDatabase) {
|
|
39
|
+
throw new Error('Invalid arguments: cleanDatabase is missing');
|
|
40
|
+
}
|
|
41
|
+
if (!options.restartController) {
|
|
42
|
+
throw new Error('Invalid arguments: restartController is missing');
|
|
43
|
+
}
|
|
44
|
+
this.objects = options.objects;
|
|
45
|
+
this.states = options.states;
|
|
46
|
+
this.processExit = options.processExit;
|
|
47
|
+
this.cleanDatabase = options.cleanDatabase;
|
|
48
|
+
this.restartController = options.restartController;
|
|
49
|
+
this.dbMigration = options.dbMigration || false;
|
|
50
|
+
this.PRESERVE_ADAPTERS = ['admin', 'backitup'];
|
|
51
|
+
this.upload = new setupUpload_1.Upload(options);
|
|
52
|
+
this.configParts = js_controller_common_1.tools.getConfigFileName().split('/');
|
|
53
|
+
this.configParts.pop(); // remove *.json
|
|
54
|
+
this.configDir = this.configParts.join('/'); // => name-data
|
|
55
|
+
}
|
|
56
|
+
// --------------------------------------- BACKUP ---------------------------------------------------
|
|
57
|
+
async _copyFile(id, srcPath, destPath) {
|
|
58
|
+
try {
|
|
59
|
+
// @ts-expect-error #1917
|
|
60
|
+
const data = await this.objects.readFileAsync(id, srcPath);
|
|
61
|
+
if (data) {
|
|
62
|
+
if (data.data !== undefined) {
|
|
63
|
+
fs_extra_1.default.writeFileSync(destPath, data.data);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// @ts-expect-error #1917 revisit
|
|
67
|
+
fs_extra_1.default.writeFileSync(destPath, data);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.log(`Can not copy File ${id}${srcPath} to ${destPath}: ${err.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async copyDir(id, srcPath, destPath) {
|
|
76
|
+
!fs_extra_1.default.existsSync(destPath) && fs_extra_1.default.mkdirSync(destPath);
|
|
77
|
+
try {
|
|
78
|
+
// @ts-expect-error #1917
|
|
79
|
+
const res = await this.objects.readDirAsync(id, srcPath);
|
|
80
|
+
if (res) {
|
|
81
|
+
for (const entry of res) {
|
|
82
|
+
if (entry.isDir) {
|
|
83
|
+
await this.copyDir(id, `${srcPath}/${entry.file}`, `${destPath}/${entry.file}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
fs_extra_1.default.ensureDirSync(destPath);
|
|
87
|
+
await this._copyFile(id, `${srcPath}/${entry.file}`, `${destPath}/${entry.file}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (!err.message.includes('Not exists')) {
|
|
94
|
+
console.warn(`Directory ${id}/${srcPath} cannot be copied: ${err}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
static getBackupDir() {
|
|
99
|
+
const dataDir = path_1.default.join(controllerDir, js_controller_common_1.tools.getDefaultDataDir());
|
|
100
|
+
const parts = dataDir.split('/');
|
|
101
|
+
parts.pop(); // remove data or appName-data
|
|
102
|
+
parts.pop();
|
|
103
|
+
return path_1.default.normalize(`${parts.join('/')}/backups/`);
|
|
104
|
+
}
|
|
105
|
+
copyFileSync(source, target) {
|
|
106
|
+
let targetFile = target;
|
|
107
|
+
try {
|
|
108
|
+
// if target is a directory a new file with the same name will be created
|
|
109
|
+
if (fs_extra_1.default.existsSync(target)) {
|
|
110
|
+
if (fs_extra_1.default.statSync(target).isDirectory()) {
|
|
111
|
+
targetFile = path_1.default.join(target, path_1.default.basename(source));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
fs_extra_1.default.writeFileSync(targetFile, fs_extra_1.default.readFileSync(source));
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
console.error(`Could not copy ${targetFile} to ${source}: ${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
copyFolderRecursiveSync(source, target) {
|
|
121
|
+
let files = [];
|
|
122
|
+
if (!fs_extra_1.default.existsSync(target)) {
|
|
123
|
+
fs_extra_1.default.mkdirSync(target);
|
|
124
|
+
}
|
|
125
|
+
// check if folder needs to be created or integrated
|
|
126
|
+
const targetFolder = path_1.default.join(target, path_1.default.basename(source));
|
|
127
|
+
if (!fs_extra_1.default.existsSync(targetFolder)) {
|
|
128
|
+
fs_extra_1.default.mkdirSync(targetFolder);
|
|
129
|
+
}
|
|
130
|
+
// copy
|
|
131
|
+
if (fs_extra_1.default.existsSync(source) && fs_extra_1.default.statSync(source).isDirectory()) {
|
|
132
|
+
files = fs_extra_1.default.readdirSync(source);
|
|
133
|
+
files.forEach(file => {
|
|
134
|
+
const curSource = path_1.default.join(source, file);
|
|
135
|
+
if (!fs_extra_1.default.existsSync(curSource)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (fs_extra_1.default.statSync(curSource).isDirectory()) {
|
|
139
|
+
this.copyFolderRecursiveSync(curSource, targetFolder);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.copyFileSync(curSource, targetFolder);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Pack and compress the backup
|
|
149
|
+
*
|
|
150
|
+
* @param name - backup name
|
|
151
|
+
*/
|
|
152
|
+
_packBackup(name) {
|
|
153
|
+
// 2021_10_25 BF (TODO): store letsencrypt files too
|
|
154
|
+
const letsEncrypt = `${this.configDir}/letsencrypt`;
|
|
155
|
+
if (fs_extra_1.default.existsSync(letsEncrypt)) {
|
|
156
|
+
this.copyFolderRecursiveSync(letsEncrypt, `${tmpDir}/backup`);
|
|
157
|
+
}
|
|
158
|
+
return new Promise(resolve => {
|
|
159
|
+
const f = fs_extra_1.default.createWriteStream(name);
|
|
160
|
+
f.on('finish', () => {
|
|
161
|
+
js_controller_common_1.tools.rmdirRecursiveSync(`${tmpDir}/backup`);
|
|
162
|
+
resolve(path_1.default.normalize(name));
|
|
163
|
+
});
|
|
164
|
+
f.on('error', err => {
|
|
165
|
+
console.error(`host.${hostname} Cannot pack directory ${tmpDir}/backup: ${err.message}`);
|
|
166
|
+
this.processExit(js_controller_common_2.EXIT_CODES.CANNOT_GZIP_DIRECTORY);
|
|
167
|
+
});
|
|
168
|
+
try {
|
|
169
|
+
tar_1.default.create({ gzip: true, cwd: `${tmpDir}/` }, ['backup']).pipe(f);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.error(`host.${hostname} Cannot pack directory ${tmpDir}/backup: ${err.message}`);
|
|
173
|
+
return void this.processExit(js_controller_common_2.EXIT_CODES.CANNOT_GZIP_DIRECTORY);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Creates backup and stores with given name
|
|
179
|
+
*
|
|
180
|
+
* @param name - name of the backup
|
|
181
|
+
* @param noConfig - do not store configs
|
|
182
|
+
*/
|
|
183
|
+
async createBackup(name, noConfig) {
|
|
184
|
+
var _a;
|
|
185
|
+
if (!name) {
|
|
186
|
+
const d = new Date();
|
|
187
|
+
name =
|
|
188
|
+
d.getFullYear() +
|
|
189
|
+
'_' +
|
|
190
|
+
('0' + (d.getMonth() + 1)).slice(-2) +
|
|
191
|
+
'_' +
|
|
192
|
+
('0' + d.getDate()).slice(-2) +
|
|
193
|
+
'-' +
|
|
194
|
+
('0' + d.getHours()).slice(-2) +
|
|
195
|
+
'_' +
|
|
196
|
+
('0' + d.getMinutes()).slice(-2) +
|
|
197
|
+
'_' +
|
|
198
|
+
('0' + d.getSeconds()).slice(-2) +
|
|
199
|
+
`_backup${js_controller_common_1.tools.appName}`;
|
|
200
|
+
}
|
|
201
|
+
name = name.toString().replace(/\\/g, '/');
|
|
202
|
+
if (!name.includes('/')) {
|
|
203
|
+
const backupPath = BackupRestore.getBackupDir();
|
|
204
|
+
// create directory if not exists
|
|
205
|
+
if (!fs_extra_1.default.existsSync(backupPath)) {
|
|
206
|
+
fs_extra_1.default.mkdirSync(backupPath);
|
|
207
|
+
}
|
|
208
|
+
if (!name.includes('.tar.gz')) {
|
|
209
|
+
name = `${backupPath + name}.tar.gz`;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
name = backupPath + name;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let result = { objects: null, states: {} };
|
|
216
|
+
const hostname = js_controller_common_1.tools.getHostName();
|
|
217
|
+
try {
|
|
218
|
+
// @ts-expect-error #1917
|
|
219
|
+
const res = await this.objects.getObjectListAsync({ include_docs: true });
|
|
220
|
+
if (res) {
|
|
221
|
+
result.objects = res.rows;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
console.error(`host.${hostname} Cannot get objects: ${e.message}`);
|
|
226
|
+
}
|
|
227
|
+
if (!noConfig) {
|
|
228
|
+
result.config = null;
|
|
229
|
+
}
|
|
230
|
+
if (!noConfig && fs_extra_1.default.existsSync(js_controller_common_1.tools.getConfigFileName())) {
|
|
231
|
+
result.config = fs_extra_1.default.readJSONSync(js_controller_common_1.tools.getConfigFileName());
|
|
232
|
+
}
|
|
233
|
+
const r = new RegExp(`^system\\.host\\.${hostname}\\.(\\w+)$`);
|
|
234
|
+
try {
|
|
235
|
+
// @ts-expect-error #1917
|
|
236
|
+
const keys = await this.states.getKeys('*');
|
|
237
|
+
// NOTE for all "replace" with $$$$ ... result will be just $$
|
|
238
|
+
// @ts-expect-error #1917
|
|
239
|
+
const objs = await this.states.getStates(keys);
|
|
240
|
+
// read iobroker.json
|
|
241
|
+
let isCustomHostname;
|
|
242
|
+
try {
|
|
243
|
+
const config = await fs_extra_1.default.readJSON(js_controller_common_1.tools.getConfigFileName());
|
|
244
|
+
// if a hostname is configured
|
|
245
|
+
isCustomHostname = !!config.system.hostname;
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
console.error(`host.${hostname} Cannot read config file: ${e.message}`);
|
|
249
|
+
}
|
|
250
|
+
if (keys && objs) {
|
|
251
|
+
for (let i = 0; i < keys.length; i++) {
|
|
252
|
+
const obj = objs[i];
|
|
253
|
+
if (!obj) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!isCustomHostname) {
|
|
257
|
+
// if its a default hostname, we will have a new default after restore and need to replace
|
|
258
|
+
if (obj.from === `system.host.${hostname}` || r.test(obj.from)) {
|
|
259
|
+
obj.from.replace(`system.host.${hostname}`, 'system.host.$$$$__hostname__$$$$');
|
|
260
|
+
}
|
|
261
|
+
if (r.test(keys[i])) {
|
|
262
|
+
keys[i] = keys[i].replace(hostname, '$$$$__hostname__$$$$');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
result.states[keys[i]] = obj;
|
|
266
|
+
}
|
|
267
|
+
console.log(`host.${hostname} ${keys.length} states saved`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
console.error(`host.${hostname} Cannot get states: ${e.message}`);
|
|
272
|
+
}
|
|
273
|
+
if (!fs_extra_1.default.existsSync(bkpDir)) {
|
|
274
|
+
fs_extra_1.default.mkdirSync(bkpDir);
|
|
275
|
+
}
|
|
276
|
+
if (!fs_extra_1.default.existsSync(tmpDir)) {
|
|
277
|
+
fs_extra_1.default.mkdirSync(tmpDir);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
js_controller_common_1.tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
|
|
284
|
+
}
|
|
285
|
+
if (!fs_extra_1.default.existsSync(`${tmpDir}/backup`)) {
|
|
286
|
+
fs_extra_1.default.mkdirSync(`${tmpDir}/backup`);
|
|
287
|
+
}
|
|
288
|
+
if (!fs_extra_1.default.existsSync(`${tmpDir}/backup/files`)) {
|
|
289
|
+
fs_extra_1.default.mkdirSync(`${tmpDir}/backup/files`);
|
|
290
|
+
}
|
|
291
|
+
// try to find user files
|
|
292
|
+
if (result.objects) {
|
|
293
|
+
for (const object of result.objects) {
|
|
294
|
+
if (!object || !object.value || !object.value._id || !object.value.common) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (object.value._id.match(/^system\.adapter\.([\w\d_-]+).(\d+)$/) &&
|
|
298
|
+
object.value.common.host === hostname) {
|
|
299
|
+
object.value.common.host = '$$__hostname__$$';
|
|
300
|
+
if (object.doc) {
|
|
301
|
+
object.doc.common.host = '$$__hostname__$$';
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else if (r.test(object.value._id)) {
|
|
305
|
+
object.value._id = object.value._id.replace(hostname, '$$$$__hostname__$$$$');
|
|
306
|
+
object.id = object.value._id;
|
|
307
|
+
if (object.doc) {
|
|
308
|
+
object.doc._id = object.value._id;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (object.value._id === 'system.host.' + hostname) {
|
|
312
|
+
object.value._id = 'system.host.$$__hostname__$$';
|
|
313
|
+
object.value.common.name = object.value._id;
|
|
314
|
+
object.value.common.hostname = '$$__hostname__$$';
|
|
315
|
+
if (object.value.native && object.value.native.os) {
|
|
316
|
+
object.value.native.os.hostname = '$$__hostname__$$';
|
|
317
|
+
}
|
|
318
|
+
object.id = object.value._id;
|
|
319
|
+
if (object.doc) {
|
|
320
|
+
object.doc._id = object.value._id;
|
|
321
|
+
object.doc.common.name = object.value._id;
|
|
322
|
+
object.doc.common.hostname = '$$__hostname__$$';
|
|
323
|
+
if (object.doc.native && object.value.native.os) {
|
|
324
|
+
object.doc.native.os.hostname = '$$__hostname__$$';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Read all files
|
|
329
|
+
if (object.value.type === 'meta' && object.value.common && object.value.common.type === 'meta.user') {
|
|
330
|
+
// do not process "xxx.0. " and "xxx.0."
|
|
331
|
+
if (object.id.trim() === object.id && object.id[object.id.length - 1] !== '.') {
|
|
332
|
+
await this.copyDir(object.id, '', `${tmpDir}/backup/files/${object.id}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Read all files
|
|
336
|
+
if (object.value.type === 'instance' && object.value.common && object.value.common.dataFolder) {
|
|
337
|
+
let dataFolderPath = object.value.common.dataFolder;
|
|
338
|
+
if (dataFolderPath[0] !== '/' && !dataFolderPath.match(/^\w:/)) {
|
|
339
|
+
dataFolderPath = path_1.default.join(this.configDir, dataFolderPath);
|
|
340
|
+
}
|
|
341
|
+
if (fs_extra_1.default.existsSync(dataFolderPath)) {
|
|
342
|
+
this.copyFolderRecursiveSync(dataFolderPath, `${tmpDir}/backup`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// special case: copy vis vis-common-user.css file
|
|
348
|
+
try {
|
|
349
|
+
// @ts-expect-error #1917
|
|
350
|
+
const data = await this.objects.readFileAsync('vis', 'css/vis-common-user.css');
|
|
351
|
+
if (data) {
|
|
352
|
+
const dir = `${tmpDir}/backup/files/`;
|
|
353
|
+
!fs_extra_1.default.existsSync(`${dir}vis`) && fs_extra_1.default.mkdirSync(`${dir}vis`);
|
|
354
|
+
!fs_extra_1.default.existsSync(`${dir}vis/css`) && fs_extra_1.default.mkdirSync(`${dir}vis/css`);
|
|
355
|
+
// @ts-expect-error #1917
|
|
356
|
+
fs_extra_1.default.writeFileSync(`${dir}vis/css/vis-common-user.css`, data.data !== undefined ? data.data : data);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// do not process 'css/vis-common-user.css'
|
|
361
|
+
}
|
|
362
|
+
console.log(`host.${hostname} ${((_a = result.objects) === null || _a === void 0 ? void 0 : _a.length) || 'no'} objects saved`);
|
|
363
|
+
try {
|
|
364
|
+
fs_extra_1.default.writeFileSync(`${tmpDir}/backup/backup.json`, JSON.stringify(result, null, 2));
|
|
365
|
+
result = null; // ... to allow GC to clean it up because no longer needed
|
|
366
|
+
this._validateBackupAfterCreation();
|
|
367
|
+
return await this._packBackup(name);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
console.error(`host.${hostname} Backup not created: ${err.message}`);
|
|
371
|
+
try {
|
|
372
|
+
js_controller_common_1.tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
|
|
376
|
+
}
|
|
377
|
+
return void this.processExit(js_controller_common_2.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
//--------------------------------------- RESTORE ---------------------------------------------------
|
|
381
|
+
/**
|
|
382
|
+
* Helper to restore raw states
|
|
383
|
+
*
|
|
384
|
+
* @param statesList - list of state ids
|
|
385
|
+
* @param stateObjects - list of state objects
|
|
386
|
+
*/
|
|
387
|
+
async _setStateHelper(statesList, stateObjects) {
|
|
388
|
+
for (let i = 0; i < statesList.length; i++) {
|
|
389
|
+
try {
|
|
390
|
+
// @ts-expect-error #1917 TODO revisit after merge
|
|
391
|
+
await this.states.setRawState(statesList[i], stateObjects[statesList[i]]);
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
console.log(`host.${hostname} Could not set value for state ${statesList[i]}: ${err.message}`);
|
|
395
|
+
}
|
|
396
|
+
if (i % 200 === 0) {
|
|
397
|
+
console.log(`host.${hostname} Processed ${i}/${statesList.length} states`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Sets all objects to the db and disables all adapters
|
|
403
|
+
*
|
|
404
|
+
* @param _objects - array of all objects to be set
|
|
405
|
+
*/
|
|
406
|
+
async _setObjHelper(_objects) {
|
|
407
|
+
for (let i = 0; i < _objects.length; i++) {
|
|
408
|
+
// Disable all adapters.
|
|
409
|
+
if (!this.dbMigration &&
|
|
410
|
+
_objects[i].id &&
|
|
411
|
+
/^system\.adapter\..+\.\d$/.test(_objects[i].id) &&
|
|
412
|
+
!_objects[i].id.startsWith('system.adapter.admin.') &&
|
|
413
|
+
!_objects[i].id.startsWith('system.adapter.backitup.')) {
|
|
414
|
+
if (_objects[i].doc.common && _objects[i].doc.common.enabled) {
|
|
415
|
+
_objects[i].doc.common.enabled = false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
await this.objects.setObjectAsync(_objects[i].id, _objects[i].doc);
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
console.warn(`host.${hostname} Cannot restore ${_objects[i].id}: ${err.message}`);
|
|
423
|
+
}
|
|
424
|
+
if (i % 200 === 0) {
|
|
425
|
+
console.log(`host.${hostname} Processed ${i}/${_objects.length} objects`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Creates all provided object if non existing
|
|
431
|
+
*
|
|
432
|
+
* @param objectList - list of objects to be created
|
|
433
|
+
*/
|
|
434
|
+
async _reloadAdapterObject(objectList) {
|
|
435
|
+
if (!Array.isArray(objectList)) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
for (const object of objectList) {
|
|
439
|
+
let obj;
|
|
440
|
+
try {
|
|
441
|
+
obj = await this.objects.getObjectAsync(object._id);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// ignore
|
|
445
|
+
}
|
|
446
|
+
if (!obj) {
|
|
447
|
+
// object not existing -> create it
|
|
448
|
+
try {
|
|
449
|
+
await this.objects.setObjectAsync(object._id, object);
|
|
450
|
+
console.log(`host.${hostname} object ${object._id} created`);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// ignore
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async _reloadAdaptersObjects() {
|
|
459
|
+
const dirs = [];
|
|
460
|
+
let _modules;
|
|
461
|
+
let p = path_1.default.join(controllerDir, 'node_modules');
|
|
462
|
+
if (fs_extra_1.default.existsSync(p)) {
|
|
463
|
+
if (!p.includes('js-controller')) {
|
|
464
|
+
_modules = fs_extra_1.default.readdirSync(p).filter(dir => fs_extra_1.default.existsSync(`${p}/${dir}/io-package.json`));
|
|
465
|
+
if (_modules) {
|
|
466
|
+
const regEx = new RegExp(`^${js_controller_common_1.tools.appName}\\.`, 'i');
|
|
467
|
+
for (const module of _modules) {
|
|
468
|
+
if (regEx.test(module) && !dirs.includes(module.substring(js_controller_common_1.tools.appName.length + 1))) {
|
|
469
|
+
dirs.push(module);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
p = path_1.default.join(controllerDir, '..', 'node_modules');
|
|
476
|
+
if (fs_extra_1.default.existsSync(p)) {
|
|
477
|
+
_modules = fs_extra_1.default.readdirSync(p).filter(dir => fs_extra_1.default.existsSync(`${p}/${dir}/io-package.json`));
|
|
478
|
+
if (_modules) {
|
|
479
|
+
const regEx = new RegExp(`^${js_controller_common_1.tools.appName}\\.`, 'i');
|
|
480
|
+
for (const module of _modules) {
|
|
481
|
+
if (regEx.test(module) && !dirs.includes(module.substring(js_controller_common_1.tools.appName.length + 1))) {
|
|
482
|
+
dirs.push(module);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// if installed as npm
|
|
490
|
+
if (fs_extra_1.default.existsSync(path_1.default.join(controllerDir, '..', '..', 'node_modules', `${js_controller_common_1.tools.appName}.js-controller`))) {
|
|
491
|
+
const p = path_1.default.join(controllerDir, '..');
|
|
492
|
+
_modules = fs_extra_1.default.readdirSync(p).filter(dir => fs_extra_1.default.existsSync(`${p}/${dir}/io-package.json`));
|
|
493
|
+
const regEx_ = new RegExp(`^${js_controller_common_1.tools.appName}\\.`, 'i');
|
|
494
|
+
for (const module of _modules) {
|
|
495
|
+
// if starting from application name + '.'
|
|
496
|
+
if (regEx_.test(module) &&
|
|
497
|
+
// If not js-controller
|
|
498
|
+
module.substring(js_controller_common_1.tools.appName.length + 1) !== 'js-controller' &&
|
|
499
|
+
!dirs.includes(module.substring(js_controller_common_1.tools.appName.length + 1))) {
|
|
500
|
+
dirs.push(module);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for (const dir of dirs) {
|
|
505
|
+
const adapterName = dir.replace(/^iobroker\./i, '');
|
|
506
|
+
await this.upload.uploadAdapter(adapterName, false, true);
|
|
507
|
+
await this.upload.uploadAdapter(adapterName, true, true);
|
|
508
|
+
let pkg = null;
|
|
509
|
+
if (!dir) {
|
|
510
|
+
console.error('Wrong');
|
|
511
|
+
}
|
|
512
|
+
const adapterDir = js_controller_common_1.tools.getAdapterDir(adapterName);
|
|
513
|
+
if (fs_extra_1.default.existsSync(`${adapterDir}/io-package.json`)) {
|
|
514
|
+
pkg = fs_extra_1.default.readJSONSync(`${adapterDir}/io-package.json`);
|
|
515
|
+
}
|
|
516
|
+
if (pkg && pkg.objects && pkg.objects.length) {
|
|
517
|
+
console.log(`host.${hostname} Setup "${dir}" adapter`);
|
|
518
|
+
await this._reloadAdapterObject(pkg.objects);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async _uploadUserFiles(root, uploadPath) {
|
|
523
|
+
uploadPath = uploadPath || '';
|
|
524
|
+
if (!fs_extra_1.default.existsSync(root)) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const files = fs_extra_1.default.readdirSync(root + uploadPath);
|
|
528
|
+
for (const file of files) {
|
|
529
|
+
const fName = path_1.default.join(root, uploadPath, file);
|
|
530
|
+
if (!fs_extra_1.default.existsSync(fName)) {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const stat = fs_extra_1.default.statSync(fName);
|
|
534
|
+
if (stat.isDirectory()) {
|
|
535
|
+
try {
|
|
536
|
+
await this._uploadUserFiles(root, `${uploadPath}/${file}`);
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
console.error(`Error: ${err}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const parts = uploadPath.split('/');
|
|
544
|
+
const adapter = parts.splice(0, 2)[1];
|
|
545
|
+
const _path = `${parts.join('/')}/${file}`;
|
|
546
|
+
console.log(`host.${hostname} Upload user file "${adapter}/${_path}`);
|
|
547
|
+
try {
|
|
548
|
+
await this.objects.writeFileAsync(adapter, _path, fs_extra_1.default.readFileSync(`${root + uploadPath}/${file}`));
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
console.error(`Error: ${err.message}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
_copyBackupedFiles(backupDir) {
|
|
557
|
+
try {
|
|
558
|
+
if (!fs_extra_1.default.existsSync(backupDir)) {
|
|
559
|
+
console.log('No additional files to restore');
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const dirs = fs_extra_1.default.readdirSync(backupDir);
|
|
563
|
+
dirs.forEach(dir => {
|
|
564
|
+
if (dir === 'files') {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const backupPath = path_1.default.join(backupDir, dir);
|
|
568
|
+
let stat;
|
|
569
|
+
try {
|
|
570
|
+
if (!fs_extra_1.default.existsSync(backupPath)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
stat = fs_extra_1.default.statSync(backupPath);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
console.error(`Ignoring ${backupPath} because can not get file type: ${err.message}`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (stat.isDirectory()) {
|
|
580
|
+
this.copyFolderRecursiveSync(backupPath, this.configDir);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
console.error(`Ignoring ${backupDir} because can not read directory: ${err.message}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Restore after controller has been stopped
|
|
590
|
+
*
|
|
591
|
+
* @param restartOnFinish - restart controller after restore
|
|
592
|
+
* @param force - skip the controller version check
|
|
593
|
+
* @param dontDeleteAdapters - skip adapter deletion, e.g. for setup custom db migration
|
|
594
|
+
*/
|
|
595
|
+
async _restoreAfterStop(restartOnFinish, force, dontDeleteAdapters) {
|
|
596
|
+
// Open file
|
|
597
|
+
let data = fs_extra_1.default.readFileSync(`${tmpDir}/backup/backup.json`, 'utf8');
|
|
598
|
+
const hostname = js_controller_common_1.tools.getHostName();
|
|
599
|
+
// replace all hostnames of instances etc with the new host
|
|
600
|
+
data = data.replace(/\$\$__hostname__\$\$/g, hostname);
|
|
601
|
+
fs_extra_1.default.writeFileSync(`${tmpDir}/backup/backup_.json`, data);
|
|
602
|
+
let restore;
|
|
603
|
+
try {
|
|
604
|
+
restore = JSON.parse(data);
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
console.error(`Cannot parse "${tmpDir}/backup/backup_.json": ${err.message}`);
|
|
608
|
+
return js_controller_common_2.EXIT_CODES.CANNOT_RESTORE_BACKUP;
|
|
609
|
+
}
|
|
610
|
+
// check that the same controller version is installed as it is contained in backup
|
|
611
|
+
const exitCode = this._ensureCompatibility(controllerDir, restore.config ? restore.config.system.hostname || hostname : hostname, restore.objects, force);
|
|
612
|
+
if (exitCode) {
|
|
613
|
+
// we had an error
|
|
614
|
+
return exitCode;
|
|
615
|
+
}
|
|
616
|
+
if (!dontDeleteAdapters) {
|
|
617
|
+
// prevent having wrong versions of adapters
|
|
618
|
+
await this._removeAllAdapters();
|
|
619
|
+
}
|
|
620
|
+
// stop all adapters
|
|
621
|
+
console.log(`host.${hostname} Clear all objects and states...`);
|
|
622
|
+
await this.cleanDatabase(false);
|
|
623
|
+
console.log(`host.${hostname} done.`);
|
|
624
|
+
// upload all data into DB
|
|
625
|
+
// restore ioBroker.json
|
|
626
|
+
if (restore.config) {
|
|
627
|
+
fs_extra_1.default.writeFileSync(js_controller_common_1.tools.getConfigFileName(), JSON.stringify(restore.config, null, 2));
|
|
628
|
+
}
|
|
629
|
+
const sList = Object.keys(restore.states);
|
|
630
|
+
await this._setObjHelper(restore.objects);
|
|
631
|
+
console.log(`${restore.objects.length} objects restored.`);
|
|
632
|
+
await this._setStateHelper(sList, restore.states);
|
|
633
|
+
console.log(`${sList.length} states restored.`);
|
|
634
|
+
// Load user files into DB
|
|
635
|
+
await this._uploadUserFiles(`${tmpDir}/backup/files`);
|
|
636
|
+
// reload objects of adapters (if some couldn't be removed - normally this shouldn't be necessary anymore)
|
|
637
|
+
await this._reloadAdaptersObjects();
|
|
638
|
+
// Reload host objects
|
|
639
|
+
const packageIO = fs_extra_1.default.readJSONSync(path_1.default.join(controllerDir, 'io-package.json'));
|
|
640
|
+
await this._reloadAdapterObject(packageIO ? packageIO.objects : null);
|
|
641
|
+
// copy all files into iob-data
|
|
642
|
+
await this._copyBackupedFiles(path_1.default.join(tmpDir, 'backup'));
|
|
643
|
+
// reinstall preserve adapters
|
|
644
|
+
await this._restorePreservedAdapters();
|
|
645
|
+
if (force) {
|
|
646
|
+
// js-controller version has changed (setup never called for this version)
|
|
647
|
+
console.log('Forced restore - executing setup ...');
|
|
648
|
+
try {
|
|
649
|
+
await promisify_child_process_1.default.exec(`"${process.execPath}" "${path_1.default.join(controllerDir, `${js_controller_common_1.tools.appName.toLowerCase()}.js`)}" setup`);
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
console.error(`Could not execute "setup" command, please ensure "setup" is called before starting ioBroker: ${e.message}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (restartOnFinish) {
|
|
656
|
+
this.restartController();
|
|
657
|
+
}
|
|
658
|
+
return js_controller_common_2.EXIT_CODES.NO_ERROR;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Removes all adapters
|
|
662
|
+
*/
|
|
663
|
+
async _removeAllAdapters() {
|
|
664
|
+
const nodeModulePath = path_1.default.join(controllerDir, '..');
|
|
665
|
+
const nodeModuleDirs = fs_extra_1.default.readdirSync(nodeModulePath, { withFileTypes: true });
|
|
666
|
+
// we need to uninstall current adapters to get exact the same system as before backup
|
|
667
|
+
for (const dir of nodeModuleDirs) {
|
|
668
|
+
if (dir.isDirectory() &&
|
|
669
|
+
dir.name.startsWith(`${js_controller_common_1.tools.appName.toLowerCase()}.`) &&
|
|
670
|
+
dir.name !== `${js_controller_common_1.tools.appName.toLowerCase()}.js-controller`) {
|
|
671
|
+
try {
|
|
672
|
+
const packJson = fs_extra_1.default.readJsonSync(path_1.default.join(nodeModulePath, dir.name, 'package.json'));
|
|
673
|
+
console.log(`Removing current installation of ${packJson.name}`);
|
|
674
|
+
await js_controller_common_1.tools.uninstallNodeModule(packJson.name);
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
// ignore
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Ensure that installed controller version matches version in backup
|
|
684
|
+
*
|
|
685
|
+
* @param controllerDir - directory of js-controller
|
|
686
|
+
* @param backupHostname - hostname in backup file
|
|
687
|
+
* @param backupObjects - the objects contained in the backup
|
|
688
|
+
* @param force - if force is true, only log
|
|
689
|
+
*/
|
|
690
|
+
_ensureCompatibility(controllerDir, backupHostname, backupObjects, force) {
|
|
691
|
+
try {
|
|
692
|
+
const ioPackJson = fs_extra_1.default.readJsonSync(path_1.default.join(controllerDir, 'io-package.json'));
|
|
693
|
+
const hostObj = backupObjects.find(obj => obj.id === `system.host.${backupHostname}`);
|
|
694
|
+
if (!hostObj) {
|
|
695
|
+
console.error('No host object found, your backup seems to be corrupted!');
|
|
696
|
+
return js_controller_common_2.EXIT_CODES.CANNOT_RESTORE_BACKUP;
|
|
697
|
+
}
|
|
698
|
+
if (hostObj.value.common.installedVersion !== ioPackJson.common.version) {
|
|
699
|
+
if (!force) {
|
|
700
|
+
console.warn('The current version of js-controller differs from the version in the backup.');
|
|
701
|
+
console.warn('The js-controller version of the backup can not be restored automatically.');
|
|
702
|
+
console.warn(`To restore the js-controller version of the backup, execute "npm i iobroker.js-controller@${hostObj.value.common.installedVersion} --production" inside your ioBroker directory`);
|
|
703
|
+
console.warn('If you really want to restore the backup with the current installed js-controller, execute the restore command with the --force flag');
|
|
704
|
+
return js_controller_common_2.EXIT_CODES.CANNOT_RESTORE_BACKUP;
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
console.info('The current version of js-controller differs from the version in the backup.');
|
|
708
|
+
console.info('The js-controller version of the backup can not be restored automatically.');
|
|
709
|
+
console.info('Note, that your backup might differ in behavior due to this version change!');
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
// ignore
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Returns all backups as array
|
|
719
|
+
*/
|
|
720
|
+
listBackups() {
|
|
721
|
+
const dir = BackupRestore.getBackupDir();
|
|
722
|
+
const result = [];
|
|
723
|
+
if (fs_extra_1.default.existsSync(dir)) {
|
|
724
|
+
const files = fs_extra_1.default.readdirSync(dir);
|
|
725
|
+
for (const file of files) {
|
|
726
|
+
if (file.match(/\.tar\.gz$/i)) {
|
|
727
|
+
result.push(file);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Validates the backup.json and all json files inside the backup after (in temporary directory), here we only abort if backup.json is corrupted
|
|
738
|
+
*/
|
|
739
|
+
_validateBackupAfterCreation() {
|
|
740
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
741
|
+
const backupJSON = require(`${tmpDir}/backup/backup.json`);
|
|
742
|
+
if (!backupJSON.objects || !backupJSON.objects.length) {
|
|
743
|
+
throw new Error('Backup does not contain valid objects');
|
|
744
|
+
}
|
|
745
|
+
// we check all other json files, we assume them as optional, because user created files may be no valid json
|
|
746
|
+
try {
|
|
747
|
+
this._checkDirectory(`${tmpDir}/backup/files`);
|
|
748
|
+
}
|
|
749
|
+
catch (err) {
|
|
750
|
+
console.warn(`host.${hostname} One or more optional files are corrupted: ${err.message}`);
|
|
751
|
+
console.warn(`host.${hostname} Please ensure that self-created JSON files are valid`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Validates the given backup.json and all json files in the backup, calls processExit afterwards
|
|
756
|
+
* @param _name - index or name of the backup
|
|
757
|
+
*/
|
|
758
|
+
validateBackup(_name) {
|
|
759
|
+
let backups;
|
|
760
|
+
let name = typeof _name === 'number' ? _name.toString() : _name;
|
|
761
|
+
if (!name) {
|
|
762
|
+
backups = this.listBackups();
|
|
763
|
+
backups.sort((a, b) => (b > a ? 1 : b === a ? 0 : -1));
|
|
764
|
+
if (backups.length) {
|
|
765
|
+
// List all available backups
|
|
766
|
+
console.log('Please specify one of the backup names:');
|
|
767
|
+
for (const t in backups) {
|
|
768
|
+
console.log(`${backups[t]} or ${backups[t].replace(`_backup${js_controller_common_1.tools.appName}.tar.gz`, '')} or ${t}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
console.warn(`No backups found. Create a backup, using "${js_controller_common_1.tools.appName} backup" first`);
|
|
773
|
+
}
|
|
774
|
+
return void this.processExit(10);
|
|
775
|
+
}
|
|
776
|
+
// If number
|
|
777
|
+
if (parseInt(name, 10).toString() === name.toString()) {
|
|
778
|
+
backups = this.listBackups();
|
|
779
|
+
backups.sort((a, b) => (b > a ? 1 : b === a ? 0 : -1));
|
|
780
|
+
name = backups[parseInt(name, 10)];
|
|
781
|
+
if (!name) {
|
|
782
|
+
console.log('No matching backup found');
|
|
783
|
+
if (backups.length) {
|
|
784
|
+
console.log('Please specify one of the backup names:');
|
|
785
|
+
for (const t in backups) {
|
|
786
|
+
console.log(`${backups[t]} or ${backups[t].replace(`_backup${js_controller_common_1.tools.appName}.tar.gz`, '')} or ${t}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
console.log(`No existing backups. Create a backup, using "${js_controller_common_1.tools.appName} backup" first`);
|
|
791
|
+
}
|
|
792
|
+
return void this.processExit(10);
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
console.log(`host.${hostname} Using backup file ${name}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
name = name.toString().replace(/\\/g, '/');
|
|
799
|
+
if (!name.includes('/')) {
|
|
800
|
+
name = BackupRestore.getBackupDir() + name;
|
|
801
|
+
const regEx = new RegExp(`_backup${js_controller_common_1.tools.appName}`, 'i');
|
|
802
|
+
if (!regEx.test(name)) {
|
|
803
|
+
name += `_backup${js_controller_common_1.tools.appName}`;
|
|
804
|
+
}
|
|
805
|
+
if (!name.match(/\.tar\.gz$/i)) {
|
|
806
|
+
name += '.tar.gz';
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (!fs_extra_1.default.existsSync(name)) {
|
|
810
|
+
console.error(`host.${hostname} Cannot find ${name}`);
|
|
811
|
+
return void this.processExit(11);
|
|
812
|
+
}
|
|
813
|
+
if (fs_extra_1.default.existsSync(`${tmpDir}/backup/backup.json`)) {
|
|
814
|
+
fs_extra_1.default.unlinkSync(`${tmpDir}/backup/backup.json`);
|
|
815
|
+
}
|
|
816
|
+
return new Promise(resolve => {
|
|
817
|
+
tar_1.default.extract({
|
|
818
|
+
file: name,
|
|
819
|
+
cwd: tmpDir
|
|
820
|
+
}, undefined, err => {
|
|
821
|
+
if (err) {
|
|
822
|
+
console.error(`host.${hostname} Cannot extract from file "${name}": ${err.message}`);
|
|
823
|
+
return void this.processExit(9);
|
|
824
|
+
}
|
|
825
|
+
if (!fs_extra_1.default.existsSync(`${tmpDir}/backup/backup.json`)) {
|
|
826
|
+
console.error(`host.${hostname} Validation failed. Cannot find extracted file from file "${tmpDir}/backup/backup.json"`);
|
|
827
|
+
return void this.processExit(9);
|
|
828
|
+
}
|
|
829
|
+
console.log(`host.${hostname} Starting validation ...`);
|
|
830
|
+
let backupJSON;
|
|
831
|
+
try {
|
|
832
|
+
backupJSON = require(`${tmpDir}/backup/backup.json`);
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
console.error(`host.${hostname} Backup corrupted. Backup ${name} does not contain a valid backup.json file: ${err.message}`);
|
|
836
|
+
try {
|
|
837
|
+
js_controller_common_1.tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
|
|
838
|
+
}
|
|
839
|
+
catch (e) {
|
|
840
|
+
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
|
|
841
|
+
}
|
|
842
|
+
return void this.processExit(26);
|
|
843
|
+
}
|
|
844
|
+
if (!backupJSON || !backupJSON.objects || !backupJSON.objects.length) {
|
|
845
|
+
console.error(`host.${hostname} Backup corrupted. Backup does not contain valid objects`);
|
|
846
|
+
try {
|
|
847
|
+
js_controller_common_1.tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
|
|
848
|
+
}
|
|
849
|
+
catch (e) {
|
|
850
|
+
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
|
|
851
|
+
}
|
|
852
|
+
return void this.processExit(26);
|
|
853
|
+
}
|
|
854
|
+
console.log(`host.${hostname} backup.json OK`);
|
|
855
|
+
try {
|
|
856
|
+
this._checkDirectory(`${tmpDir}/backup/files`, true);
|
|
857
|
+
try {
|
|
858
|
+
js_controller_common_1.tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
|
|
859
|
+
}
|
|
860
|
+
catch (e) {
|
|
861
|
+
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
|
|
862
|
+
}
|
|
863
|
+
resolve();
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
console.error(`host.${hostname} Backup corrupted: ${err.message}`);
|
|
867
|
+
return void this.processExit(26);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Checks a directory for json files and validates them, steps down recursive in subdirectories
|
|
874
|
+
* @param path - path to the directory
|
|
875
|
+
* @param verbose - if logging should be verbose
|
|
876
|
+
*/
|
|
877
|
+
_checkDirectory(path, verbose = false) {
|
|
878
|
+
if (fs_extra_1.default.existsSync(path)) {
|
|
879
|
+
const files = fs_extra_1.default.readdirSync(path);
|
|
880
|
+
if (!files.length) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
for (const file of files) {
|
|
884
|
+
const filePath = `${path}/${file}`;
|
|
885
|
+
if (fs_extra_1.default.existsSync(filePath) && fs_extra_1.default.statSync(filePath).isDirectory()) {
|
|
886
|
+
// if directory then check it
|
|
887
|
+
this._checkDirectory(filePath, verbose);
|
|
888
|
+
}
|
|
889
|
+
else if (file.endsWith('.json')) {
|
|
890
|
+
try {
|
|
891
|
+
require(filePath);
|
|
892
|
+
if (verbose) {
|
|
893
|
+
console.log(`host.${hostname} ${file} OK`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
throw new Error(`host.${hostname} ${filePath} is not a valid json file`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Restores a backup
|
|
905
|
+
*
|
|
906
|
+
* @param _name - backup name or index
|
|
907
|
+
* @param force - if force, js-controller is allowed to have a different version
|
|
908
|
+
* @param dontDeleteAdapters - skip adapter deletion, e.g. for setup custom db migration
|
|
909
|
+
* @param callback
|
|
910
|
+
*/
|
|
911
|
+
restoreBackup(_name, force, dontDeleteAdapters, callback) {
|
|
912
|
+
let backups;
|
|
913
|
+
let name = typeof _name === 'number' ? _name.toString() : _name;
|
|
914
|
+
if (!name) {
|
|
915
|
+
// List all available backups
|
|
916
|
+
console.log('Please specify one of the backup names:');
|
|
917
|
+
backups = this.listBackups();
|
|
918
|
+
backups.sort((a, b) => (b > a ? 1 : b === a ? 0 : -1));
|
|
919
|
+
if (backups.length) {
|
|
920
|
+
backups.forEach((backup, i) => console.log(`${backup} or ${backup.replace(`_backup${js_controller_common_1.tools.appName}.tar.gz`, '')} or ${i}`));
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
console.warn('No backups found');
|
|
924
|
+
}
|
|
925
|
+
return void this.processExit(10);
|
|
926
|
+
}
|
|
927
|
+
if (!this.cleanDatabase) {
|
|
928
|
+
throw new Error('Invalid arguments: cleanDatabase is missing');
|
|
929
|
+
}
|
|
930
|
+
if (!this.restartController) {
|
|
931
|
+
throw new Error('Invalid arguments: restartController is missing');
|
|
932
|
+
}
|
|
933
|
+
// If number
|
|
934
|
+
if (parseInt(name, 10).toString() === name.toString()) {
|
|
935
|
+
backups = this.listBackups();
|
|
936
|
+
backups.sort((a, b) => (b > a ? 1 : b === a ? 0 : -1));
|
|
937
|
+
name = backups[parseInt(name, 10)];
|
|
938
|
+
if (!name) {
|
|
939
|
+
console.log('No matching backup found');
|
|
940
|
+
if (backups.length) {
|
|
941
|
+
console.log('Please specify one of the backup names:');
|
|
942
|
+
backups.forEach((backup, i) => console.log(`${backup} or ${backup.replace(`_backup${js_controller_common_1.tools.appName}.tar.gz`, '')} or ${i}`));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
console.log(`host.${hostname} Using backup file ${name}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
name = name.toString().replace(/\\/g, '/');
|
|
950
|
+
if (!name.includes('/')) {
|
|
951
|
+
name = BackupRestore.getBackupDir() + name;
|
|
952
|
+
const regEx = new RegExp(`_backup${js_controller_common_1.tools.appName}`, 'i');
|
|
953
|
+
if (!regEx.test(name)) {
|
|
954
|
+
name += '_backup' + js_controller_common_1.tools.appName;
|
|
955
|
+
}
|
|
956
|
+
if (!name.match(/\.tar\.gz$/i)) {
|
|
957
|
+
name += '.tar.gz';
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (!fs_extra_1.default.existsSync(name)) {
|
|
961
|
+
console.error(`host.${hostname} Cannot find ${name}`);
|
|
962
|
+
return void this.processExit(11);
|
|
963
|
+
}
|
|
964
|
+
// delete /backup/backup.json
|
|
965
|
+
fs_extra_1.default.existsSync(`${tmpDir}/backup/backup.json`) && fs_extra_1.default.unlinkSync(`${tmpDir}/backup/backup.json`);
|
|
966
|
+
tar_1.default.extract({
|
|
967
|
+
file: name,
|
|
968
|
+
cwd: tmpDir
|
|
969
|
+
}, undefined, err => {
|
|
970
|
+
if (err) {
|
|
971
|
+
console.error(`host.${hostname} Cannot extract from file "${name}": ${err.message}`);
|
|
972
|
+
return void this.processExit(9);
|
|
973
|
+
}
|
|
974
|
+
if (!fs_extra_1.default.existsSync(`${tmpDir}/backup/backup.json`)) {
|
|
975
|
+
console.error(`host.${hostname} Cannot find extracted file from file "${tmpDir}/backup/backup.json"`);
|
|
976
|
+
return void this.processExit(9);
|
|
977
|
+
}
|
|
978
|
+
// Stop controller
|
|
979
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
980
|
+
const daemon = require('daemonize2').setup({
|
|
981
|
+
main: path_1.default.join(controllerDir, 'controller.js'),
|
|
982
|
+
name: `${js_controller_common_1.tools.appName} controller`,
|
|
983
|
+
pidfile: path_1.default.join(controllerDir, `${js_controller_common_1.tools.appName}.pid`),
|
|
984
|
+
cwd: controllerDir,
|
|
985
|
+
stopTimeout: 1000
|
|
986
|
+
});
|
|
987
|
+
daemon.on('error', async () => {
|
|
988
|
+
const exitCode = await this._restoreAfterStop(false, force, dontDeleteAdapters);
|
|
989
|
+
callback && callback(exitCode);
|
|
990
|
+
});
|
|
991
|
+
daemon.on('stopped', async () => {
|
|
992
|
+
const exitCode = await this._restoreAfterStop(true, force, dontDeleteAdapters);
|
|
993
|
+
callback && callback(exitCode);
|
|
994
|
+
});
|
|
995
|
+
daemon.on('notrunning', async () => {
|
|
996
|
+
console.log(`host.${hostname} OK.`);
|
|
997
|
+
const exitCode = await this._restoreAfterStop(false, force, dontDeleteAdapters);
|
|
998
|
+
callback && callback(exitCode);
|
|
999
|
+
});
|
|
1000
|
+
daemon.stop();
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* This method checks if adapter of PRESERVE_ADAPTERS exist, and reinstalls them if this is the case
|
|
1005
|
+
*/
|
|
1006
|
+
async _restorePreservedAdapters() {
|
|
1007
|
+
for (const adapterName of this.PRESERVE_ADAPTERS) {
|
|
1008
|
+
try {
|
|
1009
|
+
const adapterObj = await this.objects.getObjectAsync(`system.adapter.${adapterName}`);
|
|
1010
|
+
if (adapterObj && adapterObj.common && adapterObj.common.version) {
|
|
1011
|
+
let installSource;
|
|
1012
|
+
// @ts-expect-error https://github.com/ioBroker/adapter-core/issues/455
|
|
1013
|
+
if (adapterObj.common.installedFrom) {
|
|
1014
|
+
// @ts-expect-error https://github.com/ioBroker/adapter-core/issues/455
|
|
1015
|
+
installSource = adapterObj.common.installedFrom;
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
installSource = `${js_controller_common_1.tools.appName.toLowerCase()}.${adapterName}@${adapterObj.common.version}`;
|
|
1019
|
+
}
|
|
1020
|
+
console.log(`Reinstalling adapter "${adapterName}" from "${installSource}"`);
|
|
1021
|
+
await js_controller_common_1.tools.installNodeModule(installSource);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
catch (e) {
|
|
1025
|
+
console.error(`Could not ensure existence of adapter "${adapterName}": ${e.message}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
exports.BackupRestore = BackupRestore;
|
|
1031
|
+
//# sourceMappingURL=setupBackup.js.map
|