From aac2a8f830c964d6b496fe79e8c9f24948de1e8d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 12 Apr 2021 18:00:58 +0100 Subject: [PATCH] File node: Add fileWorkingDirectory to customise how relative paths are resolved --- .../@node-red/nodes/core/storage/10-file.js | 22 ++++--- packages/node_modules/node-red/settings.js | 4 ++ test/nodes/core/storage/10-file_spec.js | 59 ++++++++++++++++++- 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/storage/10-file.js b/packages/node_modules/@node-red/nodes/core/storage/10-file.js index 54ce55764..a58ae6c4c 100644 --- a/packages/node_modules/@node-red/nodes/core/storage/10-file.js +++ b/packages/node_modules/@node-red/nodes/core/storage/10-file.js @@ -51,6 +51,10 @@ module.exports = function(RED) { function processMsg(msg,nodeSend, done) { var filename = node.filename || msg.filename || ""; + var fullFilename = filename; + if (filename && RED.settings.fileWorkingDirectory && !path.isAbsolute(filename)) { + fullFilename = path.resolve(path.join(RED.settings.fileWorkingDirectory,filename)); + } if ((!node.filename) && (!node.tout)) { node.tout = setTimeout(function() { node.status({fill:"grey",shape:"dot",text:filename}); @@ -62,7 +66,7 @@ module.exports = function(RED) { node.warn(RED._("file.errors.nofilename")); done(); } else if (node.overwriteFile === "delete") { - fs.unlink(filename, function (err) { + fs.unlink(fullFilename, function (err) { if (err) { node.error(RED._("file.errors.deletefail",{error:err.toString()}),msg); } else { @@ -74,7 +78,7 @@ module.exports = function(RED) { done(); }); } else if (msg.hasOwnProperty("payload") && (typeof msg.payload !== "undefined")) { - var dir = path.dirname(filename); + var dir = path.dirname(fullFilename); if (node.createDir) { try { fs.ensureDirSync(dir); @@ -94,7 +98,7 @@ module.exports = function(RED) { if ((node.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; } var buf = encode(data, node.encoding); if (node.overwriteFile === "true") { - var wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'w', autoClose:true }); + var wstream = fs.createWriteStream(fullFilename, { encoding:'binary', flags:'w', autoClose:true }); node.wstream = wstream; wstream.on("error", function(err) { node.error(RED._("file.errors.writefail",{error:err.toString()}),msg); @@ -116,7 +120,7 @@ module.exports = function(RED) { // of the file. Check the file hasn't been deleted // or deleted and recreated. try { - var stat = fs.statSync(filename); + var stat = fs.statSync(fullFilename); // File exists - check the inode matches if (stat.ino !== node.wstreamIno) { // The file has been recreated. Close the current @@ -135,10 +139,10 @@ module.exports = function(RED) { } } if (recreateStream) { - node.wstream = fs.createWriteStream(filename, { encoding:'binary', flags:'a', autoClose:true }); + node.wstream = fs.createWriteStream(fullFilename, { encoding:'binary', flags:'a', autoClose:true }); node.wstream.on("open", function(fd) { try { - var stat = fs.statSync(filename); + var stat = fs.statSync(fullFilename); node.wstreamIno = stat.ino; } catch(err) { } @@ -258,6 +262,10 @@ module.exports = function(RED) { this.on("input",function(msg, nodeSend, nodeDone) { var filename = (node.filename || msg.filename || "").replace(/\t|\r|\n/g,''); + var fullFilename = filename; + if (filename && RED.settings.fileWorkingDirectory && !path.isAbsolute(filename)) { + fullFilename = path.resolve(path.join(RED.settings.fileWorkingDirectory,filename)); + } if (!node.filename) { node.status({fill:"grey",shape:"dot",text:filename}); } @@ -279,7 +287,7 @@ module.exports = function(RED) { var hwm; var getout = false; - var rs = fs.createReadStream(filename) + var rs = fs.createReadStream(fullFilename) .on('readable', function () { var chunk; var hwm = rs._readableState.highWaterMark; diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 331654ed8..658a8fb85 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -46,6 +46,10 @@ module.exports = { // defaults to 10Mb //execMaxBufferSize: 10000000, + // The working directory to handle relative file paths from within the File nodes + // defaults to the working directory of the Node-RED process. + //fileWorkingDirectory: "", + // The maximum length, in characters, of any message sent to the debug sidebar tab debugMaxLength: 1000, diff --git a/test/nodes/core/storage/10-file_spec.js b/test/nodes/core/storage/10-file_spec.js index cb3dc5d07..2020c70b2 100644 --- a/test/nodes/core/storage/10-file_spec.js +++ b/test/nodes/core/storage/10-file_spec.js @@ -22,6 +22,7 @@ var sinon = require("sinon"); var iconv = require("iconv-lite"); var fileNode = require("nr-test-utils").require("@node-red/nodes/core/storage/10-file.js"); var helper = require("node-red-node-test-helper"); +var RED = require("nr-test-utils").require("node-red/lib/red"); describe('file Nodes', function() { @@ -41,8 +42,9 @@ describe('file Nodes', function() { describe('file out Node', function() { + var relativePathToFile = "50-file-test-file.txt"; var resourcesDir = path.join(__dirname,"..","..","..","resources"); - var fileToTest = path.join(resourcesDir,"50-file-test-file.txt"); + var fileToTest = path.join(resourcesDir,relativePathToFile); var wait = 250; beforeEach(function(done) { @@ -51,6 +53,7 @@ describe('file Nodes', function() { }); afterEach(function(done) { + delete RED.settings.fileWorkingDirectory; fs.removeSync(path.join(resourcesDir,"file-out-node")); helper.unload().then(function() { //fs.unlinkSync(fileToTest); @@ -94,6 +97,30 @@ describe('file Nodes', function() { }); }); + it('should write to a file using RED.settings.fileWorkingDirectory', function(done) { + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":relativePathToFile, "appendNewline":false, "overwriteFile":true, wires: [["helperNode1"]]}, + {id:"helperNode1", type:"helper"}]; + helper.load(fileNode, flow, function() { + RED.settings.fileWorkingDirectory = resourcesDir; + var n1 = helper.getNode("fileNode1"); + var n2 = helper.getNode("helperNode1"); + n2.on("input", function(msg) { + try { + var f = fs.readFileSync(fileToTest); + f.should.have.length(4); + fs.unlinkSync(fileToTest); + msg.should.have.property("payload", "test"); + done(); + } + catch (e) { + done(e); + } + }); + n1.receive({payload:"test"}); + }); + }); + + it('should write multi-byte string to a file', function(done) { var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":true, wires: [["helperNode1"]]}, {id:"helperNode1", type:"helper"}]; @@ -1036,9 +1063,10 @@ describe('file Nodes', function() { describe('file in Node', function() { + var relativePathToFile = "50-file-test-file.txt"; var resourcesDir = path.join(__dirname,"..","..","..","resources"); - var fileToTest = path.join(resourcesDir,"50-file-test-file.txt"); - var fileToTest2 = "\t"+path.join(resourcesDir,"50-file-test-file.txt")+"\r\n"; + var fileToTest = path.join(resourcesDir,relativePathToFile); + var fileToTest2 = "\t"+path.join(resourcesDir,relativePathToFile)+"\r\n"; var wait = 150; beforeEach(function(done) { @@ -1047,6 +1075,7 @@ describe('file Nodes', function() { }); afterEach(function(done) { + delete RED.settings.fileWorkingDirectory; helper.unload().then(function() { fs.unlinkSync(fileToTest); helper.stopServer(done); @@ -1100,6 +1129,30 @@ describe('file Nodes', function() { }); }); + + it('should read in a file using fileWorkingDirectory to set cwd', function(done) { + var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "filename":relativePathToFile, "format":"utf8", wires:[["n2"]]}, + {id:"n2", type:"helper"}]; + helper.load(fileNode, flow, function() { + RED.settings.fileWorkingDirectory = resourcesDir; + var n1 = helper.getNode("fileInNode1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + msg.payload.should.be.a.String(); + msg.payload.should.have.length(40) + msg.payload.should.equal("File message line 1\nFile message line 2\n"); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:""}); + }); + }); + + it('should read in a file ending in cr and output a utf8 string', function(done) { var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "filename":fileToTest2, "format":"utf8", wires:[["n2"]]}, {id:"n2", type:"helper"}];