UNPKG

node-red-contrib-sensecraft-application

Version:

This project is a Node-RED contribution for the Sensecraft application, allowing users to integrate Sensecraft functionalities into their Node-RED flows.

1,254 lines (1,042 loc) 54.3 kB
const {log} = require('console'); const path = require('path'), fs = require('fs-extra'), bodyParser = require('body-parser'), log4js = require('log4js'), os = require("os"), jsonata = require('jsonata'), eol = require('eol'), YAML = require('js-yaml'), child_process = require('child_process'), crypto = require('crypto'), debounce = require('./debounce'), axios = require('axios'); let alertStatus = true function execShellCommand(cmd) { return new Promise((resolve, reject) => { child_process.exec(cmd, (error, stdout, stderr) => { if (error) { reject(new Error(stderr || stdout)); } resolve(stdout); }); }); } function encodeFileName(origName) { return origName.replace(/\\/g, '%5C').replace(/\//g, '%2F').replace(/\:/g, '%3A').replace(/\*/g, '%2A').replace(/\?/g, '%3F').replace(/\""/g, '%22').replace(/\</g, '%3C').replace(/\>/g, '%3E').replace(/\|/g, '%7C') // Characters not allowed: \ / : * ? " < > | } function changeTime(inputString) { let timestampString = inputString.match(/\d+/)[0]; let timestamp = parseInt(timestampString, 10); let date = new Date(timestamp); return date.toISOString() } function stringifyFormattedFileJson(nodes) { const str = JSON.stringify(nodes, undefined, 2); return eol.auto(str); } // NOTE(alonam) a little trick to require the same "node-red" API to give private access to our own modulesContext. const PRIVATERED = (function requireExistingNoderedInstance() { for (const child of require.main.children) { if (child.filename.endsWith('red.js')) { return require(child.filename); } } // In case node-red was not required before, just require it return require('node-red'); })(); const nodeName = path.basename(__filename).split('.')[0]; const nodeLogger = log4js.getLogger('NodeRed FlowManager'); let RED; function getNodesEnvConfigForNode(node) { return (node.name && directories.nodesEnvConfig["name:" + node.name]) || (node.id && directories.nodesEnvConfig[node.id]); } async function getNodeList() { return await PRIVATERED.runtime.nodes.getNodeList({}); } let update_flag = false; async function iinstallModeList(info) { if (info !== "") { let newModules = JSON.parse(info); let installList = [] if (newModules?.libs?.length > 0) { const oldModules = await getNodeList() // 创建一个映射,用于存储旧模块的版本信息 const oldVersionMap = new Map(oldModules.map(m => [m.module, m.version])); // 迭代新模块以找出新增或版本不同的模块 newModules = newModules.libs const updates = newModules.filter(m => { const isNewOrUpdated = !oldVersionMap.has(m.module) || oldVersionMap.get(m.module) !== m.version; if (oldVersionMap.get(m.module) !== m.version && !update_flag) { update_flag = true; } return isNewOrUpdated; }); if (updates.length > 0) { for (let i = 0; i < updates.length; i++) { let item = updates[i]; console.log(`install or updat module: ${item.module},version : ${item.version}`); await PRIVATERED.runtime.nodes.addModule({module: item.module, version: item.version}); } } } } } async function allNpm(flows) { const nodes = await getNodeList(); const now = new Date().valueOf() let result = { "id": `sensecraft${now}`, "type": "comment", "name": "sensecraft-libs", "info": [], "x": 2005, "y": 7980, "wires": [], "l": false, } const flowFile = await readFlowByNameAndType("global"); // console.log(JSON.stringify(flowFile)) flows.forEach(flow => { if (flow.type === "tab") { result.z = flow.id } // const now = new Date(); nodes.forEach(node => { node.types.forEach(type => { if (flow.type === type && node.module !== "node-red") { const flowRev = calculateRevision(node.version) let nodeInfo = { enabled: node.enabled, local: node.local, user: node.user, version: node.version, module: node.module, // rev: flowRev, // mtime: now.toISOString() } result.info.push(nodeInfo); } }) }) }) const uniqueModules = []; const seenModules = {}; result.info.forEach(item => { if (!seenModules[item.module]) { seenModules[item.module] = true; uniqueModules.push(item); } }); const infoString = { "global": JSON.parse(flowFile.str), "libs": uniqueModules, } result.info = JSON.stringify(infoString) // 查找数组中 type 为 npm 的对象的索引 const npmIndex = flows.findIndex(item => item.name === "sensecraft-libs"); if (npmIndex !== -1) { flows[npmIndex] = result; } else { flows.push(result); console.log("npmIndex === -1") } return {flows: flows, sensecraft: result} } let lastFlows = {}; const originalSaveFlows = PRIVATERED.runtime.storage.saveFlows; PRIVATERED.runtime.storage.saveFlows = async function newSaveFlows(data) { if (data.flows && data.flows.length) { // 从nodes 中提取所需的npm 包 let infoList = await allNpm(data.flows) data.flows = infoList.flows function getFlowFilePath(flowDetails) { let folderName; if (flowDetails.type === 'subflow') folderName = 'subflows'; else if (flowDetails.type === 'tab') folderName = 'flows'; else if (flowDetails.name = "sensecraft-libs") folderName = 'sensecraft-libs'; else folderName = '.'; const flowsDir = path.resolve(directories.basePath, folderName); const flowName = flowDetails.type === 'global' ? 'config-nodes' : flowDetails.name; // if it's a subflow, then the correct property is 'name' const flowFilePath = path.join(flowsDir, encodeFileName(flowName) + '.' + flowManagerSettings.fileFormat); return flowFilePath; } const sensecraftPath = getFlowFilePath(infoList.sensecraft); await writeFlowFile(sensecraftPath, infoList.sensecraft); const loadedFlowAndSubflowNames = {}; const envNodePropsChangedByUser = {}; const allFlows = {}; const orderedNodeIds = []; for (const node of data.flows) { orderedNodeIds.push(node.id); // // Enforce envnodes consistency // const envConfig = getNodesEnvConfigForNode(node); if (envConfig) { for (const key in envConfig) { const val = envConfig[key]; try { if (JSON.stringify(node[key]) !== JSON.stringify(val)) { if (!envNodePropsChangedByUser[node.id]) envNodePropsChangedByUser[node.id] = {}; envNodePropsChangedByUser[node.id][key] = val; node[key] = val; } } catch (e) { } } } // // Fill flows, subflows, config-nodes if (node.type === 'tab' || node.type === 'subflow') { if (!allFlows[node.id]) allFlows[node.id] = {nodes: []}; if (!allFlows[node.id].name) allFlows[node.id].name = node.label || node.name; if (!allFlows[node.id].type) allFlows[node.id].type = node.type; allFlows[node.id].nodes.push(node); loadedFlowAndSubflowNames[node.id] = allFlows[node.id]; } else if (!node.z) { // global (config-node) if (!allFlows.global) allFlows.global = {name: "config-nodes", type: "global", nodes: []}; allFlows.global.nodes.push(node); } else { // simple node if (!allFlows[node.z]) allFlows[node.z] = {nodes: []}; allFlows[node.z].nodes.push(node); } } // save node ordering file await fs.writeJson(directories.nodesOrderFilePath, orderedNodeIds); // If envnode property changed, send back original values to revert on Node-RED UI as well. if (Object.keys(envNodePropsChangedByUser).length > 0) { RED.comms.publish('sensecraft-application/sensecraft-application-envnodes-override-attempt', envNodePropsChangedByUser); } deployedFlowNames.clear(); for (const flowId of Object.keys(allFlows)) { const flowName = allFlows[flowId].name; const flowType = allFlows[flowId].type; if (flowType === 'tab' && (flowManagerSettings.filter.indexOf(flowName) !== -1 || flowManagerSettings.filter.length === 0 || onDemandFlowsManager.onDemandFlowsSet.has(flowName))) { deployedFlowNames.add(flowName); } // Potentially we save different flow file than the one we deploy, like "credentials" property is deleted in flow file. const flowNodes = JSON.parse(JSON.stringify(allFlows[flowId].nodes)); for (const node of flowNodes) { // Set properties used by envnodes to mared them as sensecraft-application managed. /*const foundNodeEnvConfig = getNodesEnvConfigForNode(node); if (foundNodeEnvConfig) { for (const prop in foundNodeEnvConfig) { node[prop] = "sensecraft-application-managed"; } }*/ // delete credentials if (node.credentials != null) { delete node.credentials; } } const flowFilePath = getFlowFilePath({type: flowType, name: flowName}); try { if (lastFlows[flowId] && allFlows[flowId] && lastFlows[flowId].name != allFlows[flowId].name) { const flowsDir = path.dirname(flowFilePath); const from = path.resolve(flowsDir, `${encodeFileName(lastFlows[flowId].name)}.${flowManagerSettings.fileFormat}`); const to = path.resolve(flowFilePath); try { // First, try rename with git await execShellCommand(`git mv -f "${from}" "${to}"`); } catch (e) { // if not working with git, do a normal "mv" command try { await fs.move(from, to, {overwrite: true}); } catch (e) { } } } // save flow file const fileStr = await writeFlowFile(flowFilePath, flowNodes); const stat = await fs.stat(flowFilePath); // update revisions const flowRev = calculateRevision(fileStr) switch (flowType) { case 'tab': revisions.byFlowName[flowName] = {rev: flowRev, mtime: stat.mtime}; break case 'subflow': revisions.bySubflowName[flowName] = {rev: flowRev, mtime: stat.mtime}; break case 'global': revisions.global = {rev: flowRev, mtime: stat.mtime}; break } } catch (err) { } } // Check which flows were removed, if any for (const flowId in lastFlows) { const flowName = lastFlows[flowId].name; if (!allFlows[flowId] && ((flowManagerSettings.filter.indexOf(flowName) !== -1 || flowManagerSettings.filter.length === 0) || lastFlows[flowId].type === 'subflow')) { const flowFilePath = getFlowFilePath(lastFlows[flowId]); await fs.remove(flowFilePath); } } lastFlows = loadedFlowAndSubflowNames; } return originalSaveFlows.apply(PRIVATERED.runtime.storage, arguments); } const flowManagerSettings = {}; async function writeFlowFile(filePath, flowStrOrObject) { let str; async function isReallyChanged(newObj) { try { const oldObj = (await readFlowFile(filePath)).obj; const oldSet = new Set(oldObj.map(item => JSON.stringify(item))); const newSet = new Set(newObj.map(item => JSON.stringify(item))); if (oldSet.size !== newSet.size) return true; newSet.forEach(function (item) { if (!oldSet.has(item)) throw 'Found change'; }) } catch (e) { return true; } } let changed; if (typeof flowStrOrObject === 'string' || flowStrOrObject instanceof String) { changed = await isReallyChanged(filePath.endsWith('.yaml') ? YAML.safeLoad(flowStrOrObject) : JSON.parse(flowStrOrObject)); str = flowStrOrObject; } else if (flowManagerSettings.fileFormat === 'yaml') { changed = await isReallyChanged(flowStrOrObject); str = YAML.safeDump(flowStrOrObject); } else { changed = await isReallyChanged(flowStrOrObject); str = stringifyFormattedFileJson(flowStrOrObject); } if (changed) { await fs.outputFile(filePath, str); } return str; } async function readFlowFile(filePath, ignoreObj) { const retVal = {}; const fileContentsStr = await fs.readFile(filePath, 'utf-8'); retVal.str = fileContentsStr; if (ignoreObj) { retVal.mtime = (await fs.stat(filePath)).mtime return retVal; } const indexOfExtension = filePath.lastIndexOf('.'); const fileExt = filePath.substring(indexOfExtension + 1).toLowerCase(); const finalObject = fileExt === 'yaml' ? YAML.safeLoad(fileContentsStr) : JSON.parse(fileContentsStr); if (fileExt !== flowManagerSettings.fileFormat) { // File needs conversion const newFilePathWithNewExt = filePath.substring(0, indexOfExtension) + '.' + flowManagerSettings.fileFormat; await writeFlowFile(newFilePathWithNewExt, finalObject); // Delete old file await fs.remove(filePath); filePath = newFilePathWithNewExt; } retVal.mtime = (await fs.stat(filePath)).mtime retVal.obj = finalObject; return retVal; } async function readConfigFlowFile(ignoreObj) { try { return await readFlowFile(directories.configNodesFilePathWithoutExtension + '.json', ignoreObj); } catch (e) { return await readFlowFile(directories.configNodesFilePathWithoutExtension + '.yaml', ignoreObj); } } async function readFlowByNameAndType(type, name, ignoreObj) { const fileNameWithExt = `${name}.${flowManagerSettings.fileFormat}`; if (type === 'flow') { return await readFlowFile(path.join(directories.flowsDir, fileNameWithExt), ignoreObj); } else if (type === 'subflow') { return await readFlowFile(path.join(directories.subflowsDir, fileNameWithExt), ignoreObj); } else if (type === 'global') { return await (readConfigFlowFile(ignoreObj)); } } async function flowFileExists(flowName) { return fs.exists(path.resolve(directories.flowsDir, `${flowName}.${flowManagerSettings.fileFormat}`)); } const onDemandFlowsManager = { onDemandFlowsSet: new Set(), updateOnDemandFlowsSet: async function (newSet) { let totalSet; // // If flow file does not exist, or is already passed filtering settings, we don't consider it an on-demand flow // if (flowManagerSettings.filter.length === 0) { totalSet = new Set(); } else { totalSet = newSet; for (const flowName of Array.from(totalSet)) { if (!await flowFileExists(flowName) || flowManagerSettings.filter.indexOf(flowName) !== -1) { totalSet.delete(flowName); } } } onDemandFlowsManager.onDemandFlowsSet = totalSet; return onDemandFlowsManager.onDemandFlowsSet; } } async function readProject() { try { // newer nodered versions const newCfgPath = path.join(RED.settings.userDir, '.config.projects.json'); const newCfgPath_exists = fs.existsSync(newCfgPath); if (newCfgPath_exists) { const redConfig = await fs.readJson(newCfgPath); return {path: newCfgPath, activeProject: redConfig.activeProject}; } // older nodered versions const oldCfgPath = path.join(RED.settings.userDir, '.config.json'); const oldCfgPath_exists = fs.existsSync(oldCfgPath); if (oldCfgPath_exists) { const redConfig = await fs.readJson(oldCfgPath); return {path: oldCfgPath, activeProject: redConfig.projects.activeProject}; } } catch (e) { } return null; } const revisions = { byFlowName: {}, bySubflowName: {}, global: null // config nodes, etc.. } let deployedFlowNames = new Set(); function calculateRevision(str) { return crypto.createHash('md5').update(str.replace(/\s+/g, '')).digest("hex"); } const directories = {}; async function main() { let initialLoadPromise = (() => { let res; const p = new Promise((resolve, reject) => { res = resolve; }); p.resolve = res; return p })() const originalGetFlows = PRIVATERED.runtime.storage.getFlows; PRIVATERED.runtime.storage.getFlows = async function () { if (initialLoadPromise) await initialLoadPromise; const retVal = await originalGetFlows.apply(PRIVATERED.runtime.storage, arguments); const flowsInfo = await loadFlows(null, true); retVal.flows = flowsInfo.flows; retVal.rev = calculateRevision(JSON.stringify(flowsInfo.flows)); revisions.byFlowName = flowsInfo.flowVersions; revisions.bySubflowName = flowsInfo.subflowVersions; revisions.global = flowsInfo.globalVersion; deployedFlowNames = new Set(Object.values(flowsInfo.loadedFlowAndSubflowNames) .filter(flow => flow.type === 'tab') .map(flow => flow.name)); lastFlows = flowsInfo.loadedFlowAndSubflowNames; return retVal; } async function refreshDirectories() { let basePath, project = null; if (RED.settings.editorTheme.projects.enabled) { projectObj = await readProject(); if (projectObj?.activeProject) { project = projectObj.activeProject; const activeProjectPath = path.join(RED.settings.userDir, 'projects', project); basePath = activeProjectPath; } else { basePath = RED.settings.userDir; } } else { basePath = RED.settings.userDir; } Object.assign(directories, { basePath: basePath, subflowsDir: path.resolve(basePath, 'subflows'), flowsDir: path.resolve(basePath, 'flows'), envNodesDir: path.resolve(basePath, 'envnodes'), flowFile: path.resolve(basePath, RED.settings.flowFile || 'flows_' + os.hostname() + '.json'), project: project, flowManagerCfg: path.resolve(basePath, 'sensecraft-application-cfg.json'), configNodesFilePathWithoutExtension: path.resolve(basePath, 'config-nodes'), nodesOrderFilePath: path.resolve(basePath, 'sensecraft-application-nodes-order.json'), }); // Read sensecraft-application settings let needToSaveFlowManagerSettings = false; try { const fileJson = await fs.readJson(directories.flowManagerCfg); // Backwards compatibility if (Array.isArray(fileJson)) { needToSaveFlowManagerSettings = true; flowManagerSettings.filter = fileJson; } else if (typeof fileJson === 'object') { Object.assign(flowManagerSettings, fileJson); } } catch (e) { } finally { if (!Array.isArray(flowManagerSettings.filter)) { needToSaveFlowManagerSettings = true; flowManagerSettings.filter = []; } if (!flowManagerSettings.fileFormat) { needToSaveFlowManagerSettings = true; flowManagerSettings.fileFormat = 'json'; } } if (needToSaveFlowManagerSettings) { await fs.outputFile(directories.flowManagerCfg, stringifyFormattedFileJson(flowManagerSettings)); } directories.configNodesFilePath = directories.configNodesFilePathWithoutExtension + '.' + flowManagerSettings.fileFormat; // Loading ENV configuration for nodes directories.nodesEnvConfig = {}; const envNodeResolutionPromises = []; try { // envnodes dir is optional for (const envNodeFileName of await fs.readdir(directories.envNodesDir)) { const ext = envNodeFileName.substring(envNodeFileName.lastIndexOf('.') + 1); const fileExtIndex = ["jsonata", "json", "js"].indexOf(ext); if (fileExtIndex === -1) continue; envNodeResolutionPromises.push((async () => { const absoluteFilePath = path.resolve(directories.envNodesDir, envNodeFileName); let result = null; try { switch (fileExtIndex) { case 0: { // jsonata const fileContents = await fs.readFile(absoluteFilePath, 'UTF-8'); result = jsonata(fileContents).evaluate({ require: require, basePath: directories.basePath }); break } case 1: { const fileContents = await fs.readFile(absoluteFilePath, 'UTF-8'); result = JSON.parse(fileContents); break } case 2: { const jsFile = require(absoluteFilePath); if (isObject(jsFile)) { result = jsFile; } else if (typeof jsFile === 'function') { const returnedVal = jsFile(RED); if (returnedVal instanceof Promise) { result = await returnedVal; } else { result = returnedVal; } } break } } } catch (e) { nodeLogger.error('JSONata parsing failed for env nodes:\n', e); } return result; })()); } } catch (e) { } const results = await Promise.all(envNodeResolutionPromises); results.forEach(result => { Object.assign(directories.nodesEnvConfig, result) }); await fs.ensureDir(directories.flowsDir); await fs.ensureDir(directories.subflowsDir); } async function readAllFlowFileNames(type = 'subflow') { const filesUnderFolder = (await fs.readdir(type === 'subflow' ? directories.subflowsDir : directories.flowsDir)); const relevantItems = filesUnderFolder.filter(item => { const ext = path.extname(item).toLowerCase(); return ext === '.json' || ext === '.yaml'; }); return relevantItems; } async function readAllFlowFileNamesWithoutExt(type) { return (await readAllFlowFileNames(type)).map(file => file.substring(0, file.lastIndexOf('.'))); } async function loadFlows(flowsToShow = null, getMode = false) { const retVal = { flows: [], rev: null, flowVersions: {}, subflowVersions: {}, globalVersion: null, loadedFlowAndSubflowNames: {} } let flowJsonSum = { tabs: [], subflows: [], groups: [], global: [], groupedNodes: [], nodes: [], byNodeId: {} }; let items = await readAllFlowFileNames('flow'); if (flowsToShow === null) { flowsToShow = flowManagerSettings.filter; } nodeLogger.info('Flow filtering state:', flowsToShow); // read flows for (const algoFlowFileName of items) { try { const itemWithoutExt = algoFlowFileName.substring(0, algoFlowFileName.lastIndexOf('.')); if (!algoFlowFileName.toLowerCase().match(/.*\.(json)|(yaml)$/g)) continue; const flowJsonFile = await readFlowFile(path.join(directories.flowsDir, algoFlowFileName)); retVal.flowVersions[itemWithoutExt] = { rev: calculateRevision(flowJsonFile.str), mtime: flowJsonFile.mtime }; // Ignore irrelevant flows (filter flows functionality) if (flowsToShow && flowsToShow.length && flowsToShow.indexOf(itemWithoutExt) === -1) { continue; } // find tab node let tab = null; for (let i = flowJsonFile.obj.length - 1; i >= 0; i--) { const node = flowJsonFile.obj[i]; flowJsonSum.byNodeId[node.id] = node; if (node.type === 'tab') { flowJsonSum.tabs.push(node); flowJsonFile.obj.splice(i, 1); tab = node; } else if (node.type === 'group') { flowJsonFile.obj.splice(i, 1); flowJsonSum.groups.push(node); } else if (typeof node.g === "string") { flowJsonFile.obj.splice(i, 1); flowJsonSum.groupedNodes.push(node); } } if (!tab) { throw new Error("Could not find tab node in flow file") } Array.prototype.push.apply(flowJsonSum.nodes, flowJsonFile.obj); retVal.loadedFlowAndSubflowNames[tab.id] = {type: tab.type, name: tab.label}; } catch (e) { nodeLogger.error('Could not load flow ' + algoFlowFileName + '\r\n' + e.stack || e); } } // read subflows const subflowItems = (await fs.readdir(directories.subflowsDir)).filter(item => { const itemLC = item.toLowerCase(); return itemLC.endsWith('.yaml') || itemLC.endsWith('.json'); }); for (const subflowFileName of subflowItems) { try { const flowJsonFile = await readFlowFile(path.join(directories.subflowsDir, subflowFileName)); // find subflow node let subflowNode = false; for (let i = flowJsonFile.obj.length - 1; i >= 0; i--) { const node = flowJsonFile.obj[i]; flowJsonSum.byNodeId[node.id] = node; if (node.type === 'subflow') { flowJsonSum.subflows.push(node); flowJsonFile.obj.splice(i, 1); subflowNode = node; } else if (node.type === 'group') { flowJsonFile.obj.splice(i, 1); flowJsonSum.groups.push(node); } else if (typeof node.g === "string") { flowJsonFile.obj.splice(i, 1); flowJsonSum.groupedNodes.push(node); } } if (!subflowNode) { throw new Error("Could not find subflow node in flow file") } Array.prototype.push.apply(flowJsonSum.nodes, flowJsonFile.obj); const itemWithoutExt = subflowFileName.substring(0, subflowFileName.lastIndexOf('.')); retVal.subflowVersions[itemWithoutExt] = { rev: calculateRevision(flowJsonFile.str), mtime: flowJsonFile.mtime }; retVal.loadedFlowAndSubflowNames[subflowNode.id] = {type: subflowNode.type, name: subflowNode.name}; } catch (e) { nodeLogger.error('Could not load subflow ' + subflowFileName + '\r\n' + e.stack || e); } } { // read config nodes let configFlowFile; try { configFlowFile = await readConfigFlowFile(); } catch (e) { await writeFlowFile(directories.configNodesFilePathWithoutExtension + '.' + flowManagerSettings.fileFormat, []); configFlowFile = await readConfigFlowFile(); } retVal.globalVersion = { rev: calculateRevision(configFlowFile.str), mtime: configFlowFile.mtime }; for (const node of configFlowFile.obj) { flowJsonSum.byNodeId[node.id] = node; } Array.prototype.push.apply(flowJsonSum.global, configFlowFile.obj); } let orderedNodes = null; try { const unorderedNodesLeft = new Set(Object.keys(flowJsonSum.byNodeId)); const nodesOrderArray = await fs.readJson(directories.nodesOrderFilePath); orderedNodes = [] for (const nodeId of nodesOrderArray) { const theNode = flowJsonSum.byNodeId[nodeId]; if (theNode) { orderedNodes.push(theNode); unorderedNodesLeft.delete(nodeId); } } if (unorderedNodesLeft.size > 0) { orderedNodes = null; } } catch (e) { } if (!orderedNodes) { // No ordering exists, at least come up with a similar order orderedNodes = [...flowJsonSum.tabs, ...flowJsonSum.subflows, ...flowJsonSum.groups, ...flowJsonSum.global, ...flowJsonSum.groupedNodes, ...flowJsonSum.nodes]; } retVal.flows = orderedNodes; const nodesEnvConfig = directories.nodesEnvConfig; // If we have node config modifications for this ENV, iterate and apply node updates from ENV config. if (nodesEnvConfig && Object.keys(nodesEnvConfig).length) { for (const node of retVal.flows) { // If no patch exists for this id if (!node) continue; const foundNodeEnvConfig = getNodesEnvConfigForNode(node); if (foundNodeEnvConfig) { Object.assign(node, foundNodeEnvConfig); } } } if (!getMode) { nodeLogger.info('Loading flows:', items); nodeLogger.info('Loading subflows:', subflowItems); try { retVal.rev = await PRIVATERED.nodes.setFlows(retVal.flows, null, 'flows'); revisions.byFlowName = retVal.flowVersions; revisions.bySubflowName = retVal.subflowVersions; revisions.global = retVal.globalVersion; nodeLogger.info('Finished setting node-red nodes successfully.'); } catch (e) { nodeLogger.error('Failed setting node-red nodes\r\n' + e.stack || e); } } return retVal; } async function checkIfMigrationIsRequried() { try { // Check if we need to migrate from "fat" flow json file to managed mode. if ((await fs.readdir(directories.flowsDir)).length === 0 && (await fs.readdir(directories.subflowsDir)).length === 0 && await fs.exists(directories.flowFile)) { nodeLogger.info('First boot with sensecraft-application detected, starting migration process...'); return true; } } catch (e) { } return false; } async function startFlowManager() { await refreshDirectories(); if (await checkIfMigrationIsRequried()) { const masterFlowFile = await fs.readJson(directories.flowFile); const flowNodes = {}; const globalConfigNodes = [], simpleNodes = []; for (const node of masterFlowFile) { if (node.type === 'tab' || node.type === 'subflow') flowNodes[node.id] = [node]; else if (!node.z || node.z.length === 0) globalConfigNodes.push(node); else simpleNodes.push(node); } for (const node of simpleNodes) { if (flowNodes[node.z]) flowNodes[node.z].push(node); } // finally write files const fileWritePromises = [writeFlowFile(directories.configNodesFilePath, globalConfigNodes)]; for (const flowId of Object.keys(flowNodes)) { const nodesInFlow = flowNodes[flowId]; const topNode = nodesInFlow[0]; const flowName = topNode.label || topNode.name; // label on tabs , const destinationFile = path.resolve(directories.basePath, topNode.type === 'tab' ? 'flows' : 'subflows', encodeFileName(flowName) + '.' + flowManagerSettings.fileFormat); fileWritePromises.push(writeFlowFile(destinationFile, nodesInFlow)); } await Promise.all(fileWritePromises); nodeLogger.info('sensecraft-application migration complete.'); } } await startFlowManager(); if (RED.settings.editorTheme.projects.enabled) { let projFile = await readProject(); if (!projFile) return; // no cfg file, do nothing. fs.watch(projFile.path, debounce(async () => { const newProjFile = await readProject(); if (!projFile) return; // no cfg, do nothing if (projFile?.activeProject != newProjFile?.activeProject) { projFile = newProjFile; await startFlowManager(); } }, 500)); } RED.httpAdmin.get('/' + nodeName + '/flow-names', RED.auth.needsPermission("flows.read"), async function (req, res) { try { let flowFiles = await fs.readdir(path.join(directories.basePath, "flows")); flowFiles = flowFiles.filter(file => file.toLowerCase().match(/.*\.(json)|(yaml)$/g)); res.send(flowFiles); } catch (e) { res.status(404).send(); } }); async function getFlowState(type, flowName) { let flowFile try { flowFile = await readFlowByNameAndType(type, flowName, true); } catch (e) { return null } const retVal = {}; let lastLoadedFlowVersionInfo; if (type === 'flow') { lastLoadedFlowVersionInfo = revisions.byFlowName[flowName]; // If flow was not deployed either "on-demend" or passed filtering, we determine that it was not deployed. const wasLoadedOnDemand = type === 'flow' && onDemandFlowsManager.onDemandFlowsSet.has(flowName) retVal.deployed = deployedFlowNames.has(flowName); retVal.onDemand = wasLoadedOnDemand; } else if (type === 'subflow') { lastLoadedFlowVersionInfo = revisions.bySubflowName[flowName]; retVal.deployed = true; } else if (type === 'global') { lastLoadedFlowVersionInfo = revisions.global; retVal.deployed = true; } else { return null; } let strOld = flowFile.str let strNew = JSON.parse(flowFile.str) if (Array.isArray(strNew)) { strNew = strNew.filter(item => item.name !== "sensecraft-libs") strOld = JSON.stringify(strNew) } const fileRev = calculateRevision(strOld); retVal.rev = fileRev retVal.mtime = flowFile.mtime; retVal.hasUpdate = !lastLoadedFlowVersionInfo || fileRev !== lastLoadedFlowVersionInfo.rev; if (retVal.hasUpdate && lastLoadedFlowVersionInfo) { retVal.oldRev = lastLoadedFlowVersionInfo.rev; retVal.oldMtime = lastLoadedFlowVersionInfo.mtime } return retVal; } async function getFlowStateForType(type) { if (type === 'flow' || type === 'subflow') { const retVal = {}; const flowNames = await readAllFlowFileNamesWithoutExt(type); for (const flowName of flowNames) { retVal[flowName] = await getFlowState(type, flowName); } return retVal; } else if (type === 'global') { return await getFlowState(type); } } RED.httpAdmin.get('/' + nodeName + '/states/:type/:flowName?', RED.auth.needsPermission("flows.read"), async function (req, res) { try { if (['flow', 'subflow', 'global'].indexOf(req.params.type) !== -1) { let retVal; if (req.params.flowName) { retVal = await getFlowState(req.params.type, req.params.flowName); } else { retVal = await getFlowStateForType(req.params.type); } if (retVal) { return res.send(retVal); } else { return res.status(404).send({error: `No such ${req.params.type} file`}); } } else { return res.status(404).send({error: `Unrecognised flow type: ${req.params.type}`}); } } catch (e) { return res.status(404).send(); } }); RED.httpAdmin.get('/' + nodeName + '/states', RED.auth.needsPermission("flows.read"), async function (req, res) { try { const retVal = {}; for (const flowType of ['flow', 'subflow', 'global']) { retVal[flowType] = await getFlowStateForType(flowType); } return res.send(retVal); } catch (e) { return res.status(404).send(); } }); function isObject(value) { return value && typeof value === 'object' && value.constructor === Object; } RED.httpAdmin.all('/' + nodeName + '/remotes/:remoteName/**', [RED.auth.needsPermission("flows.read"), RED.auth.needsPermission("flows.write"), bodyParser.text({ limit: "50mb", type: '*/*' })], async function (req, res) { try { const remote = flowManagerSettings.remoteDeploy.remotes.find(remote => remote.name === req.params.remoteName); if (!remote) { throw new Error("Remote not found") } const toCutAfter = `/sensecraft-application/remotes/${req.params.remoteName}`; const appendUrl = req.url.substring(req.url.indexOf(toCutAfter) + toCutAfter.length); const nrAddressWithSlash = remote.nrAddress.endsWith('/') ? remote.nrAddress : (remote.nrAddress + '/'); const remotePathUrl = nrAddressWithSlash + 'sensecraft-application' + appendUrl; const remoteResponse = await axios({ method: req.method, url: remotePathUrl.toString(), headers: req.headers, data: req.body, transformResponse: [] // Disabling force json parsing }) if (req.headers.accept === 'text/plain') { res = res.contentType('text/plain'); } else { res = res.contentType(flowManagerSettings.fileFormat.toLowerCase() === 'yaml' ? 'application/x-yaml' : 'application/json'); } return res.status(remoteResponse.status).send(remoteResponse.data); } catch (e) { try { return res.status(e.response.status).send(e.response.data); } catch (e) { return res.status(400).send({"error": "Unknown communication failure"}); } } }); RED.httpAdmin.post('/' + nodeName + '/states', RED.auth.needsPermission("flows.write"), async function loadFlowsOnDemand(req, res) { try { if (!isObject(req.body) || !req.body.action) return res.status(400).send({"error": 'missing "action" key'}); const allFlows = await readAllFlowFileNamesWithoutExt('flow'); const filterChosenFlows = (!flowManagerSettings.filter || flowManagerSettings.filter.length === 0) ? allFlows : flowManagerSettings.filter; // calculate which flows to load (null means all flows, no filtering) let flowsToShow; const requestedToLoadAll = req.body.action === 'loadAll'; const reloadOnly = req.body.action === 'reloadOnly'; const removeOndemand = req.body.action === 'removeOndemand' && req.body.flows; const addOndemand = req.body.action === 'addOndemand' && req.body.flows; const replaceOndemand = req.body.action === 'replaceOndemand' && req.body.flows; if (requestedToLoadAll) { flowsToShow = allFlows; } else if (reloadOnly) { flowsToShow = [...filterChosenFlows, ...Array.from(onDemandFlowsManager.onDemandFlowsSet)]; // 发送消息给前端,让重启客户端 if (update_flag) { // 发送消息 console.log("node red need to restart") RED.comms.publish('sensecraft-application/sensecraft-application-envnodes-npm-update', {npm_update: true}); // 重置状态 update_flag = false } alertStatus = false } else if (removeOndemand) { const newSet = new Set(onDemandFlowsManager.onDemandFlowsSet); for (const undeployFlow of removeOndemand) { newSet.delete(undeployFlow); } flowsToShow = [...filterChosenFlows, ...Array.from(newSet)]; } else if (addOndemand || replaceOndemand) { flowsToShow = Array.from(new Set([...filterChosenFlows, ...req.body.flows, ...(addOndemand ? Array.from(onDemandFlowsManager.onDemandFlowsSet) : [])])); } else { return res.status(400).send({"error": 'malformed action request, could be missing "flows" array'}) } // calculate which of the loaded flows are "on-demand" flows await onDemandFlowsManager.updateOnDemandFlowsSet(new Set(flowsToShow)); await loadFlows(flowsToShow); res.send({"status": "ok"}); } catch (e) { res.status(404).send({error: e.message}); } }); RED.httpAdmin.get('/' + nodeName + '/cfg', RED.auth.needsPermission("flows.read"), async function (req, res) { res.send(await fs.readJson(directories.flowManagerCfg)); }); RED.httpAdmin.get('/' + nodeName + '/filter-flows', RED.auth.needsPermission("flows.read"), async function (req, res) { res.send(flowManagerSettings.filter); }); RED.httpAdmin.put('/' + nodeName + '/filter-flows', RED.auth.needsPermission("flows.read"), async function (req, res) { const filterArray = req.body; try { flowManagerSettings.filter = filterArray; await onDemandFlowsManager.updateOnDemandFlowsSet(new Set()); await fs.outputFile(directories.flowManagerCfg, stringifyFormattedFileJson(flowManagerSettings)); await loadFlows(flowManagerSettings.filter, false); res.send({}); } catch (e) { res.status(404).send(); } }); async function handleFlowFile(req, res) { try { const type = req.params.type; const flowTypes = ['flows', 'subflows', 'global']; let indexOfFlowType = flowTypes.indexOf(type); if (indexOfFlowType === -1) { // try singular indexOfFlowType = ['flow', 'subflow'].indexOf(type); } let pathToWrite = indexOfFlowType === 0 ? directories.flowsDir : indexOfFlowType === 1 ? directories.subflowsDir : indexOfFlowType === 2 ? (directories.configNodesFilePathWithoutExtension + '.' + flowManagerSettings.fileFormat) : ''; let fullPath; if (indexOfFlowType === 2) { fullPath = pathToWrite; } else { const fileName = req.params.fileName; if (!fileName || fileName.indexOf('..') !== -1 || fileName.indexOf('/') !== -1 || fileName.indexOf('\\') !== -1) { return res.status(400).send({error: "Flow file illegal"}); } fullPath = path.resolve(pathToWrite, fileName + '.' + flowManagerSettings.fileFormat); } if (indexOfFlowType === -1) { return res.status(400).send({error: "Unrecognized flow type, please use one of " + JSON.stringify(flowTypes).split('"').join("")}); } else if (!fullPath) { return res.status(400).send({error: "malformed flow filename"}); } if (req.method === 'GET') { if (!await fs.pathExists(fullPath)) { return res.status(404).send({error: "Flow file not found"}); } let str = (await readFlowFile(fullPath, true)).str; const libs = (await readFlowFile(directories.basePath + "/sensecraft-libs/sensecraft-libs.json", true)).str if (libs.length > 0) { let jsonSrt = JSON.parse(str); if (jsonSrt.length > 0) { jsonSrt.push(JSON.parse(libs)); str = JSON.stringify(jsonSrt); } } if (req.headers.accept === 'text/plain') { return res.contentType('text/plain').send(str); } else { return res.contentType(flowManagerSettings.fileFormat.toLowerCase() === 'yaml' ? 'application/x-yaml' : 'application/json').send(str); } } else if (req.method === 'POST') { if (Array.isArray(req.body)) { let libs = req.body.filter((item) => { return item.type === "comment" && item.name === "sensecraft-libs" }) if (libs.length > 0) { await iinstallModeList(libs[0].info) } } await writeFlowFile(fullPath, req.body); const mtime = req.query.mtime; const atime = req.query.atime; if (mtime || atime) { await fs.utimes(fullPath, atime ? new Date(atime) : new Date(), mtime ? new Date(mtime) : new Date()); } } else if (req.method === 'DELETE') { if (!await fs.pathExists(fullPath)) { return res.status(404).send({error: "Delete failed, Flow file not found"}); } await fs.remove(fullPath) } else { return res.status(400).send({error: "Unknown method"}); } res.send({status: "ok"}); } catch (e) { console.log(e); res.status(400).send({error: e.message}); } } RED.httpAdmin.delete('/' + nodeName + '/flow-files/:type/:fileName?', RED.auth.needsPermission("flows.write"), handleFlowFile); RED.httpAdmin.post('/' + nodeName + '/flow-files/:type/:fileName?', [RED.auth.needsPermission("flows.write"), bodyParser.text({ limit: "50mb", type: '*/*' })], handleFlowFile); RED.httpAdmin.get('/' + nodeName + '/flow-files/:type/:fileName?', RED.auth.needsPermission("flows.read"), handleFlowFile); // get flow path RED.httpAdmin.get('/' + nodeName + '/status', RED.auth.needsPermission("flows.read"), async function (req, res) { res.send({"path": directories.basePath}); }); // 从flow 解析成state RED.httpAdmin.post('/' + nodeName + '/applicaiton/sate/:applicaitonId', RED.auth.needsPermission("flows.read"), async function (req, res) { const applicaitonId = req.params.applicaitonId if (!applicaitonId) { res.status(400).send({error: "applicaiton id not found"}); } const input = req.body if (input.length === 0 || !input) { res.status(400).send({error: "Flow file not found"}); } let libsMod = input.filter((item) => { return item.name === "sensecraft-libs" }) let mtime = new Date().toISOString() if (libsMod[0]?.id) { mtime = changeTime(libsMod[0].id) } const output = { flow: {}, subflow: {}, global: {} }; if (libsMod && libsMod.length > 0) { const infoParse = JSON.parse(libsMod[0].info) if (infoParse && infoParse?.global) { const globalInfo = infoParse?.global output.global = { deployed: true, rev: calculateRevision(JSON.stringify(globalInfo)), mtime: mtime, hasUpdate: false