'use strict'; function Upload(options) { const fs = require('fs'); const tools = require(__dirname + '/../tools.js'); options = options || {}; if (!options.states) throw 'Invalid arguments: states is missing'; if (!options.objects) throw 'Invalid arguments: objects is missing'; let states = options.states; let objects = options.objects; let mime; // get all instances of one adapter function getInstances(adapter, callback) { objects.getObjectList({startkey: 'system.adapter.' + adapter + '.', endkey: 'system.adapter.' + adapter + '.\u9999'}, (err, arr) => { let instances = []; if (!err && arr && arr.rows) { for (let i = 0; i < arr.rows.length; i++) { if (arr.rows[i].value.type !== 'instance') continue; instances.push(arr.rows[i].value._id); } } callback(instances); }); } // get all instances of all adapters in the list function getAllInstances(adapters, callback) { let instances = []; let count = 0; for (let k = 0; k < adapters.length; k++) { if (!adapters[k]) continue; if (adapters[k].indexOf('.') === -1) count++; } for (let i = 0; i < adapters.length; i++) { if (!adapters[i]) continue; if (adapters[i].indexOf('.') === -1) { getInstances(adapters[i], inst => { for (let j = 0; j < inst.length; j++) { if (instances.indexOf(inst[j]) === -1) { instances.push(inst[j]); } } if (!--count && callback) { callback(instances); callback = null; } }); } else { if (instances.indexOf(adapters[i]) === -1) { instances.push(adapters[i]); } } } if (!count && callback) { callback(instances); callback = null; } } // Check if some adapters must be restarted and restart them function checkRestartOther(adapter, callback) { let adapterDir = tools.getAdapterDir(adapter); try { let adapterConf = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json').toString()); if (adapterConf.common.restartAdapters) { if (typeof adapterConf.common.restartAdapters !== 'object') adapterConf.common.restartAdapters = [adapterConf.common.restartAdapters]; if (adapterConf.common.restartAdapters.length && adapterConf.common.restartAdapters[0]) { getAllInstances(adapterConf.common.restartAdapters, instances => { if (!instances || !instances.length) { if (callback) { callback(); callback = null; } } else { let instancesCount = instances.length; for (let r = 0; r < instances.length; r++) { objects.getObject(instances[r], (err, obj) => { // if instance is enabled if (!err && obj && obj.common.enabled) { obj.common.enabled = false; // disable instance obj.from = 'system.host.' + tools.getHostName() + '.cli'; obj.ts = Date.now(); objects.setObject(obj._id, obj, err => { if (!err) { obj.common.enabled = true; // enable instance obj.from = 'system.host.' + tools.getHostName() + '.cli'; obj.ts = Date.now(); objects.setObject(obj._id, obj, err => { console.log('Adapter "' + obj._id + '" restarted.'); if (!--instancesCount && callback) { callback(); callback = null; } }); } else { if (err) { console.error('Cannot restart adapter "' + obj._id + '": ' + err); } else { console.warn('Adapter "' + obj._id + '" is disabled and cannot be restarted.'); } if (!--instancesCount && callback) { callback(); callback = null; } } }); } else if (!--instancesCount && callback) { callback(); callback = null; } }); } } }); } else if (callback) { callback(); callback = null; } } else if (callback) { callback(); callback = null; } } catch (e) { console.error('Cannot parse ' + adapterDir + '/io-package.json:' + e); if (callback) { callback(); callback = null; } } } this.uploadAdapterFull = (adapters, callback) => { if (!adapters || !adapters.length) { if (callback) callback(); return; } const adapter = adapters.pop(); this.uploadAdapter(adapter, true, true, () => { this.upgradeAdapterObjects(adapter, () => { this.uploadAdapter(adapter, false, true, () => { setImmediate(() => this.uploadAdapterFull(adapters, callback)); }); }); }); }; this.uploadFile = (source, target, callback) => { const request = require('request'); target = target.replace(/\\/g, '/'); source = source.replace(/\\/g, '/'); if (target[0] === '/') target = target.substring(1); if (target[target.length - 1] === '/') { let name = source.split('/').pop(); name = name.split('?')[0]; if (name.indexOf('.') === -1) name = 'index.html'; target += name; } let parts = target.split('/'); const adapter = parts[0]; parts.splice(0, 1); target = parts.join('/'); if (source.match(/^http:\/\/|^https:\/\//)) { request(source, (error, response, body) => { if (!error && response.statusCode === 200) { objects.writeFile(adapter, target, body, err => { if (err) console.error(err); if (typeof callback === 'function') callback(err, adapter + '/' + target); }); } else { console.error('Cannot get URL: ' + error || response.statusCode); if (typeof callback === 'function') callback(error || response.statusCode, adapter + '/' + target); } }); } else { try { objects.writeFile(adapter, target, fs.readFileSync(source), err => { if (err) console.error(err); if (typeof callback === 'function') callback(err, adapter + '/' + target); }); } catch (err) { console.error('Cannot read file "' + source + '": ' + err); if (typeof callback === 'function') callback(err, adapter + '/' + target); } } }; // Upload www folder of adapter into ObjectsDB this.uploadAdapter = (adapter, isAdmin, forceUpload, subTree, callback) => { const id = adapter + (isAdmin ? '.admin' : ''); const adapterDir = tools.getAdapterDir(adapter); let dir = adapterDir + (isAdmin ? '/admin' : '/www'); let files = []; let rev; if (typeof subTree === 'function') { callback = subTree; subTree = null; } if (subTree) { dir += '/' + subTree; } if (!isAdmin) { let cfg; // check for common.wwwDontUpload (required for legacy adapter) if (fs.existsSync(adapterDir + '/io-package.json')) { cfg = require(adapterDir + '/io-package.json'); } if (cfg && cfg.common && cfg.common.wwwDontUpload) { if (typeof callback === 'function') callback(adapter); return; } } // do not upload www dir of admin adapter if (adapter === 'admin' && !isAdmin) { // To DO remove after a while console.log('This should never happens!'); if (typeof callback === 'function') callback(adapter); return; } // Create "upload progress" object if not exists if (!isAdmin) { objects.getObject('system.adapter.' + adapter + '.upload', (err, obj) => { if (err || !obj) { objects.setObject('system.adapter.' + adapter + '.upload', { _id: 'system.adapter.' + adapter + '.upload', type: 'state', common: { name: adapter + '.upload', type: 'number', role: 'indicator.state', unit: '%', def: 0, desc: 'Upload process indicator' }, from: 'system.host.' + tools.getHostName() + '.cli', ts: Date.now(), native: {} } ); } }); // Set indicator to 0 states.setState('system.adapter.' + adapter + '.upload', 0, true); } if (!mime) mime = require('mime'); function done(err, res) { if (err) { callback(); } else { console.log('got ' + dir); files = res; setTimeout(_adapter => { maxFiles = files.length || 1; upload(_adapter); }, 25, adapter); } } let maxFiles = 0; let lastProgressUpdate = (new Date()).getTime(); function upload(adapter) { let file; if (!files.length) { if (!isAdmin) { states.setState('system.adapter.' + adapter + '.upload', {val: 0, ack: true}, () => { if (typeof callback === 'function') callback(adapter); }); } else { if (typeof callback === 'function') callback(adapter); } } else { file = files.pop(); if (file === '.gitignore') { upload(adapter); return; } const mimeType = mime.lookup(file); let attName; attName = file.split('/' + tools.appName + '.'); if (attName.length === 1) { // try to find anyway if adapter is not lower case const pos = file.toLowerCase().indexOf(tools.appName.toLowerCase()); if (pos !== -1) { attName = ['', file.substring(tools.appName.length + 2)]; } } attName = attName.pop(); attName = attName.split('/').slice(2).join('/'); if (files.length > 100) { if (!(files.length % 50)) { console.log('upload [' + files.length + ']', id, file, attName, mimeType); } } else if (files.length > 20) { if (!(files.length % 10)) { console.log('upload [' + files.length + ']', id, file, attName, mimeType); } } else { console.log('upload [' + files.length + ']', id, file, attName, mimeType); } // Update upload indicator if (!isAdmin) { const now = (new Date()).getTime(); if (now - lastProgressUpdate > 1000) { lastProgressUpdate = now; states.setState('system.adapter.' + adapter + '.upload', {val: Math.round(1000 * (maxFiles - files.length) / maxFiles) / 10, ack: true}); } } fs.createReadStream(file).pipe( objects.insert(id, attName, null, mimeType, { rev: rev }, (err, res) => { if (err) { console.log(err); if (typeof callback === 'function') callback(adapter); } if (res) rev = res.rev; setTimeout(upload, 50, adapter); }) ); } } function walk(dir, done) { let results = []; fs.readdir(dir, (err, list) => { if (err) return done(err); let i = 0; (function next() { let file = list[i++]; if (!file) return done(null, results); file = dir + '/' + file; fs.stat(file, (err, stat) => { if (stat && stat.isDirectory()) { walk(file, (err, res) => { results = results.concat(res); next(); }); } else { if (!file.match(/\.npmignore$/) && !file.match(/\.gitignore$/)) results.push(file); next(); } }); })(); }); } objects.getObject(id, (err, res) => { if (err || !res) { objects.setObject(id, { type: 'meta', common: { name: id.split('.').pop(), type: isAdmin ? 'admin' : 'www' }, from: 'system.host.' + tools.getHostName() + '.cli', ts: Date.now(), native: {} }, (err, res) => { if (res) rev = res.rev; if (!isAdmin) { checkRestartOther(adapter, () => walk(dir, done)); } else { walk(dir, done); } }); } else { if (!forceUpload) { if (typeof callback === 'function') callback(adapter); } else { rev = res.rev; if (!isAdmin) { checkRestartOther(adapter, () => walk(dir, done)); } else { walk(dir, done); } } } }); }; function extendNative(target, additional) { for (let attr in additional) { if (additional.hasOwnProperty(attr)) { if (target[attr] === undefined) { target[attr] = additional[attr]; } else if (typeof additional[attr] === 'object' && !(additional[attr] instanceof Array)) { try { target[attr] = target[attr] || {}; } catch (e) { console.warn(`Cannot update attribute ${attr} of native`); } extendNative(target[attr], additional[attr]); } } } return target; } function extendCommon(target, additional) { for (let attr in additional) { if (additional.hasOwnProperty(attr)) { if (attr === 'title' || attr === 'schedule' || attr === 'mode' || attr === 'loglevel' || attr === 'enabled') { if (target[attr] === undefined) { target[attr] = additional[attr]; } } else if (typeof additional[attr] !== 'object' || (additional[attr] instanceof Array)) { try { target[attr] = additional[attr]; } catch (e) { console.warn(`Cannot update attribute ${attr} of common`); } } else { target[attr] = target[attr] || {}; if (typeof target[attr] !== 'object') { target[attr] = {}; // here we clean the simple value with object } extendCommon(target[attr], additional[attr]); } } } return target; } this.upgradeAdapterObjects = (name, iopack, callback) => { if (typeof iopack === 'function') { callback = iopack; iopack = null; } if (!iopack) { const adapterDir = tools.getAdapterDir(name); try { iopack = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json', 'utf8')); } catch (e) { console.error('Cannot find io-package.json in ' + adapterDir); iopack = null; } } if (!iopack) { callback(name); } else { objects.getObject('system.adapter.' + name, (err, obj) => { if (err || !obj) { console.error('system.adapter.' + name + ' does not exist'); callback(name); } else { obj.common = iopack.common || {}; obj.native = iopack.native || {}; obj.common.installedVersion = iopack.common.version; const hostname = tools.getHostName(); obj.from = 'system.host.' + tools.getHostName() + '.cli'; obj.ts = Date.now(); objects.setObject('system.adapter.' + name, obj, () => { // Update all instances of this host objects.getObjectView('system', 'instance', {startkey: 'system.adapter.' + name + '.', endkey: 'system.adapter.' + name + '.\u9999'}, null, function (err, res) { let cntr = 0; if (res) { for (let i = 0; i < res.rows.length; i++) { if (res.rows[i].value.common.host === hostname) { cntr++; objects.getObject(res.rows[i].id, (err, _obj) => { let newObject = JSON.parse(JSON.stringify(_obj)); // all common settings should be taken from new one newObject.common = extendCommon(newObject.common, iopack.common); newObject.native = extendNative(newObject.native, iopack.native); newObject.common.installedVersion = iopack.common.version; newObject.common.version = iopack.common.version; // Compare objects to reduce restarts of instances if (JSON.stringify(newObject) !== JSON.stringify(_obj)) { console.log('Update "' + newObject._id + '"'); newObject.from = 'system.host.' + tools.getHostName() + '.cli'; newObject.ts = Date.now(); objects.setObject(newObject._id, newObject, () => { if (!--cntr && callback) callback(name); }); } else { if (!--cntr && callback) callback(name); } }); } } } // updates "_design/system" and co if (iopack.objects && typeof iopack.objects === 'object') { for (let _id in iopack.objects) { if (!iopack.objects.hasOwnProperty(_id)) continue; cntr++; iopack.objects[_id].from = 'system.host.' + tools.getHostName() + '.cli'; iopack.objects[_id].ts = Date.now(); objects.setObject(iopack.objects[_id]._id, iopack.objects[_id], err => { if (err) console.error('Cannot update object: ' + err); if (!--cntr && callback) callback(name); }); } } if (!cntr && callback) callback(name); }); }); } }); } }; } module.exports = Upload;