iobroker.js-controller
Version:
Updated by reinstall.js on 2018-06-11T15:19:56.688Z
1,166 lines (1,058 loc) • 46 kB
JavaScript
/**
* Backup
*
* Copyright 2013-2022 bluefox <dogafox@gmail.com>
*
* MIT License
*
*/
;
const fs = require('fs-extra');
const { tools } = require('@iobroker/js-controller-common');
const pathLib = require('path');
const hostname = tools.getHostName();
const Upload = require('./setupUpload');
const { EXIT_CODES } = require('@iobroker/js-controller-common');
const cpPromise = require('promisify-child-process');
// We cannot use relative paths for the backup locations, as they used by both
// require, which resolves relative paths from __dirname
// and the fs methods, which resolve relative paths from process.cwd()
const tmpDir = pathLib.normalize(pathLib.join(__dirname, '../../tmp'));
const bkpDir = pathLib.normalize(pathLib.join(__dirname, '../../backups'));
class BackupRestore {
constructor(options) {
options = options || {};
if (!options.states) {
throw new Error('Invalid arguments: states is missing');
}
if (!options.objects) {
throw new Error('Invalid arguments: objects is missing');
}
if (!options.processExit) {
throw new Error('Invalid arguments: processExit is missing');
}
if (!options.cleanDatabase) {
throw new Error('Invalid arguments: cleanDatabase is missing');
}
if (!options.restartController) {
throw new Error('Invalid arguments: restartController is missing');
}
this.objects = options.objects;
this.states = options.states;
this.processExit = options.processExit;
this.cleanDatabase = options.cleanDatabase;
this.restartController = options.restartController;
this.dbMigration = options.dbMigration || false;
this.mime = null;
/** these adapters will be reinstalled during restore, while others will be installed after next controller start */
this.PRESERVE_ADAPTERS = ['admin', 'backitup'];
this.upload = new Upload(options);
this.configParts = tools.getConfigFileName().split('/');
this.configParts.pop(); // remove *.json
this.configDir = this.configParts.join('/'); // => name-data
} // endConstructor
// --------------------------------------- BACKUP ---------------------------------------------------
async _copyFile(id, srcPath, destPath) {
try {
const data = await this.objects.readFileAsync(id, srcPath, '');
if (data) {
if (data.data !== undefined) {
fs.writeFileSync(destPath, data.data);
} else {
fs.writeFileSync(destPath, data);
}
}
} catch (err) {
console.log(`Can not copy File ${id}${srcPath} to ${destPath}: ${err.message}`);
}
}
async copyDir(id, srcPath, destPath) {
!fs.existsSync(destPath) && fs.mkdirSync(destPath);
try {
const res = await this.objects.readDirAsync(id, srcPath);
if (res) {
for (let t = 0; t < res.length; t++) {
if (res[t].isDir) {
await this.copyDir(id, `${srcPath}/${res[t].file}`, `${destPath}/${res[t].file}`);
} else {
!fs.existsSync(destPath) && fs.mkdirSync(destPath);
await this._copyFile(id, `${srcPath}/${res[t].file}`, `${destPath}/${res[t].file}`);
}
}
}
} catch (err) {
if (!err.message.includes('Not exists')) {
console.warn(`Directory ${id}/${srcPath} cannot be copied: ` + err);
}
}
}
getBackupDir() {
let dataDir = tools.getDefaultDataDir();
// All paths are returned always relative to /node_modules/appName.js-controller
if (dataDir) {
if (dataDir[0] === '.' && dataDir[1] === '.') {
dataDir = `${__dirname}/../../${dataDir}`;
} else if (dataDir[0] === '.' && dataDir[1] === '/') {
dataDir = `${__dirname}/../../${dataDir.substring(2)}`;
}
}
dataDir = dataDir.replace(/\\/g, '/');
if (dataDir[dataDir.length - 1] !== '/') {
dataDir += '/';
}
const parts = dataDir.split('/');
parts.pop(); // remove data or appName-data
parts.pop();
return pathLib.normalize(`${parts.join('/')}/backups/`);
}
copyFileSync(source, target) {
let targetFile = target;
try {
// if target is a directory a new file with the same name will be created
if (fs.existsSync(target)) {
if (fs.statSync(target).isDirectory()) {
targetFile = pathLib.join(target, pathLib.basename(source));
}
}
fs.writeFileSync(targetFile, fs.readFileSync(source));
} catch (e) {
console.error(`Could not copy ${targetFile} to ${source}: ${e.message}`);
}
}
copyFolderRecursiveSync(source, target) {
let files = [];
if (!fs.existsSync(target)) {
fs.mkdirSync(target);
}
// check if folder needs to be created or integrated
const targetFolder = pathLib.join(target, pathLib.basename(source));
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder);
}
// copy
if (fs.existsSync(source) && fs.statSync(source).isDirectory()) {
files = fs.readdirSync(source);
files.forEach(file => {
const curSource = pathLib.join(source, file);
if (!fs.existsSync(curSource)) {
return;
}
if (fs.statSync(curSource).isDirectory()) {
this.copyFolderRecursiveSync(curSource, targetFolder);
} else {
this.copyFileSync(curSource, targetFolder);
}
});
}
}
/**
* Pack and compress the backup
*
* @param {string} name - backup name
* @return {Promise<string>}
* @private
*/
_packBackup(name) {
// 2021_10_25 BF (TODO): store letsencrypt files too
const letsEncrypt = `${this.configDir}/letsencrypt`;
if (fs.existsSync(letsEncrypt)) {
this.copyFolderRecursiveSync(letsEncrypt, `${tmpDir}/backup`);
}
const tar = require('tar');
return new Promise(resolve => {
const f = fs.createWriteStream(name);
f.on('finish', () => {
tools.rmdirRecursiveSync(`${tmpDir}/backup`);
resolve(pathLib.normalize(name));
});
f.on('error', err => {
console.error(`host.${hostname} Cannot pack directory ${tmpDir}/backup: ${err.message}`);
this.processExit(EXIT_CODES.CANNOT_GZIP_DIRECTORY);
});
try {
tar.create({ gzip: true, cwd: `${tmpDir}/` }, ['backup']).pipe(f);
} catch (err) {
console.error(`host.${hostname} Cannot pack directory ${tmpDir}/backup: ${err.message}`);
return void this.processExit(EXIT_CODES.CANNOT_GZIP_DIRECTORY);
}
});
}
/**
* Creates backup and stores with given name
* @param {string} name - name of the backup
* @param {boolean?} noConfig - do not store configs
* @return {Promise<string>}
*/
async createBackup(name, noConfig) {
if (!name) {
const d = new Date();
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) +
'_backup' +
tools.appName;
}
name = name.toString().replace(/\\/g, '/');
if (!name.includes('/')) {
const path = this.getBackupDir();
// create directory if not exists
if (!fs.existsSync(path)) {
fs.mkdirSync(path);
}
if (!name.includes('.tar.gz')) {
name = `${path + name}.tar.gz`;
} else {
name = path + name;
}
}
let result = { objects: null, states: {} };
const hostname = tools.getHostName();
try {
const res = await this.objects.getObjectListAsync({ include_docs: true });
result.objects = res.rows;
} catch (e) {
console.error(`host.${hostname} Cannot get objects: ${e.message}`);
}
if (!noConfig) {
result.config = null;
}
if (!noConfig && fs.existsSync(tools.getConfigFileName())) {
result.config = fs.readJSONSync(tools.getConfigFileName());
}
const r = new RegExp(`^system\\.host\\.${hostname}\\.(\\w+)$`);
try {
const keys = await this.states.getKeys('*');
/*for (const i = keys.length - 1; i >= 0; i--) {
if (keys[i].startsWith('messagebox.') || keys[i].startsWith('log.')) {
keys.splice(i, 1);
}
}*/
// NOTE for all "replace" with $$$$ ... result will be just $$
const obj = await this.states.getStates(keys);
// read iobroker.json
let isCustomHostname;
try {
const config = await fs.readJSON(tools.getConfigFileName());
// if a hostname is configured
isCustomHostname = !!config.system.hostname;
} catch (e) {
console.error(`host.${hostname} Cannot read config file: ${e.message}`);
}
for (let i = 0; i < keys.length; i++) {
if (!obj[i]) {
continue;
}
if (!isCustomHostname) {
// if its a default hostname, we will have a new default after restore and need to replace
if (obj[i].from === `system.host.${hostname}` || r.test(obj[i].from)) {
obj[i].from.replace(`system.host.${hostname}`, 'system.host.$$$$__hostname__$$$$');
}
if (r.test(keys[i])) {
keys[i] = keys[i].replace(hostname, '$$$$__hostname__$$$$');
}
}
result.states[keys[i]] = obj[i];
}
console.log(`host.${hostname} ${keys.length} states saved`);
} catch (e) {
console.error(`host.${hostname} Cannot get states: ${e.message}`);
}
if (!fs.existsSync(bkpDir)) {
fs.mkdirSync(bkpDir);
}
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir);
}
try {
tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
} catch (e) {
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
}
if (!fs.existsSync(`${tmpDir}/backup`)) {
fs.mkdirSync(`${tmpDir}/backup`);
}
if (!fs.existsSync(`${tmpDir}/backup/files`)) {
fs.mkdirSync(`${tmpDir}/backup/files`);
}
// try to find user files
for (let j = 0; j < result.objects.length; j++) {
if (
!result.objects[j] ||
!result.objects[j].value ||
!result.objects[j].value._id ||
!result.objects[j].value.common
) {
continue;
}
//if (result.objects[j].doc) delete result.objects[j].doc;
if (
result.objects[j].value._id.match(/^system\.adapter\.([\w\d_-]+).(\d+)$/) &&
result.objects[j].value.common.host === hostname
) {
result.objects[j].value.common.host = '$$__hostname__$$';
if (result.objects[j].doc) {
result.objects[j].doc.common.host = '$$__hostname__$$';
}
} else if (r.test(result.objects[j].value._id)) {
result.objects[j].value._id = result.objects[j].value._id.replace(hostname, '$$$$__hostname__$$$$');
result.objects[j].id = result.objects[j].value._id;
if (result.objects[j].doc) {
result.objects[j].doc._id = result.objects[j].value._id;
}
} else if (result.objects[j].value._id === 'system.host.' + hostname) {
result.objects[j].value._id = 'system.host.$$__hostname__$$';
result.objects[j].value.common.name = result.objects[j].value._id;
result.objects[j].value.common.hostname = '$$__hostname__$$';
if (result.objects[j].value.native && result.objects[j].value.native.os) {
result.objects[j].value.native.os.hostname = '$$__hostname__$$';
}
result.objects[j].id = result.objects[j].value._id;
if (result.objects[j].doc) {
result.objects[j].doc._id = result.objects[j].value._id;
result.objects[j].doc.common.name = result.objects[j].value._id;
result.objects[j].doc.common.hostname = '$$__hostname__$$';
if (result.objects[j].doc.native && result.objects[j].value.native.os) {
result.objects[j].doc.native.os.hostname = '$$__hostname__$$';
}
}
}
// Read all files
if (
result.objects[j].value.type === 'meta' &&
result.objects[j].value.common &&
result.objects[j].value.common.type === 'meta.user'
) {
// do not process "xxx.0. " and "xxx.0."
if (
result.objects[j].id.trim() === result.objects[j].id &&
result.objects[j].id[result.objects[j].id.length - 1] !== '.'
) {
await this.copyDir(result.objects[j].id, '', `${tmpDir}/backup/files/${result.objects[j].id}`);
}
}
// Read all files
if (
result.objects[j].value.type === 'instance' &&
result.objects[j].value.common &&
result.objects[j].value.common.dataFolder
) {
let path = result.objects[j].value.common.dataFolder;
if (path[0] !== '/' && !path.match(/^\w:/)) {
path = pathLib.join(this.configDir, path);
}
if (fs.existsSync(path)) {
this.copyFolderRecursiveSync(path, `${tmpDir}/backup`);
}
}
}
// special case: copy vis vis-common-user.css file
try {
const data = await this.objects.readFileAsync('vis', 'css/vis-common-user.css');
if (data) {
const dir = `${tmpDir}/backup/files/`;
!fs.existsSync(`${dir}vis`) && fs.mkdirSync(`${dir}vis`);
!fs.existsSync(`${dir}vis/css`) && fs.mkdirSync(`${dir}vis/css`);
fs.writeFileSync(`${dir}vis/css/vis-common-user.css`, data.data !== undefined ? data.data : data);
}
} catch {
// do not process 'css/vis-common-user.css'
}
console.log(`host.${hostname} ${result.objects.length} objects saved`);
try {
fs.writeFileSync(`${tmpDir}/backup/backup.json`, JSON.stringify(result, null, 2));
result = null; // ... to allow GC to clean it up because no longer needed
this._validateBackupAfterCreation();
return await this._packBackup(name);
} catch (err) {
console.error(`host.${hostname} Backup not created: ${err.message}`);
try {
tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
} catch (e) {
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
}
return void this.processExit(EXIT_CODES.CANNOT_EXTRACT_FROM_ZIP);
}
}
//--------------------------------------- RESTORE ---------------------------------------------------
/**
* Helper to restore raw states
*
* @param {string[]} statesList - list of state ids
* @param {object[]} stateObjects - list of state objects
* @return {Promise<void>}
* @private
*/
async _setStateHelper(statesList, stateObjects) {
for (let i = 0; i < statesList.length; i++) {
try {
await this.states.setRawState(statesList[i], stateObjects[statesList[i]]);
} catch (err) {
console.log(`host.${hostname} Could not set value for state ${statesList[i]}: ${err.message}`);
}
if (i % 200 === 0) {
console.log(`host.${hostname} Processed ${i}/${statesList.length} states`);
}
}
}
/**
* Sets all objects to the db and disables all adapters
*
* @param {object[]} _objects - array of all objects to be set
* @return {Promise<void>}
* @private
*/
async _setObjHelper(_objects) {
for (let i = 0; i < _objects.length; i++) {
// Disable all adapters.
if (
!this.dbMigration &&
_objects[i].id &&
/^system\.adapter\..+\.\d$/.test(_objects[i].id) &&
!_objects[i].id.startsWith('system.adapter.admin.') &&
!_objects[i].id.startsWith('system.adapter.backitup.')
) {
if (_objects[i].doc.common && _objects[i].doc.common.enabled) {
_objects[i].doc.common.enabled = false;
}
}
if (_objects[i].doc && _objects[i].doc._rev) {
delete _objects[i].doc._rev;
}
try {
await this.objects.setObjectAsync(_objects[i].id, _objects[i].doc);
} catch (err) {
console.warn(`host.${hostname} Cannot restore ${_objects[i].id}: ${err.message}`);
}
if (i % 200 === 0) {
console.log(`host.${hostname} Processed ${i}/${_objects.length} objects`);
}
}
}
/**
* Creates all provided object if non existing
*
* @param {object[]} objectList - list of objects to be created
* @return {Promise<void>}
*/
async _reloadAdapterObject(objectList) {
if (!Array.isArray(objectList)) {
return;
}
for (const object of objectList) {
let obj;
try {
obj = await this.objects.getObjectAsync(object._id);
} catch {
// ignore
}
if (!obj) {
// object not existing -> create it
try {
await this.objects.setObjectAsync(object._id, object);
console.log(`host.${hostname} object ${object._id} created`);
} catch {
// ignore
}
}
}
}
async _reloadAdaptersObjects() {
const dirs = [];
let _modules;
let p = pathLib.normalize(`${__dirname}/../../node_modules`);
if (fs.existsSync(p)) {
if (!p.includes('js-controller')) {
_modules = fs.readdirSync(p).filter(dir => fs.existsSync(`${p}/${dir}/io-package.json`));
if (_modules) {
const regEx = new RegExp(`^${tools.appName}\\.`, 'i');
for (const module of _modules) {
if (regEx.test(module) && !dirs.includes(module.substring(tools.appName.length + 1))) {
dirs.push(module);
}
}
}
} else {
p = pathLib.normalize(`${__dirname}/../../../node_modules`);
if (fs.existsSync(p)) {
_modules = fs.readdirSync(p).filter(dir => fs.existsSync(`${p}/${dir}/io-package.json`));
if (_modules) {
const regEx = new RegExp(`^${tools.appName}\\.`, 'i');
for (const module of _modules) {
if (regEx.test(module) && !dirs.includes(module.substring(tools.appName.length + 1))) {
dirs.push(module);
}
}
}
}
}
}
// if installed as npm
if (fs.existsSync(`${__dirname}/../../../../node_modules/${tools.appName}.js-controller`)) {
const p = pathLib.normalize(`${__dirname}/../../..`);
_modules = fs.readdirSync(p).filter(dir => fs.existsSync(`${p}/${dir}/io-package.json`));
const regEx_ = new RegExp(`^${tools.appName}\\.`, 'i');
for (const module of _modules) {
// if starting from application name + '.'
if (
regEx_.test(module) &&
// If not js-controller
module.substring(tools.appName.length + 1) !== 'js-controller' &&
!dirs.includes(module.substring(tools.appName.length + 1))
) {
dirs.push(module);
}
}
}
for (const dir of dirs) {
const adapterName = dir.replace(/^iobroker\./i, '');
await this.upload.uploadAdapter(adapterName, false, true);
await this.upload.uploadAdapter(adapterName, true, true);
let pkg = null;
if (!dir) {
console.error('Wrong');
}
const adapterDir = tools.getAdapterDir(adapterName);
if (fs.existsSync(`${adapterDir}/io-package.json`)) {
pkg = fs.readJSONSync(`${adapterDir}/io-package.json`);
}
if (pkg && pkg.objects && pkg.objects.length) {
console.log(`host.${hostname} Setup "${dir}" adapter`);
await this._reloadAdapterObject(pkg.objects);
}
}
}
async _uploadUserFiles(root, path) {
path = path || '';
if (!fs.existsSync(root)) {
return;
}
const files = fs.readdirSync(root + path);
for (const file of files) {
const fName = pathLib.join(root, path, file);
if (!fs.existsSync(fName)) {
continue;
}
const stat = fs.statSync(fName);
if (stat.isDirectory()) {
try {
await this._uploadUserFiles(root, `${path}/${file}`);
} catch (err) {
console.error(`Error: ${err}`);
}
} else {
const parts = path.split('/');
let adapter = parts.splice(0, 2);
adapter = adapter[1];
const _path = `${parts.join('/')}/${file}`;
console.log(`host.${hostname} Upload user file "${adapter}/${_path}`);
try {
await this.objects.writeFileAsync(adapter, _path, fs.readFileSync(`${root + path}/${file}`));
} catch (err) {
console.error(`Error: ${err.message}`);
}
}
}
}
_copyBackupedFiles(backupDir) {
try {
if (!fs.existsSync(backupDir)) {
console.log('No additional files to restore');
return;
}
const dirs = fs.readdirSync(backupDir);
dirs.forEach(dir => {
if (dir === 'files') {
return;
}
const path = pathLib.join(backupDir, dir);
let stat;
try {
if (!fs.existsSync(path)) {
return;
}
stat = fs.statSync(path);
} catch (err) {
console.error(`Ignoring ${path} because can not get file type: ${err.message}`);
return;
}
if (stat.isDirectory()) {
this.copyFolderRecursiveSync(path, this.configDir);
}
});
} catch (err) {
console.error(`Ignoring ${backupDir} because can not read directory: ${err.message}`);
}
}
/**
* Restore after controller has been stopped
*
* @param {boolean} restartOnFinish - restart controller after restore
* @param {boolean} force - skip the controller version check
* @param {boolean} dontDeleteAdapters - skip adapter deletion, e.g. for setup custom db migration
* @returns {Promise<number>}
* @private
*/
async _restoreAfterStop(restartOnFinish, force, dontDeleteAdapters) {
// Open file
let data = fs.readFileSync(`${tmpDir}/backup/backup.json`, 'utf8');
const hostname = tools.getHostName();
// replace all hostnames of instances etc with the new host
data = data.replace(/\$\$__hostname__\$\$/g, hostname);
fs.writeFileSync(`${tmpDir}/backup/backup_.json`, data);
let restore;
try {
restore = JSON.parse(data);
} catch (err) {
console.error(`Cannot parse "${tmpDir}/backup/backup_.json": ${err.message}`);
return EXIT_CODES.CANNOT_RESTORE_BACKUP;
}
const controllerDir = tools.getControllerDir();
// check that the same controller version is installed as it is contained in backup
const exitCode = this._ensureCompatibility(
controllerDir,
restore.config ? restore.config.system.hostname || hostname : hostname,
restore.objects,
force
);
if (exitCode) {
// we had an error
return exitCode;
}
if (!dontDeleteAdapters) {
// prevent having wrong versions of adapters
await this._removeAllAdapters(controllerDir);
}
// stop all adapters
console.log(`host.${hostname} Clear all objects and states...`);
await this.cleanDatabase(false);
console.log(`host.${hostname} done.`);
// upload all data into DB
// restore ioBroker.json
if (restore.config) {
fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(restore.config, null, 2));
}
const sList = Object.keys(restore.states);
await this._setObjHelper(restore.objects);
console.log(`${restore.objects.length} objects restored.`);
await this._setStateHelper(sList, restore.states);
console.log(`${sList.length} states restored.`);
// Required for upload adapter
this.mime = this.mime || require('mime');
// Load user files into DB
await this._uploadUserFiles(`${tmpDir}/backup/files`);
// reload objects of adapters (if some couldn't be removed - normally this shouldn't be necessary anymore)
await this._reloadAdaptersObjects();
// Reload host objects
const packageIO = fs.readJSONSync(`${__dirname}/../../io-package.json`);
await this._reloadAdapterObject(packageIO ? packageIO.objects : null);
// copy all files into iob-data
await this._copyBackupedFiles(pathLib.join(tmpDir, 'backup'));
// reinstall preserve adapters
await this._restorePreservedAdapters();
if (force) {
// js-controller version has changed (setup never called for this version)
console.log('Forced restore - executing setup ...');
try {
await cpPromise.exec(
`"${process.execPath}" "${pathLib.join(controllerDir, `${tools.appName.toLowerCase()}.js`)}" setup`
);
} catch (e) {
console.error(
`Could not execute "setup" command, please ensure "setup" is called before starting ioBroker: ${e.message}`
);
}
}
if (restartOnFinish) {
this.restartController();
}
return EXIT_CODES.NO_ERROR;
}
/**
* Removes all adapters
* @param {string} controllerDir - directory of js-controller
* @return {Promise<void>}
* @private
*/
async _removeAllAdapters(controllerDir) {
const nodeModulePath = pathLib.join(controllerDir, '..');
const nodeModuleDirs = fs.readdirSync(nodeModulePath, { withFileTypes: true });
// we need to uninstall current adapters to get exact the same system as before backup
for (const dir of nodeModuleDirs) {
if (
dir.isDirectory() &&
dir.name.startsWith(`${tools.appName.toLowerCase()}.`) &&
dir.name !== `${tools.appName.toLowerCase()}.js-controller`
) {
try {
const packJson = fs.readJsonSync(pathLib.join(nodeModulePath, dir.name, 'package.json'));
console.log(`Removing current installation of ${packJson.name}`);
await tools.uninstallNodeModule(packJson.name);
} catch {
// ignore
}
}
}
}
/**
* Ensure that installed controller version matches version in backup
*
* @param {string} controllerDir - directory of js-controller
* @param {string} backupHostname - hostname in backup file
* @param {object[]} backupObjects - the objects contained in the backup
* @param {boolean} force - if force is true, only log
* @return {undefined|number}
* @private
*/
_ensureCompatibility(controllerDir, backupHostname, backupObjects, force) {
try {
const ioPackJson = fs.readJsonSync(pathLib.join(controllerDir, 'io-package.json'));
const hostObj = backupObjects.find(obj => obj.id === `system.host.${backupHostname}`);
if (hostObj.value.common.installedVersion !== ioPackJson.common.version) {
if (!force) {
console.warn('The current version of js-controller differs from the version in the backup.');
console.warn('The js-controller version of the backup can not be restored automatically.');
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`
);
console.warn(
'If you really want to restore the backup with the current installed js-controller, execute the restore command with the --force flag'
);
return EXIT_CODES.CANNOT_RESTORE_BACKUP;
} else {
console.info('The current version of js-controller differs from the version in the backup.');
console.info('The js-controller version of the backup can not be restored automatically.');
console.info('Note, that your backup might differ in behavior due to this version change!');
}
}
} catch {
// ignore
}
}
/**
* Returns all backups as array
*
* @return {string[]}
*/
listBackups() {
const dir = this.getBackupDir();
const result = [];
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir);
for (const file of files) {
if (file.match(/\.tar\.gz$/i)) {
result.push(file);
}
}
return result;
} else {
return result;
}
}
/**
* Validates the backup.json and all json files inside the backup after (in temporary directory), here we only abort if backup.json is corrupted
*/
_validateBackupAfterCreation() {
const backupJSON = require(`${tmpDir}/backup/backup.json`);
if (!backupJSON.objects || !backupJSON.objects.length) {
throw new Error('Backup does not contain valid objects');
}
// we check all other json files, we assume them as optional, because user created files may be no valid json
try {
this._checkDirectory(`${tmpDir}/backup/files`);
} catch (err) {
console.warn(`host.${hostname} One or more optional files are corrupted: ${err.message}`);
console.warn(`host.${hostname} Please ensure that self-created JSON files are valid`);
}
} // endValidateBackupAfterCreation
/**
* Validates the given backup.json and all json files in the backup, calls processExit afterwards
* @param {string} name - index or name of the backup
*/
validateBackup(name) {
let backups;
// @ts-ignore
if (!name && name !== 0) {
backups = this.listBackups();
backups.sort((a, b) => (b > a ? 1 : b === a ? 0 : -1));
if (backups.length) {
// List all available backups
console.log('Please specify one of the backup names:');
for (const t in backups) {
console.log(`${backups[t]} or ${backups[t].replace(`_backup${tools.appName}.tar.gz`, '')} or ${t}`);
}
} else {
console.warn(`No backups found. Create a backup, using "${tools.appName} backup" first`);
}
return void this.processExit(10);
}
// If number
if (parseInt(name, 10).toString() === name.toString()) {
backups = this.listBackups();
backups.sort((a, b) => (b > a ? 1 : b === a ? 0 : -1));
name = backups[parseInt(name, 10)];
if (!name) {
console.log('No matching backup found');
if (backups.length) {
console.log('Please specify one of the backup names:');
for (const t of Object.keys(backups)) {
console.log(
`${backups[t]} or ${backups[t].replace(`_backup${tools.appName}.tar.gz`, '')} or ${t}`
);
}
} else {
console.log(`No existing backups. Create a backup, using "${tools.appName} backup" first`);
}
return void this.processExit(10);
} else {
console.log(`host.${hostname} Using backup file ${name}`);
}
}
name = (name || '').toString().replace(/\\/g, '/');
if (!name.includes('/')) {
name = this.getBackupDir() + name;
const regEx = new RegExp(`_backup${tools.appName}`, 'i');
if (!regEx.test(name)) {
name += `_backup${tools.appName}`;
}
if (!name.match(/\.tar\.gz$/i)) {
name += '.tar.gz';
}
}
if (!fs.existsSync(name)) {
console.error(`host.${hostname} Cannot find ${name}`);
return void this.processExit(11);
}
const tar = require('tar');
if (fs.existsSync(`${tmpDir}/backup/backup.json`)) {
fs.unlinkSync(`${tmpDir}/backup/backup.json`);
}
return new Promise(resolve => {
tar.extract(
{
file: name,
cwd: tmpDir
},
err => {
if (err) {
console.error(`host.${hostname} Cannot extract from file "${name}": ${err.message}`);
return void this.processExit(9);
}
if (!fs.existsSync(`${tmpDir}/backup/backup.json`)) {
console.error(
`host.${hostname} Validation failed. Cannot find extracted file from file "${tmpDir}/backup/backup.json"`
);
return void this.processExit(9);
}
console.log(`host.${hostname} Starting validation ...`);
let backupJSON;
try {
backupJSON = require(`${tmpDir}/backup/backup.json`);
} catch (err) {
console.error(
`host.${hostname} Backup corrupted. Backup ${name} does not contain a valid backup.json file: ${err.message}`
);
try {
tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
} catch (e) {
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
}
return void this.processExit(26);
}
if (!backupJSON || !backupJSON.objects || !backupJSON.objects.length) {
console.error(`host.${hostname} Backup corrupted. Backup does not contain valid objects`);
try {
tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
} catch (e) {
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
}
return void this.processExit(26);
} // endIf
console.log(`host.${hostname} backup.json OK`);
try {
this._checkDirectory(`${tmpDir}/backup/files`, true);
try {
tools.rmdirRecursiveSync(`${tmpDir}/backup/`);
} catch (e) {
console.error(`host.${hostname} Cannot clear temporary backup directory: ${e.message}`);
}
resolve();
} catch (err) {
console.error(`host.${hostname} Backup corrupted: ${err.message}`);
return void this.processExit(26);
}
}
);
});
} // endValidateBackup
/**
* Checks a directory for json files and validates them, steps down recursive in subdirectories
* @param {string} path - path to the directory
* @param {boolean} verbose - if logging should be verbose
* @private
*/
_checkDirectory(path, verbose = false) {
if (fs.existsSync(path)) {
const files = fs.readdirSync(path);
if (!files.length) {
return;
}
for (const file of files) {
const filePath = `${path}/${file}`;
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
// if directory then check it
this._checkDirectory(filePath, verbose);
} else if (file.endsWith('.json')) {
try {
require(filePath);
if (verbose) {
console.log(`host.${hostname} ${file} OK`);
}
} catch {
throw new Error(`host.${hostname} ${filePath} is not a valid json file`);
}
}
}
} // endIf
} // endCheckDirectory
/**
* Restores a backup
*
* @param {string|number} name - backup name or index
* @param {boolean} force - if force, js-controller is allowed to have a different version
* @param {boolean} dontDeleteAdapters - skip adapter deletion, e.g. for setup custom db migration
* @param {(number) => void} callback
*/
restoreBackup(name, force, dontDeleteAdapters, callback) {
let backups;
if (!name && name !== 0) {
// List all available backups
console.log('Please specify one of the backup names:');
backups = this.listBackups();
backups.sort((a, b) => (b > a ? 1 : b === a ? 0 : -1));
if (backups.length) {
backups.forEach((backup, i) =>
console.log(`${backup} or ${backup.replace(`_backup${tools.appName}.tar.gz`, '')} or ${i}`)
);
} else {
console.warn('No backups found');
}
return void this.processExit(10);
}
if (!this.cleanDatabase) {
throw new Error('Invalid arguments: cleanDatabase is missing');
}
if (!this.restartController) {
throw new Error('Invalid arguments: restartController is missing');
}
// If number
if (parseInt(name, 10).toString() === name.toString()) {
backups = this.listBackups();
backups.sort((a, b) => (b > a ? 1 : b === a ? 0 : -1));
name = backups[parseInt(name, 10)];
if (!name) {
console.log('No matching backup found');
if (backups.length) {
console.log('Please specify one of the backup names:');
backups.forEach((backup, i) =>
console.log(`${backup} or ${backup.replace(`_backup${tools.appName}.tar.gz`, '')} or ${i}`)
);
} // endIf
} else {
console.log(`host.${hostname} Using backup file ${name}`);
}
}
name = (name || '').toString().replace(/\\/g, '/');
if (!name.includes('/')) {
name = this.getBackupDir() + name;
const regEx = new RegExp(`_backup${tools.appName}`, 'i');
if (!regEx.test(name)) {
name += '_backup' + tools.appName;
}
if (!name.match(/\.tar\.gz$/i)) {
name += '.tar.gz';
}
}
if (!fs.existsSync(name)) {
console.error(`host.${hostname} Cannot find ${name}`);
return void this.processExit(11);
}
const tar = require('tar');
// delete /backup/backup.json
fs.existsSync(`${tmpDir}/backup/backup.json`) && fs.unlinkSync(`${tmpDir}/backup/backup.json`);
tar.extract(
{
file: name,
cwd: tmpDir
},
err => {
if (err) {
console.error(`host.${hostname} Cannot extract from file "${name}": ${err.message}`);
return void this.processExit(9);
}
if (!fs.existsSync(`${tmpDir}/backup/backup.json`)) {
console.error(
`host.${hostname} Cannot find extracted file from file "${tmpDir}/backup/backup.json"`
);
return void this.processExit(9);
}
// Stop controller
const daemon = require('daemonize2').setup({
main: '../../controller.js',
name: `${tools.appName} controller`,
pidfile: `${__dirname}/../${tools.appName}.pid`,
cwd: '../../',
stopTimeout: 1000
});
daemon.on('error', async () => {
const exitCode = await this._restoreAfterStop(false, force, dontDeleteAdapters);
callback && callback(exitCode);
});
daemon.on('stopped', async () => {
const exitCode = await this._restoreAfterStop(true, force, dontDeleteAdapters);
callback && callback(exitCode);
});
daemon.on('notrunning', async () => {
console.log(`host.${hostname} OK.`);
const exitCode = await this._restoreAfterStop(false, force, dontDeleteAdapters);
callback && callback(exitCode);
});
daemon.stop();
}
);
}
/**
* This method checks if adapter of PRESERVE_ADAPTERS exist, and reinstalls them if this is the case
*
* @return {Promise<void>}
* @private
*/
async _restorePreservedAdapters() {
for (const adapterName of this.PRESERVE_ADAPTERS) {
try {
const adapterObj = await this.objects.getObjectAsync(`system.adapter.${adapterName}`);
if (adapterObj && adapterObj.common && adapterObj.common.version) {
let installSource;
if (adapterObj.common.installedFrom) {
installSource = adapterObj.common.installedFrom;
} else {
installSource = `${tools.appName.toLowerCase()}.${adapterName}@${adapterObj.common.version}`;
}
console.log(`Reinstalling adapter "${adapterName}" from "${installSource}"`);
await tools.installNodeModule(installSource);
}
} catch (e) {
console.error(`Could not ensure existence of adapter "${adapterName}": ${e.message}`);
}
}
}
}
module.exports = BackupRestore;