UNPKG

iobroker.javascript

Version:

Javascript/Coffescript Script Engine for ioBroker

339 lines (309 loc) 13.8 kB
'use strict'; const fs = require('fs'); const path = require('path'); class Mirror { constructor(options) { if (!options || !options.diskRoot) { return; } if (!options.adapter) { throw new Error('No adapter defined'); } this.adapter = options.adapter; this.diskRoot = path.normalize(options.diskRoot).replace(/\\/g, '/'); if (!fs.existsSync(this.diskRoot)) { try { fs.mkdirSync(this.diskRoot); } catch (e) { this.log.error(`Cannot create directory ${this.diskRoot}: ${e}`); return; } } this.log = options.log || { debug: function (text) {console.log(text);}, info: function (text) {console.log(text);}, log: function (text) {console.log(text);}, warn: function (text) {console.warn(text);}, error: function (text) {console.error(text);}, silly: function (text) {console.log(text);} }; this.diskList = this.scanDisk(); this.scanDB(list => { this.dbList = list; this.sync(); this.log.info('Watch ' + this.diskRoot); fs.watch(this.diskRoot, (eventType, filename) => { this.log.debug('File ' + filename + ' ' + eventType); this.onFileChange(eventType, filename); }); }); } static getDBFolder(id) { const parts = id.split('.'); parts.pop(); return parts.join('.'); } updateFolderTime(id) { this.dbList[id].ts = Date.now(); this.adapter.setForeignObject(id, this.dbList[id]); } _getDiskPath(pathDisk) { return path.join(this.diskRoot, pathDisk.join('/')).replace(/\\/g, '/'); } static _getDbPath(pathDB) { if (!pathDB || !pathDB.length) { return 'script.js'; } else { return 'script.js.' + pathDB.join('.'); } } static createRecursiveDir(dirDisk) { const parts = dirDisk.replace(/\\/g, '/').split('/'); let path = ''; for (let i = 0; i < parts.length; i++) { path += (path ? '/' : '') + parts[i]; if (!fs.existsSync(path)) { fs.mkdirSync(path); } } } _getFilesInPath(pathDisk) { pathDisk = pathDisk.substring(this.diskRoot.length + 1); const id = pathDisk.replace(/\./g, '\\.').replace(/[/\\]/g, '\\.'); const reg = new RegExp('script\\.js' + (id ? '.' + id : '') + '\\.[^.]+$'); return Object.keys(this.diskList).filter(file => reg.test(file)).map(id => id.split('.').pop()); } _getObjectsInPath(pathDB) { const reg = new RegExp(pathDB.replace(/\./g, '\\.') + '\\.[^.]+$'); return Object.keys(this.dbList).filter(id => reg.test(id)).map(id => id.split('.').pop()); } sync(pathDisk, pathDB) { pathDisk = pathDisk || []; pathDB = pathDB || []; const dirDisk = this._getDiskPath(pathDisk); const dirDB = Mirror._getDbPath(pathDB); const files = this._getFilesInPath(dirDisk); const objects = this._getObjectsInPath(dirDB); for (let o = objects.length - 1; o >= 0; o--) { if (this.dbList[dirDB + '.' + objects[o]].type === 'channel') { const nextPathDisk = JSON.parse(JSON.stringify(pathDisk)); nextPathDisk.push(objects[o]); const nextPathDB = JSON.parse(JSON.stringify(pathDB)); nextPathDB.push(objects[o]); return this.sync(nextPathDisk, nextPathDB); } else { for (let f = files.length - 1; f >= 0; f--) { if (objects[o] === files[f]) { const id = dirDB + '.' + objects[o]; if (!this.dbList[id].ts || (this.dbList[id].ts > this.diskList[id].ts && this.dbList[id].ts - this.diskList[id].ts > 2000)) { // copy text to file this.dbList[id].ts = Date.now(); this.diskList[id].source = this.dbList[id].common.source; this.diskList[id].ts = Date.now(); this.log.debug('Update disk with ' + this.diskList[id].name); Mirror.createRecursiveDir(dirDisk); fs.writeFileSync(this.diskList[id].name, this.dbList[id].common.source); } else if (this.dbList[id].ts && (this.dbList[id].ts < this.diskList[id].ts && this.diskList[id].ts - this.dbList[id].ts > 2000)) { this.dbList[id].common.source = this.diskList[id].source; this.dbList[id].ts = this.diskList[id].ts; this.log.debug('Update DB with ' + id); this.adapter.setForeignObject(id, this.dbList[id]); } files.splice(f, 1); objects.splice(o, 1); } } } } // go through objects, that does not exist on disk for (let o = objects.length - 1; o >= 0; o--) { const fileName = this._scriptId2FileName(dirDB + '.' + objects[o], this.dbList[dirDB + '.' + objects[o]].common.engineType); this.log.info('Created script file on disk ' + fileName); Mirror.createRecursiveDir(dirDisk); const f = 'script.js.' + fileName.substring(this.diskRoot.length).replace(/[\\/]g/, '.').replace(/\.js$|\.ts$/g, ''); this.diskList[f] = {name: fileName, source: this.dbList[dirDB + '.' + objects[o]].common.source, ts: Date.now()}; fs.writeFileSync(fileName, this.dbList[dirDB + '.' + objects[o]].common.source); } // go through files, that does not exist in DB for (let f = files.length - 1; f >= 0; f--) { this.log.warn('Please delete file ' + path.join(dirDisk, files[f]) + ' or create according script in ioBroker'); } } _scriptId2FileName(id, type) { id = id.substring('script.js.'.length); const parts = id.split('.'); return path.join(this.diskRoot, parts.join('/')).replace(/\\/g, '/') + (type === 'TypeScript/ts' ? '.ts' : '.js'); } _fileName2scriptId(file) { file = file.substring(this.diskRoot.length).replace(/\.js$/g, '').replace(/\.ts$/g, ''); const parts = file.replace(/\\/g, '/').split('/'); if (!parts[0] && parts.length) { parts.shift(); } return 'script.js.' + parts.join('.'); } onFileChange(event, file) { if (!file.match(/\.ts$|\.js$/)) return; const id = this._fileName2scriptId(file); if (event === 'change' || event === 'create') { try { const stats = fs.statSync(file); const source = fs.readFileSync(file).toString(); this.diskList[file] = {ts: stats.atime, source}; if (this.dbList[id]) { if (this.dbList[id].common.source !== source) { this.dbList[id].common.source = source; this.dbList[id].ts = stats.atime; this.adapter.setForeignObject(id, this.dbList[id]); } else { this.dbList[id].ts = stats.atime; } } else { const parts = id.split('.'); // new script this.dbList[id] = { _id: id, common: { name: parts.pop(), engineType: 'Javascript/js', source, enabled: false, engine: 'system.adapter.javascript.0', debug: false, verbose: false }, type: 'script', native: {}, ts: stats.atime }; this.adapter.setForeignObject(id, this.dbList[id]); } } catch (e) { this.log.error('Cannot read file ' + file + ': ' + e); } } else if (event === 'delete' || event === 'rename') { if (this.dbList[id]) { delete this.dbList[id]; this.adapter.delForeignObject(id); } if (this.diskList[file]) { delete this.diskList[file]; } } } onObjectChange(id, obj) { if (!this.dbList || !id) { return; } const file = this._scriptId2FileName(id); if (!obj || !obj.common) { if (this.dbList[id]) { delete this.dbList[id]; const folderId = Mirror.getDBFolder(id); if (this.dbList[folderId]) { this.updateFolderTime(folderId); } if (fs.existsSync(file)) { try { fs.unlinkSync(file); } catch (e) { this.log.error('Cannot delete ' + file + ': ' + e); } } if (this.diskList[file]) { delete this.diskList[file]; } } } else { if (this.dbList[id]) { const folderDirParts = file.split(/[\\/]/); folderDirParts.pop(); Mirror.createRecursiveDir(folderDirParts.join('/')); if (this.dbList[id].common.source !== obj.common.source) { this.dbList[id] = obj; fs.writeFileSync(file, obj.common.source); this.diskList[file] = {ts: Date.now(), source: obj.common.source}; } else if (!this.diskList[id] || this.diskList[id].source !== obj.common.source) { fs.writeFileSync(file, obj.common.source); this.diskList[id] = {ts: Date.now(), source: obj.common.source, name: file}; } } else { // new script this.dbList[id] = obj; if (!this.diskList[id] || this.diskList[id].source !== obj.common.source) { const folderDirParts = file.split(/[\\/]/); folderDirParts.pop(); Mirror.createRecursiveDir(folderDirParts.join('/')); fs.writeFileSync(file, obj.common.source); this.diskList[id] = {ts: Date.now(), source: obj.common.source, name: file}; } } this.dbList[id].ts = Date.now(); } } scanDisk(dirPath, list) { dirPath = dirPath || this.diskRoot; list = list || {}; if (fs.existsSync(dirPath)) { const files = fs.readdirSync(dirPath); files.forEach(file => { const fullName = path.join(dirPath, file); const stats = fs.statSync(fullName); if (stats.isDirectory()) { this.scanDisk(fullName.replace(/\\/g, '/'), list); } else if (file.match(/\.js$|\.ts$/)) { let f = fullName.replace(/[\\/]/g, '.'); f = 'script.js.' + f.substring(this.diskRoot.length + 1).replace(/\.js$|\.ts$/g, ''); list[f] = {ts: Math.round(stats.atime), source: fs.readFileSync(fullName).toString(), name: fullName}; } }); } return list; } _checkIfAllFoldersAreExist(id, list) { const parts = id.split('.'); for (let i = parts.length - 1; i >= 2; i--) { parts.pop(); const folderId = parts.join('.'); if (!list[folderId]) { const obj = { _id: folderId, common: { name: parts[parts.length - 1] }, type: 'channel', native: {} }; list[folderId] = obj; this.adapter.setForeignState(folderId, obj); } } } scanDB(cb) { this.adapter.objects.getObjectView('system', 'channel', {startkey: 'script.js.', endkey: 'script.js.\u9999'}, (err, res) => { // this is not required, because javascript subscribes on ALL objects // adapter.subscribeForeignObjects('script.js.*'); const list = {}; for (let i = 0; i < res.rows.length; i++) { const value = res.rows[i].value; if (value && value._id && value.common) { list[res.rows[i].value._id] = res.rows[i].value; } } this.adapter.objects.getObjectView('script', 'javascript', {startkey: 'script.js.', endkey: 'script.js.\u9999'}, (err, res) => { for (let i = 0; i < res.rows.length; i++) { const value = res.rows[i].value; if (value && value._id && value.common) { list[res.rows[i].value._id] = res.rows[i].value; // ensure that every script has a folder and if not then create it this._checkIfAllFoldersAreExist(res.rows[i].value._id, list); } } cb && cb(list); }); }); } } module.exports = Mirror;