@iobroker/js-controller-cli 7.0.0 → 7.0.2-alpha.0-20241026-8055a2557
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/cjs/lib/setup/setupBackup.d.ts +11 -3
- package/build/cjs/lib/setup/setupBackup.js +85 -63
- package/build/cjs/lib/setup/setupBackup.js.map +3 -3
- package/build/cjs/lib/setup.js +3 -3
- package/build/cjs/lib/setup.js.map +2 -2
- package/build/esm/lib/setup/setupBackup.d.ts +11 -3
- package/build/esm/lib/setup/setupBackup.d.ts.map +1 -1
- package/build/esm/lib/setup/setupBackup.js +101 -67
- package/build/esm/lib/setup/setupBackup.js.map +1 -1
- package/build/esm/lib/setup.js +3 -3
- package/build/esm/lib/setup.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -47,6 +47,8 @@ export declare class BackupRestore {
|
|
|
47
47
|
private readonly HOSTNAME_PLACEHOLDER_REPLACE;
|
|
48
48
|
/** Regex to replace all occurrences of the HOSTNAME_PLACEHOLDER */
|
|
49
49
|
private readonly HOSTNAME_PLACEHOLDER_REGEX;
|
|
50
|
+
/** Postfix for backup name */
|
|
51
|
+
private readonly BACKUP_POSTFIX;
|
|
50
52
|
constructor(options: CLIBackupRestoreOptions);
|
|
51
53
|
private _copyFile;
|
|
52
54
|
copyDir(id: string, srcPath: string, destPath: string): Promise<void>;
|
|
@@ -138,9 +140,11 @@ export declare class BackupRestore {
|
|
|
138
140
|
*/
|
|
139
141
|
listBackups(): string[];
|
|
140
142
|
/**
|
|
141
|
-
* Validates
|
|
143
|
+
* Validates a JSONL-style backup and all json files inside the backup (in temporary directory)
|
|
144
|
+
*
|
|
145
|
+
* @param noConfig if the backup does not contain a `config.json` (used by setup custom migration)
|
|
142
146
|
*/
|
|
143
|
-
private
|
|
147
|
+
private _validateTempDirectory;
|
|
144
148
|
/**
|
|
145
149
|
* Validate that the created JSONL files in the temporary directories are parseable
|
|
146
150
|
*/
|
|
@@ -150,7 +154,11 @@ export declare class BackupRestore {
|
|
|
150
154
|
*
|
|
151
155
|
* @param _name - index or name of the backup
|
|
152
156
|
*/
|
|
153
|
-
validateBackup(_name: string | number): Promise<void
|
|
157
|
+
validateBackup(_name: string | number): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Validate an unpacked legacy backup in the temporary directory
|
|
160
|
+
*/
|
|
161
|
+
private _validateLegacyTempDir;
|
|
154
162
|
/**
|
|
155
163
|
* Checks a directory for json files and validates them, steps down recursive in subdirectories
|
|
156
164
|
*
|
|
@@ -55,6 +55,7 @@ class BackupRestore {
|
|
|
55
55
|
HOSTNAME_PLACEHOLDER = "$$__hostname__$$";
|
|
56
56
|
HOSTNAME_PLACEHOLDER_REPLACE = "$$$$__hostname__$$$$";
|
|
57
57
|
HOSTNAME_PLACEHOLDER_REGEX = /\$\$__hostname__\$\$/g;
|
|
58
|
+
BACKUP_POSTFIX = `_backup${import_js_controller_common.tools.appNameLowerCase}`;
|
|
58
59
|
constructor(options) {
|
|
59
60
|
options = options || {};
|
|
60
61
|
if (!options.states) {
|
|
@@ -179,10 +180,12 @@ class BackupRestore {
|
|
|
179
180
|
}
|
|
180
181
|
});
|
|
181
182
|
}
|
|
182
|
-
async createBackup(name, noConfig) {
|
|
183
|
+
async createBackup(name, noConfig = false) {
|
|
183
184
|
if (!name) {
|
|
184
185
|
const d = new Date();
|
|
185
|
-
name = `${d.getFullYear()}_${`0${d.getMonth() + 1}`.slice(-2)}_${`0${d.getDate()}`.slice(-2)}-${`0${d.getHours()}`.slice(-2)}_${`0${d.getMinutes()}`.slice(-2)}_${`0${d.getSeconds()}`.slice(-2)}
|
|
186
|
+
name = `${d.getFullYear()}_${`0${d.getMonth() + 1}`.slice(-2)}_${`0${d.getDate()}`.slice(-2)}-${`0${d.getHours()}`.slice(-2)}_${`0${d.getMinutes()}`.slice(-2)}_${`0${d.getSeconds()}`.slice(-2)}${this.BACKUP_POSTFIX}`;
|
|
187
|
+
} else if (!name.endsWith(this.BACKUP_POSTFIX) && !name.endsWith(`${this.BACKUP_POSTFIX}.tar.gz`)) {
|
|
188
|
+
name += this.BACKUP_POSTFIX;
|
|
186
189
|
}
|
|
187
190
|
name = name.toString().replace(/\\/g, "/");
|
|
188
191
|
if (!name.includes("/")) {
|
|
@@ -260,7 +263,7 @@ class BackupRestore {
|
|
|
260
263
|
}
|
|
261
264
|
console.log(`host.${hostname} Validating backup ...`);
|
|
262
265
|
try {
|
|
263
|
-
await this.
|
|
266
|
+
await this._validateTempDirectory(noConfig);
|
|
264
267
|
console.log(`host.${hostname} The backup is valid!`);
|
|
265
268
|
return await this._packBackup(name);
|
|
266
269
|
} catch (e) {
|
|
@@ -468,8 +471,12 @@ class BackupRestore {
|
|
|
468
471
|
const { force, dontDeleteAdapters } = options;
|
|
469
472
|
const hostname = import_js_controller_common.tools.getHostName();
|
|
470
473
|
const backupBaseDir = import_node_path.default.join(this.tmpDir, "backup");
|
|
471
|
-
|
|
472
|
-
|
|
474
|
+
let backupHostName = hostname;
|
|
475
|
+
let config;
|
|
476
|
+
if (await import_fs_extra.default.pathExists(import_node_path.default.join(backupBaseDir, "config.json"))) {
|
|
477
|
+
config = await import_fs_extra.default.readJSON(import_node_path.default.join(backupBaseDir, "config.json"));
|
|
478
|
+
backupHostName = config.system?.hostname || hostname;
|
|
479
|
+
}
|
|
473
480
|
const objFd = await (0, import_promises.open)(import_node_path.default.join(backupBaseDir, "objects.jsonl"));
|
|
474
481
|
const rlObj = objFd.readLines();
|
|
475
482
|
const hostObjArr = [];
|
|
@@ -489,8 +496,10 @@ class BackupRestore {
|
|
|
489
496
|
if (!dontDeleteAdapters) {
|
|
490
497
|
await this._removeAllAdapters();
|
|
491
498
|
}
|
|
492
|
-
|
|
493
|
-
|
|
499
|
+
if (config) {
|
|
500
|
+
import_fs_extra.default.writeFileSync(import_js_controller_common.tools.getConfigFileName(), JSON.stringify(config, null, 2));
|
|
501
|
+
await this.connectToNewDatabase(config);
|
|
502
|
+
}
|
|
494
503
|
console.log(`host.${hostname} Clear all objects and states...`);
|
|
495
504
|
await this.cleanDatabase(false);
|
|
496
505
|
console.log(`host.${hostname} done.`);
|
|
@@ -521,7 +530,7 @@ class BackupRestore {
|
|
|
521
530
|
async _restoreAfterStop(options) {
|
|
522
531
|
const { force, restartOnFinish, dontDeleteAdapters } = options;
|
|
523
532
|
const backupBaseDir = import_node_path.default.join(this.tmpDir, "backup");
|
|
524
|
-
const isJsonl = await import_fs_extra.default.pathExists(import_node_path.default.join(backupBaseDir, "
|
|
533
|
+
const isJsonl = await import_fs_extra.default.pathExists(import_node_path.default.join(backupBaseDir, "objects.jsonl"));
|
|
525
534
|
if (isJsonl) {
|
|
526
535
|
const exitCode = await this._restoreJsonlBackup(options);
|
|
527
536
|
if (exitCode !== import_js_controller_common.EXIT_CODES.NO_ERROR) {
|
|
@@ -655,16 +664,22 @@ class BackupRestore {
|
|
|
655
664
|
}
|
|
656
665
|
return result;
|
|
657
666
|
}
|
|
658
|
-
async
|
|
667
|
+
async _validateTempDirectory(noConfig = false) {
|
|
659
668
|
const backupBaseDir = import_node_path.default.join(this.tmpDir, "backup");
|
|
660
|
-
|
|
669
|
+
if (!noConfig) {
|
|
670
|
+
await import_fs_extra.default.readJSON(import_node_path.default.join(backupBaseDir, "config.json"));
|
|
671
|
+
console.log(`host.${this.hostname} "config.json" is valid`);
|
|
672
|
+
}
|
|
661
673
|
if (!await import_fs_extra.default.pathExists(import_node_path.default.join(backupBaseDir, "objects.jsonl"))) {
|
|
662
674
|
throw new Error("Backup does not contain valid objects");
|
|
663
675
|
}
|
|
676
|
+
console.log(`host.${this.hostname} "objects.jsonl" exists`);
|
|
664
677
|
if (!await import_fs_extra.default.pathExists(import_node_path.default.join(backupBaseDir, "states.jsonl"))) {
|
|
665
678
|
throw new Error("Backup does not contain valid states");
|
|
666
679
|
}
|
|
680
|
+
console.log(`host.${this.hostname} "states.jsonl" exists`);
|
|
667
681
|
await this._validateDatabaseFiles();
|
|
682
|
+
console.log(`host.${this.hostname} JSONL lines are valid`);
|
|
668
683
|
try {
|
|
669
684
|
this._checkDirectory(import_node_path.default.join(backupBaseDir, "files"));
|
|
670
685
|
} catch (e) {
|
|
@@ -695,7 +710,7 @@ class BackupRestore {
|
|
|
695
710
|
}
|
|
696
711
|
await statesFd.close();
|
|
697
712
|
}
|
|
698
|
-
validateBackup(_name) {
|
|
713
|
+
async validateBackup(_name) {
|
|
699
714
|
let backups;
|
|
700
715
|
let name = typeof _name === "number" ? _name.toString() : _name;
|
|
701
716
|
if (!name) {
|
|
@@ -704,12 +719,12 @@ class BackupRestore {
|
|
|
704
719
|
if (backups.length) {
|
|
705
720
|
console.log("Please specify one of the backup names:");
|
|
706
721
|
for (const t in backups) {
|
|
707
|
-
console.log(`${backups[t]} or ${backups[t].replace(
|
|
722
|
+
console.log(`${backups[t]} or ${backups[t].replace(`${this.BACKUP_POSTFIX}.tar.gz`, "")} or ${t}`);
|
|
708
723
|
}
|
|
709
724
|
} else {
|
|
710
725
|
console.warn(`No backups found. Create a backup, using "${import_js_controller_common.tools.appName} backup" first`);
|
|
711
726
|
}
|
|
712
|
-
|
|
727
|
+
throw new import_customError.IoBrokerError({ message: "Backup not found", code: import_js_controller_common.EXIT_CODES.INVALID_ARGUMENTS });
|
|
713
728
|
}
|
|
714
729
|
if (parseInt(name, 10).toString() === name.toString()) {
|
|
715
730
|
backups = this.listBackups();
|
|
@@ -720,21 +735,21 @@ class BackupRestore {
|
|
|
720
735
|
if (backups.length) {
|
|
721
736
|
console.log("Please specify one of the backup names:");
|
|
722
737
|
for (const t in backups) {
|
|
723
|
-
console.log(`${backups[t]} or ${backups[t].replace(
|
|
738
|
+
console.log(`${backups[t]} or ${backups[t].replace(`${this.BACKUP_POSTFIX}.tar.gz`, "")} or ${t}`);
|
|
724
739
|
}
|
|
725
740
|
} else {
|
|
726
741
|
console.log(`No existing backups. Create a backup, using "${import_js_controller_common.tools.appName} backup" first`);
|
|
727
742
|
}
|
|
728
|
-
|
|
743
|
+
throw new import_customError.IoBrokerError({ message: "Backup not found", code: import_js_controller_common.EXIT_CODES.INVALID_ARGUMENTS });
|
|
729
744
|
}
|
|
730
745
|
console.log(`host.${this.hostname} Using backup file ${name}`);
|
|
731
746
|
}
|
|
732
747
|
name = name.toString().replace(/\\/g, "/");
|
|
733
748
|
if (!name.includes("/")) {
|
|
734
749
|
name = BackupRestore.getBackupDir() + name;
|
|
735
|
-
const regEx = new RegExp(
|
|
750
|
+
const regEx = new RegExp(this.BACKUP_POSTFIX, "i");
|
|
736
751
|
if (!regEx.test(name)) {
|
|
737
|
-
name +=
|
|
752
|
+
name += this.BACKUP_POSTFIX;
|
|
738
753
|
}
|
|
739
754
|
if (!name.match(/\.tar\.gz$/i)) {
|
|
740
755
|
name += ".tar.gz";
|
|
@@ -742,53 +757,60 @@ class BackupRestore {
|
|
|
742
757
|
}
|
|
743
758
|
if (!import_fs_extra.default.existsSync(name)) {
|
|
744
759
|
console.error(`host.${this.hostname} Cannot find ${name}`);
|
|
745
|
-
|
|
760
|
+
throw new import_customError.IoBrokerError({ message: "Backup not found", code: import_js_controller_common.EXIT_CODES.INVALID_ARGUMENTS });
|
|
746
761
|
}
|
|
747
762
|
if (import_fs_extra.default.existsSync(`${this.tmpDir}/backup/backup.json`)) {
|
|
748
763
|
import_fs_extra.default.unlinkSync(`${this.tmpDir}/backup/backup.json`);
|
|
749
764
|
}
|
|
750
|
-
|
|
751
|
-
import_tar.default.extract({
|
|
765
|
+
try {
|
|
766
|
+
await import_tar.default.extract({
|
|
752
767
|
file: name,
|
|
753
768
|
cwd: this.tmpDir
|
|
754
|
-
}, void 0, (err) => {
|
|
755
|
-
if (err) {
|
|
756
|
-
console.error(`host.${this.hostname} Cannot extract from file "${name}": ${err.message}`);
|
|
757
|
-
return void this.processExit(import_js_controller_common.EXIT_CODES.INVALID_ARGUMENTS);
|
|
758
|
-
}
|
|
759
|
-
if (!import_fs_extra.default.existsSync(`${this.tmpDir}/backup/backup.json`)) {
|
|
760
|
-
console.error(`host.${this.hostname} Validation failed. Cannot find extracted file from file "${this.tmpDir}/backup/backup.json"`);
|
|
761
|
-
return void this.processExit(import_js_controller_common.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP);
|
|
762
|
-
}
|
|
763
|
-
console.log(`host.${this.hostname} Starting validation ...`);
|
|
764
|
-
let backupJSON;
|
|
765
|
-
try {
|
|
766
|
-
backupJSON = import_fs_extra.default.readJSONSync(`${this.tmpDir}/backup/backup.json`);
|
|
767
|
-
} catch (err2) {
|
|
768
|
-
console.error(`host.${this.hostname} Backup corrupted. Backup ${name} does not contain a valid backup.json file: ${err2.message}`);
|
|
769
|
-
this.removeTempBackupDir();
|
|
770
|
-
return void this.processExit(import_js_controller_common.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP);
|
|
771
|
-
}
|
|
772
|
-
if (!backupJSON || !backupJSON.objects || !backupJSON.objects.length) {
|
|
773
|
-
console.error(`host.${this.hostname} Backup corrupted. Backup does not contain valid objects`);
|
|
774
|
-
try {
|
|
775
|
-
this.removeTempBackupDir();
|
|
776
|
-
} catch (e) {
|
|
777
|
-
console.error(`host.${this.hostname} Cannot clear temporary backup directory: ${e.message}`);
|
|
778
|
-
}
|
|
779
|
-
return void this.processExit(import_js_controller_common.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP);
|
|
780
|
-
}
|
|
781
|
-
console.log(`host.${this.hostname} backup.json OK`);
|
|
782
|
-
try {
|
|
783
|
-
this._checkDirectory(`${this.tmpDir}/backup/files`, true);
|
|
784
|
-
this.removeTempBackupDir();
|
|
785
|
-
resolve();
|
|
786
|
-
} catch (err2) {
|
|
787
|
-
console.error(`host.${this.hostname} Backup corrupted: ${err2.message}`);
|
|
788
|
-
return void this.processExit(import_js_controller_common.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP);
|
|
789
|
-
}
|
|
790
769
|
});
|
|
791
|
-
})
|
|
770
|
+
} catch (e) {
|
|
771
|
+
const errMessage = `Cannot extract from file "${name}": ${e.message}`;
|
|
772
|
+
console.error(`host.${this.hostname} ${errMessage}`);
|
|
773
|
+
throw new import_customError.IoBrokerError({ message: "Backup not found", code: import_js_controller_common.EXIT_CODES.INVALID_ARGUMENTS });
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
if (import_fs_extra.default.existsSync(import_node_path.default.join(this.tmpDir, "backup", "backup.json"))) {
|
|
777
|
+
this._validateLegacyTempDir();
|
|
778
|
+
} else {
|
|
779
|
+
await this._validateTempDirectory();
|
|
780
|
+
}
|
|
781
|
+
} catch (e) {
|
|
782
|
+
console.error(`host.${this.hostname} ${e.message}`);
|
|
783
|
+
try {
|
|
784
|
+
this.removeTempBackupDir();
|
|
785
|
+
} catch (e2) {
|
|
786
|
+
console.error(`host.${this.hostname} Cannot clear temporary backup directory: ${e2.message}`);
|
|
787
|
+
}
|
|
788
|
+
throw new import_customError.IoBrokerError({ message: e.message, code: import_js_controller_common.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP });
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
this.removeTempBackupDir();
|
|
792
|
+
} catch (e) {
|
|
793
|
+
console.error(`host.${this.hostname} Cannot clear temporary backup directory: ${e.message}`);
|
|
794
|
+
throw new import_customError.IoBrokerError({ message: e.message, code: import_js_controller_common.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
_validateLegacyTempDir() {
|
|
798
|
+
console.log(`host.${this.hostname} Starting validation ...`);
|
|
799
|
+
let backupJSON;
|
|
800
|
+
try {
|
|
801
|
+
backupJSON = import_fs_extra.default.readJSONSync(`${this.tmpDir}/backup/backup.json`);
|
|
802
|
+
} catch (e) {
|
|
803
|
+
throw new Error(`Backup corrupted. Backup does not contain a valid backup.json file: ${e.message}`);
|
|
804
|
+
}
|
|
805
|
+
if (!backupJSON || !backupJSON.objects || !backupJSON.objects.length) {
|
|
806
|
+
throw new Error(`host.${this.hostname} Backup corrupted. Backup does not contain valid objects`);
|
|
807
|
+
}
|
|
808
|
+
console.log(`host.${this.hostname} backup.json OK`);
|
|
809
|
+
try {
|
|
810
|
+
this._checkDirectory(`${this.tmpDir}/backup/files`, true);
|
|
811
|
+
} catch (e) {
|
|
812
|
+
throw new Error(`Backup corrupted: ${e.message}`);
|
|
813
|
+
}
|
|
792
814
|
}
|
|
793
815
|
_checkDirectory(path2, verbose = false) {
|
|
794
816
|
if (import_fs_extra.default.existsSync(path2)) {
|
|
@@ -822,7 +844,7 @@ class BackupRestore {
|
|
|
822
844
|
backups = this.listBackups();
|
|
823
845
|
backups.sort((a, b) => b > a ? 1 : b === a ? 0 : -1);
|
|
824
846
|
if (backups.length) {
|
|
825
|
-
backups.forEach((backup, i) => console.log(`${backup} or ${backup.replace(
|
|
847
|
+
backups.forEach((backup, i) => console.log(`${backup} or ${backup.replace(`${this.BACKUP_POSTFIX}.tar.gz`, "")} or ${i}`));
|
|
826
848
|
} else {
|
|
827
849
|
console.warn("No backups found");
|
|
828
850
|
}
|
|
@@ -842,7 +864,7 @@ class BackupRestore {
|
|
|
842
864
|
console.log("No matching backup found");
|
|
843
865
|
if (backups.length) {
|
|
844
866
|
console.log("Please specify one of the backup names:");
|
|
845
|
-
backups.forEach((backup, i) => console.log(`${backup} or ${backup.replace(
|
|
867
|
+
backups.forEach((backup, i) => console.log(`${backup} or ${backup.replace(`${this.BACKUP_POSTFIX}.tar.gz`, "")} or ${i}`));
|
|
846
868
|
}
|
|
847
869
|
} else {
|
|
848
870
|
console.log(`host.${this.hostname} Using backup file ${name}`);
|
|
@@ -851,9 +873,9 @@ class BackupRestore {
|
|
|
851
873
|
name = name.toString().replace(/\\/g, "/");
|
|
852
874
|
if (!name.includes("/")) {
|
|
853
875
|
name = BackupRestore.getBackupDir() + name;
|
|
854
|
-
const regEx = new RegExp(
|
|
876
|
+
const regEx = new RegExp(this.BACKUP_POSTFIX, "i");
|
|
855
877
|
if (!regEx.test(name)) {
|
|
856
|
-
name +=
|
|
878
|
+
name += this.BACKUP_POSTFIX;
|
|
857
879
|
}
|
|
858
880
|
if (!name.match(/\.tar\.gz$/i)) {
|
|
859
881
|
name += ".tar.gz";
|
|
@@ -876,8 +898,8 @@ class BackupRestore {
|
|
|
876
898
|
console.error(`host.${this.hostname} Cannot extract from file "${name}": ${e.message}`);
|
|
877
899
|
return { exitCode: import_js_controller_common.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP, objects: this.objects, states: this.states };
|
|
878
900
|
}
|
|
879
|
-
if (!await import_fs_extra.default.pathExists(import_node_path.default.join(backupBasePath, "backup.json")) && !await import_fs_extra.default.pathExists(import_node_path.default.join(backupBasePath, "
|
|
880
|
-
console.error(`host.${this.hostname} Cannot find extracted file "${import_node_path.default.join(backupBasePath, "backup.json")}" or "${import_node_path.default.join(backupBasePath, "
|
|
901
|
+
if (!await import_fs_extra.default.pathExists(import_node_path.default.join(backupBasePath, "backup.json")) && !await import_fs_extra.default.pathExists(import_node_path.default.join(backupBasePath, "objects.jsonl"))) {
|
|
902
|
+
console.error(`host.${this.hostname} Cannot find extracted file "${import_node_path.default.join(backupBasePath, "backup.json")}" or "${import_node_path.default.join(backupBasePath, "objects.jsonl")}"`);
|
|
881
903
|
return { exitCode: import_js_controller_common.EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP, objects: this.objects, states: this.states };
|
|
882
904
|
}
|
|
883
905
|
await import_cliProcess.CLIProcess.stopJSController();
|