diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index 96b3ddd7c..d50a2b029 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -60,7 +60,7 @@ "Sunday" ], "on": "on", - "onstart": "Inject once at start?", + "onstart": "Inject once at start ?", "tip": "Note: \"interval between times\" and \"at a specific time\" will use cron.
See info box for details.", "success": "Successfully injected: __label__", "errors": { @@ -96,7 +96,7 @@ "placeholder": { "extraparams": "extra input parameters" }, - "spawn": "Use spawn() instead of exec()?", + "spawn": "Use spawn() instead of exec() ?", "tip": "Tip: spawn expects only one command word - and appended args to be comma separated." }, "function": { @@ -210,7 +210,7 @@ "return": "Return" }, "setby": "- set by msg.method -", - "basicauth": "Use basic authentication?", + "basicauth": "Use basic authentication ?", "utf8": "a UTF-8 string", "binary": "a binary buffer", "json": "a parsed JSON object", @@ -269,8 +269,8 @@ "host": "at host", "payload": "payload(s)", "delimited": "delimited by", - "close-connection": "Close connection after each message is sent?", - "decode-base64": "Decode Base64 message?", + "close-connection": "Close connection after each message is sent ?", + "decode-base64": "Decode Base64 message ?", "server": "Server", "return": "Return" }, @@ -326,7 +326,7 @@ "send": "Send a", "toport": "to port", "address": "Address", - "decode-base64": "Decode Base64 encoded payload?" + "decode-base64": "Decode Base64 encoded payload ?" }, "placeholder": { "interface": "(optional) ip address of eth0", @@ -418,7 +418,7 @@ "resultrange": "to the result range", "from": "from", "to": "to", - "roundresult": "Round result to the nearest integer?" + "roundresult": "Round result to the nearest integer ?" }, "placeholder": { "min": "e.g. 0", @@ -509,9 +509,9 @@ "gpiopin": "GPIO Pin", "selectpin": "select pin", "resistor": "Resistor?", - "readinitial": "Read initial state of pin on deploy/restart?", + "readinitial": "Read initial state of pin on deploy/restart ?", "type": "Type", - "initpin": "Initialise pin state?", + "initpin": "Initialise pin state ?", "button": "Button", "pimouse": "Pi Mouse", "left": "Left", @@ -571,7 +571,7 @@ "tail": { "label": { "filename": "Filename", - "splitlines": "Split lines on \\n?" + "splitlines": "Split lines on \\n ?" }, "errors": { "windowsnotsupport": "Not currently supported on Windows." @@ -581,7 +581,8 @@ "label": { "filename": "Filename", "action": "Action", - "addnewline": "Add newline (\\n) to each payload?", + "addnewline": "Add newline (\\n) to each payload ?", + "createdir": "Create directory if it doesn't exist ?", "outputas": "Output as", "filelabel": "file", "deletelabel": "delete __file__" @@ -605,7 +606,8 @@ "invaliddelete": "Warning: Invalid delete. Please use specific delete option in config dialog.", "deletefail": "failed to delete file: __error__", "writefail": "failed to write to file: __error__", - "appendfail": "failed to append to file: __error__" + "appendfail": "failed to append to file: __error__", + "createfail": "failed to create file: __error__" } } } diff --git a/nodes/core/storage/50-file.html b/nodes/core/storage/50-file.html index 3cb12dd96..3de507688 100644 --- a/nodes/core/storage/50-file.html +++ b/nodes/core/storage/50-file.html @@ -29,9 +29,14 @@
- +
+
+ + + +
@@ -76,6 +81,7 @@ name: {value:""}, filename: {value:""}, appendNewline: {value:true}, + createDir: {value:false}, overwriteFile: {value:"false"} }, color:"BurlyWood", diff --git a/nodes/core/storage/50-file.js b/nodes/core/storage/50-file.js index 910c3b4ea..4f4c82d74 100644 --- a/nodes/core/storage/50-file.js +++ b/nodes/core/storage/50-file.js @@ -1,5 +1,5 @@ /** - * Copyright 2013, 2014 IBM Corp. + * Copyright 2013, 2015 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,37 +17,48 @@ module.exports = function(RED) { "use strict"; var fs = require("fs-extra"); + var os = require("os"); function FileNode(n) { RED.nodes.createNode(this,n); this.filename = n.filename; this.appendNewline = n.appendNewline; this.overwriteFile = n.overwriteFile.toString(); + this.createDir = n.createDir || false; var node = this; + this.on("input",function(msg) { - var filename = this.filename || msg.filename || ""; - if (msg.filename && n.filename && (n.filename !== msg.filename)) { - node.warn(RED._("common.errors.nooverride")); - } - if (!this.filename) { + var filename = node.filename || msg.filename || ""; + if (!node.filename) { node.status({fill:"grey",shape:"dot",text:filename}); } if (filename === "") { node.warn(RED._("file.errors.nofilename")); - } else if (msg.hasOwnProperty('delete')) { // remove warning at some point in future - node.warn(RED._("file.errors.invaliddelete")); - } else if (msg.payload && (typeof msg.payload != "undefined")) { + } else if (msg.hasOwnProperty("payload") && (typeof msg.payload !== "undefined")) { var data = msg.payload; - if ((typeof data === "object")&&(!Buffer.isBuffer(data))) { + if ((typeof data === "object") && (!Buffer.isBuffer(data))) { data = JSON.stringify(data); } if (typeof data === "boolean") { data = data.toString(); } - if ((this.appendNewline)&&(!Buffer.isBuffer(data))) { data += "\n"; } + if (typeof data === "number") { data = data.toString(); } + if ((this.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL; } if (this.overwriteFile === "true") { // using "binary" not {encoding:"binary"} to be 0.8 compatible for a while - //fs.writeFile(filename, data, {encoding:"binary"}, function (err) { fs.writeFile(filename, data, "binary", function (err) { - if (err) { node.error(RED._("file.errors.writefail",{error:err.toString()}),msg); } + //fs.writeFile(filename, data, {encoding:"binary"}, function (err) { + if (err) { + if ((err.code === "ENOENT") && node.createDir) { + fs.ensureFile(filename, function (err) { + if (err) { node.error(RED._("file.errors.createfail",{error:err.toString()}),msg); } + else { + fs.writeFile(filename, data, "binary", function (err) { + if (err) { node.error(RED._("file.errors.writefail",{error:err.toString()}),msg); } + }); + } + }); + } + else { node.error(RED._("file.errors.writefail",{error:err.toString()}),msg); } + } else if (RED.settings.verbose) { node.log(RED._("file.status.wrotefile",{file:filename})); } }); } @@ -59,9 +70,21 @@ module.exports = function(RED) { } else { // using "binary" not {encoding:"binary"} to be 0.8 compatible for a while longer - //fs.appendFile(filename, data, {encoding:"binary"}, function (err) { fs.appendFile(filename, data, "binary", function (err) { - if (err) { node.error(RED._("file.errors.appendfail",{error:err.toString()}),msg); } + //fs.appendFile(filename, data, {encoding:"binary"}, function (err) { + if (err) { + if ((err.code === "ENOENT") && node.createDir) { + fs.ensureFile(filename, function (err) { + if (err) { node.error(RED._("file.errors.createfail",{error:err.toString()}),msg); } + else { + fs.appendFile(filename, data, "binary", function (err) { + if (err) { node.error(RED._("file.errors.appendfail",{error:err.toString()}),msg); } + }); + } + }); + } + else { node.error(RED._("file.errors.appendfail",{error:err.toString()}),msg); } + } else if (RED.settings.verbose) { node.log(RED._("file.status.appendedfile",{file:filename})); } }); } @@ -82,11 +105,8 @@ module.exports = function(RED) { options['encoding'] = this.format; } this.on("input",function(msg) { - var filename = this.filename || msg.filename || ""; - if (msg.filename && n.filename && (n.filename !== msg.filename)) { - node.warn(RED._("file.errors.nooverride")); - } - if (!this.filename) { + var filename = node.filename || msg.filename || ""; + if (!node.filename) { node.status({fill:"grey",shape:"dot",text:filename}); } if (filename === "") { diff --git a/test/nodes/core/storage/50-file_spec.js b/test/nodes/core/storage/50-file_spec.js index 9b1b34860..ae1bff84d 100644 --- a/test/nodes/core/storage/50-file_spec.js +++ b/test/nodes/core/storage/50-file_spec.js @@ -240,6 +240,109 @@ describe('file Nodes', function() { }); }); + it('should fail to create a new directory if not asked to do so (append)', function(done) { + // Stub file write so we can make writes fail + var fileToTest2 = path.join(resourcesDir,"a","50-file-test-file.txt"); + //var spy = sinon.stub(fs, 'appendFile', function(arg,arg2,arg3,arg4){ arg4(new Error("Stub error message")); }); + + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest2, "appendNewline":true, "overwriteFile":false}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileNode1"); + setTimeout(function() { + try { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file"; + }); + //console.log(logEvents); + logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.startWith("file.errors.appendfail"); + done(); + } + catch(e) { done(e); } + //finally { fs.appendFile.restore(); } + },wait); + n1.receive({payload:"test2"}); + }); + }); + + it('should try to create a new directory if asked to do so (append)', function(done) { + // Stub file write so we can make writes fail + var fileToTest2 = path.join(resourcesDir,"a","50-file-test-file.txt"); + var spy = sinon.stub(fs, "ensureFile", function(arg1,arg2,arg3,arg4) { arg2(null); }); + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest2, "appendNewline":true, "overwriteFile":false, "createDir":true}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileNode1"); + setTimeout(function() { + try { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file"; + }); + //console.log(logEvents); + logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.startWith("file.errors.appendfail"); + done(); + } + catch(e) { done(e); } + finally { fs.ensureFile.restore(); } + },wait); + n1.receive({payload:"test2"}); + }); + }); + + it('should fail to create a new directory if not asked to do so (overwrite)', function(done) { + // Stub file write so we can make writes fail + var fileToTest2 = path.join(resourcesDir,"a","50-file-test-file.txt"); + //var spy = sinon.stub(fs, 'appendFile', function(arg,arg2,arg3,arg4){ arg4(new Error("Stub error message")); }); + + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest2, "appendNewline":false, "overwriteFile":true}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileNode1"); + setTimeout(function() { + try { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file"; + }); + //console.log(logEvents); + logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.startWith("file.errors.writefail"); + done(); + } + catch(e) { done(e); } + //finally { fs.appendFile.restore(); } + },wait); + n1.receive({payload:"test2"}); + }); + }); + + it('should try to create a new directory if asked to do so (overwrite)', function(done) { + // Stub file write so we can make writes fail + var fileToTest2 = path.join(resourcesDir,"a","50-file-test-file.txt"); + var spy = sinon.stub(fs, "ensureFile", function(arg1,arg2,arg3,arg4){ arg2(null); }); + + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest2, "appendNewline":true, "overwriteFile":true, "createDir":true}]; + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileNode1"); + setTimeout(function() { + try { + var logEvents = helper.log().args.filter(function(evt) { + return evt[0].type == "file"; + }); + //console.log(logEvents); + logEvents.should.have.length(1); + logEvents[0][0].should.have.a.property('msg'); + logEvents[0][0].msg.toString().should.startWith("file.errors.writefail"); + done(); + } + catch(e) { done(e); } + finally { fs.ensureFile.restore(); } + },wait); + n1.receive({payload:"test2"}); + }); + }); + }); @@ -303,27 +406,28 @@ describe('file Nodes', function() { //}); //}); - it('should warn if msg.props try to overide', function(done) { - var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "filename":fileToTest, "format":"", wires:[["n2"]]}, - {id:"n2", type:"helper"}]; - helper.load(fileNode, flow, function() { - var n1 = helper.getNode("fileInNode1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - msg.should.have.property('payload'); - msg.payload.should.have.length(39).and.be.a.Buffer; - msg.payload.toString().should.equal("File message line 1\File message line 2\n"); - var logEvents = helper.log().args.filter(function(evt) { - return evt[0].type == "file in"; - }); - logEvents.should.have.length(1); - logEvents[0][0].should.have.a.property('msg'); - logEvents[0][0].msg.toString().should.startWith("file.errors.nooverride"); - done(); - }); - n1.receive({payload:"",filename:"foo.txt"}); - }); - }); +// Commented out as we no longer need to warn of the very old deprecated behaviour + //it('should warn if msg.props try to overide', function(done) { + //var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "filename":fileToTest, "format":"", wires:[["n2"]]}, + //{id:"n2", type:"helper"}]; + //helper.load(fileNode, flow, function() { + //var n1 = helper.getNode("fileInNode1"); + //var n2 = helper.getNode("n2"); + //n2.on("input", function(msg) { + //msg.should.have.property('payload'); + //msg.payload.should.have.length(39).and.be.a.Buffer; + //msg.payload.toString().should.equal("File message line 1\File message line 2\n"); + //var logEvents = helper.log().args.filter(function(evt) { + //return evt[0].type == "file in"; + //}); + //logEvents.should.have.length(1); + //logEvents[0][0].should.have.a.property('msg'); + //logEvents[0][0].msg.toString().should.startWith("file.errors.nooverride"); + //done(); + //}); + //n1.receive({payload:"",filename:"foo.txt"}); + //}); + //}); it('should warn if no filename set', function(done) { var flow = [{id:"fileInNode1", type:"file in", name: "fileInNode", "format":""}];