@flowfuse/nr-file-nodes 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/file.js ADDED
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Copyright JS Foundation and other contributors, http://js.foundation
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ **/
16
+
17
+ module.exports = function (RED) {
18
+ 'use strict'
19
+
20
+ // Do not register nodes in runtime if settings are not provided
21
+ if (
22
+ !RED.settings.flowforge ||
23
+ !RED.settings.flowforge.projectID ||
24
+ !RED.settings.flowforge.teamID ||
25
+ !RED.settings.flowforge.fileStore ||
26
+ !RED.settings.flowforge.fileStore.url
27
+ ) {
28
+ throw new Error('FlowFuse file nodes cannot be loaded without required settings')
29
+ }
30
+ const VFS = require('./vfs')
31
+ const os = require('os')
32
+ const path = require('path')
33
+ const iconv = require('iconv-lite')
34
+
35
+ function encode (data, enc) {
36
+ if (enc !== 'none') {
37
+ return iconv.encode(data, enc)
38
+ }
39
+ return Buffer.from(data)
40
+ }
41
+
42
+ function decode (data, enc) {
43
+ if (enc !== 'none') {
44
+ return iconv.decode(data, enc)
45
+ }
46
+ return data.toString()
47
+ }
48
+
49
+ function FileNode (n) {
50
+ // Write/delete a file
51
+ RED.nodes.createNode(this, n)
52
+ this.filename = n.filename
53
+ this.filenameType = n.filenameType
54
+ this.appendNewline = n.appendNewline
55
+ this.overwriteFile = n.overwriteFile.toString()
56
+ this.createDir = n.createDir || false
57
+ this.encoding = n.encoding || 'none'
58
+ const node = this
59
+ const fs = VFS(RED)
60
+ node.wstream = null
61
+ node.msgQueue = []
62
+ node.closing = false
63
+ node.closeCallback = null
64
+
65
+ function processMsg (msg, nodeSend, done) {
66
+ let filename = node.filename || ''
67
+ // Pre V3 compatibility - if filenameType is empty, do in place upgrade
68
+ if (typeof node.filenameType === 'undefined' || node.filenameType === '') {
69
+ // existing node AND filenameType is not set - inplace (compatible) upgrade
70
+ if (filename === '') { // was using empty value to denote msg.filename
71
+ node.filename = 'filename'
72
+ node.filenameType = 'msg'
73
+ } else { // was using a static filename - set typedInput type to str
74
+ node.filenameType = 'str'
75
+ }
76
+ }
77
+
78
+ RED.util.evaluateNodeProperty(node.filename, node.filenameType, node, msg, (err, value) => {
79
+ if (err) {
80
+ node.error(err, msg)
81
+ return done()
82
+ } else {
83
+ filename = value
84
+ }
85
+ })
86
+ filename = filename || ''
87
+ msg.filename = filename
88
+ let fullFilename = filename
89
+ if (filename && RED.settings.fileWorkingDirectory && !path.isAbsolute(filename)) {
90
+ // fullFilename = path.resolve(path.join(RED.settings.fileWorkingDirectory, filename))
91
+ fullFilename = path.join(RED.settings.fileWorkingDirectory, filename)
92
+ }
93
+ if ((!node.filename) && (!node.tout)) {
94
+ node.tout = setTimeout(function () {
95
+ node.status({ fill: 'grey', shape: 'dot', text: filename })
96
+ clearTimeout(node.tout)
97
+ node.tout = null
98
+ }, 333)
99
+ }
100
+ if (path.isAbsolute(fullFilename)) {
101
+ fullFilename = fullFilename.slice(1)
102
+ }
103
+ if (filename === '') {
104
+ node.warn(RED._('file.errors.nofilename'))
105
+ done()
106
+ } else if (node.overwriteFile === 'delete') {
107
+ fs.unlink(fullFilename, function (err) {
108
+ if (err) {
109
+ node.error(RED._('file.errors.deletefail', { error: err.toString() }), msg)
110
+ } else {
111
+ node.debug(RED._('file.status.deletedfile', { file: filename }))
112
+ nodeSend(msg)
113
+ }
114
+ done()
115
+ })
116
+ // eslint-disable-next-line no-prototype-builtins
117
+ } else if (msg.hasOwnProperty('payload') && (typeof msg.payload !== 'undefined')) {
118
+ async function ensureDir (name, successCallback) {
119
+ const dir = path.dirname(name)
120
+ if (node.createDir) {
121
+ fs.ensureDir(dir, function (err) {
122
+ if (err) {
123
+ node.error(RED._('file.errors.createfail', { error: err.toString() }), msg)
124
+ }
125
+ successCallback()
126
+ })
127
+ } else {
128
+ successCallback()
129
+ }
130
+ }
131
+
132
+ ensureDir(fullFilename, function success () {
133
+ let data = msg.payload
134
+ if ((typeof data === 'object') && (!Buffer.isBuffer(data))) {
135
+ data = JSON.stringify(data)
136
+ }
137
+ if (typeof data === 'boolean') { data = data.toString() }
138
+ if (typeof data === 'number') { data = data.toString() }
139
+ if ((node.appendNewline) && (!Buffer.isBuffer(data))) { data += os.EOL }
140
+ let buf
141
+ if (node.encoding === 'setbymsg') {
142
+ buf = encode(data, msg.encoding || 'none')
143
+ } else { buf = encode(data, node.encoding) }
144
+ if (node.overwriteFile === 'true') {
145
+ fs.writeFile(fullFilename, buf, function (err) {
146
+ if (err) {
147
+ node.error(RED._('file.errors.writefail', { error: err.toString() }), msg)
148
+ } else {
149
+ nodeSend(msg)
150
+ }
151
+ done()
152
+ })
153
+ } else {
154
+ fs.appendFile(fullFilename, buf, function (err) {
155
+ if (err) {
156
+ node.error(RED._('file.errors.appendfail', { error: err.toString() }), msg)
157
+ } else {
158
+ nodeSend(msg)
159
+ }
160
+ done()
161
+ })
162
+ }
163
+ })
164
+ } else {
165
+ done()
166
+ }
167
+ }
168
+
169
+ function processQueue (queue) {
170
+ const event = queue[0]
171
+ processMsg(event.msg, event.send, function () {
172
+ event.done()
173
+ queue.shift()
174
+ if (queue.length > 0) {
175
+ processQueue(queue)
176
+ } else if (node.closing) {
177
+ closeNode()
178
+ }
179
+ })
180
+ }
181
+
182
+ this.on('input', function (msg, nodeSend, nodeDone) {
183
+ const msgQueue = node.msgQueue
184
+ msgQueue.push({
185
+ msg,
186
+ send: nodeSend,
187
+ done: nodeDone
188
+ })
189
+ if (msgQueue.length > 1) {
190
+ // pending write exists
191
+ return
192
+ }
193
+ try {
194
+ processQueue(msgQueue)
195
+ } catch (e) {
196
+ node.msgQueue = []
197
+ if (node.closing) {
198
+ closeNode()
199
+ }
200
+ throw e
201
+ }
202
+ })
203
+
204
+ function closeNode () {
205
+ if (node.wstream) { node.wstream.end() }
206
+ if (node.tout) { clearTimeout(node.tout) }
207
+ node.status({})
208
+ const cb = node.closeCallback
209
+ node.closeCallback = null
210
+ node.closing = false
211
+ if (cb) {
212
+ cb()
213
+ }
214
+ }
215
+
216
+ this.on('close', function (done) {
217
+ if (node.closing) {
218
+ // already closing
219
+ return
220
+ }
221
+ node.closing = true
222
+ if (done) {
223
+ node.closeCallback = done
224
+ }
225
+ if (node.msgQueue.length > 0) {
226
+ // close after queue processed
227
+
228
+ } else {
229
+ closeNode()
230
+ }
231
+ })
232
+ }
233
+ RED.nodes.registerType('file', FileNode)
234
+
235
+ function FileInNode (n) {
236
+ // Read a file
237
+ RED.nodes.createNode(this, n)
238
+ this.filename = n.filename
239
+ this.filenameType = n.filenameType
240
+ this.format = n.format
241
+ this.chunk = false
242
+ this.encoding = n.encoding || 'none'
243
+ this.allProps = n.allProps || false
244
+ if (n.sendError === undefined) {
245
+ this.sendError = true
246
+ } else {
247
+ this.sendError = n.sendError
248
+ }
249
+ if (this.format === 'lines') { this.chunk = true }
250
+ if (this.format === 'stream') { this.chunk = true }
251
+ const node = this
252
+ const fs = VFS(RED)
253
+ this.on('input', function (msg, nodeSend, nodeDone) {
254
+ let filename = node.filename || ''
255
+ // Pre V3 compatibility - if filenameType is empty, do in place upgrade
256
+ if (typeof node.filenameType === 'undefined' || node.filenameType === '') {
257
+ // existing node AND filenameType is not set - inplace (compatible) upgrade
258
+ if (filename === '') { // was using empty value to denote msg.filename
259
+ node.filename = 'filename'
260
+ node.filenameType = 'msg'
261
+ } else { // was using a static filename - set typedInput type to str
262
+ node.filenameType = 'str'
263
+ }
264
+ }
265
+ let propertyError = false
266
+ RED.util.evaluateNodeProperty(node.filename, node.filenameType, node, msg, (err, value) => {
267
+ if (err) {
268
+ node.error(err, msg)
269
+ propertyError = true
270
+ // return done()
271
+ } else {
272
+ filename = (value || '').replace(/\t|\r|\n/g, '')
273
+ }
274
+ })
275
+ if (propertyError) {
276
+ return
277
+ }
278
+ filename = filename || ''
279
+ let fullFilename = filename
280
+
281
+ if (filename && RED.settings.fileWorkingDirectory && !path.isAbsolute(filename)) {
282
+ // fullFilename = path.resolve(path.join(RED.settings.fileWorkingDirectory, filename))
283
+ fullFilename = path.join(RED.settings.fileWorkingDirectory, filename)
284
+ }
285
+ if (!node.filename) {
286
+ node.status({ fill: 'grey', shape: 'dot', text: filename })
287
+ }
288
+ if (path.isAbsolute(fullFilename)) {
289
+ fullFilename = fullFilename.slice(1)
290
+ }
291
+ if (filename === '') {
292
+ node.warn(RED._('file.errors.nofilename'))
293
+ nodeDone()
294
+ } else {
295
+ msg.filename = filename
296
+ let lines = Buffer.from([])
297
+ let spare = ''
298
+ let count = 0
299
+ let type = 'buffer'
300
+ let ch = ''
301
+ if (node.format === 'lines') {
302
+ ch = '\n'
303
+ type = 'string'
304
+ }
305
+ let getout = false
306
+
307
+ const rs = fs.createReadStream(fullFilename)
308
+ .on('readable', function () {
309
+ let chunk
310
+ let m
311
+ const hwm = rs._readableState.highWaterMark
312
+ while ((chunk = rs.read()) !== null) {
313
+ if (node.chunk === true) {
314
+ getout = true
315
+ if (node.format === 'lines') {
316
+ spare += decode(chunk, node.encoding)
317
+ const bits = spare.split('\n')
318
+ let i = 0
319
+ for (i = 0; i < bits.length - 1; i++) {
320
+ m = {}
321
+ if (node.allProps === true) {
322
+ m = RED.util.cloneMessage(msg)
323
+ } else {
324
+ m.topic = msg.topic
325
+ m.filename = msg.filename
326
+ }
327
+ m.payload = bits[i]
328
+ m.parts = { index: count, ch, type, id: msg._msgid }
329
+ count += 1
330
+ nodeSend(m)
331
+ }
332
+ spare = bits[i]
333
+ }
334
+ if (node.format === 'stream') {
335
+ m = {}
336
+ if (node.allProps === true) {
337
+ m = RED.util.cloneMessage(msg)
338
+ } else {
339
+ m.topic = msg.topic
340
+ m.filename = msg.filename
341
+ }
342
+ m.payload = chunk
343
+ m.parts = { index: count, ch, type, id: msg._msgid }
344
+ count += 1
345
+ if (chunk.length < hwm) { // last chunk is smaller that high water mark = eof
346
+ getout = false
347
+ m.parts.count = count
348
+ }
349
+ nodeSend(m)
350
+ }
351
+ } else {
352
+ lines = Buffer.concat([lines, chunk])
353
+ }
354
+ }
355
+ })
356
+ .on('error', function (err) {
357
+ node.error(err, msg)
358
+ if (node.sendError) {
359
+ const sendMessage = RED.util.cloneMessage(msg)
360
+ delete sendMessage.payload
361
+ sendMessage.error = err
362
+ nodeSend(sendMessage)
363
+ }
364
+ nodeDone()
365
+ })
366
+ .on('end', function () {
367
+ if (node.chunk === false) {
368
+ if (node.format === 'utf8') {
369
+ msg.payload = decode(lines, node.encoding)
370
+ } else { msg.payload = lines }
371
+ nodeSend(msg)
372
+ } else if (node.format === 'lines') {
373
+ let m = {}
374
+ if (node.allProps) {
375
+ m = RED.util.cloneMessage(msg)
376
+ } else {
377
+ m.topic = msg.topic
378
+ m.filename = msg.filename
379
+ }
380
+ m.payload = spare
381
+ m.parts = {
382
+ index: count,
383
+ count: count + 1,
384
+ ch,
385
+ type,
386
+ id: msg._msgid
387
+ }
388
+ nodeSend(m)
389
+ } else if (getout) { // last chunk same size as high water mark - have to send empty extra packet.
390
+ const m = { parts: { index: count, count, ch, type, id: msg._msgid } }
391
+ nodeSend(m)
392
+ }
393
+ nodeDone()
394
+ })
395
+ }
396
+ })
397
+ this.on('close', function () {
398
+ node.status({})
399
+ })
400
+ }
401
+ RED.nodes.registerType('file in', FileInNode)
402
+ }
@@ -0,0 +1,69 @@
1
+ <!--
2
+ Copyright JS Foundation and other contributors, http://js.foundation
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ -->
16
+
17
+ <script type="text/html" data-help-name="file">
18
+ <p>Writes <code>msg.payload</code> to a file, either adding to the end or replacing the existing content.
19
+ Alternatively, it can delete the file.</p>
20
+ <h3>Inputs</h3>
21
+ <dl class="message-properties">
22
+ <dt class="optional">filename <span class="property-type">string</span></dt>
23
+ <dd>The name of the file to be updated can be provided in the node configuration, or as a message property.
24
+ By default it will use <code>msg.filename</code> but this can be customised in the node.
25
+ </dd>
26
+ <dt class="optional">encoding <span class="property-type">string</span></dt>
27
+ <dd>If encoding is configured to be set by msg, then this optional property can set the encoding.</dt>
28
+ </dl>
29
+ <h3>Output</h3>
30
+ <p>On completion of write, input message is sent to output port.</p>
31
+ <h3>Details</h3>
32
+ <p>Each message payload will be added to the end of the file, optionally appending
33
+ a newline (\n) character between each one.</p>
34
+ <p>If <code>msg.filename</code> is used the file will be closed after every write.
35
+ For best performance use a fixed filename.</p>
36
+ <p>It can be configured to overwrite the entire file rather than append. For example,
37
+ when writing binary data to a file, such as an image, this option should be used
38
+ and the option to append a newline should be disabled.</p>
39
+ <p>Encoding of data written to a file can be specified from list of encodings.</p>
40
+ <p>Alternatively, this node can be configured to delete the file.</p>
41
+ </script>
42
+
43
+ <script type="text/html" data-help-name="file in">
44
+ <p>Reads the contents of a file as either a string or binary buffer.</p>
45
+ <h3>Inputs</h3>
46
+ <dl class="message-properties">
47
+ <dt class="optional">filename <span class="property-type">string</span></dt>
48
+ <dd>The name of the file to be read can be provided in the node configuration, or as a message property.
49
+ By default it will use <code>msg.filename</code> but this can be customised in the node.
50
+ </dd>
51
+ </dl>
52
+ <h3>Outputs</h3>
53
+ <dl class="message-properties">
54
+ <dt>payload <span class="property-type">string | buffer</span></dt>
55
+ <dd>The contents of the file as either a string or binary buffer.</dd>
56
+ <dt class="optional">filename <span class="property-type">string</span></dt>
57
+ <dd>If not configured in the node, this optional property sets the name of the file to be read.</dd>
58
+ </dl>
59
+ <h3>Details</h3>
60
+ <p>The filename should be an absolute path, otherwise it will be relative to
61
+ the working directory of the Node-RED process.</p>
62
+ <p>On Windows, path separators may need to be escaped, for example: <code>\\Users\\myUser</code>.</p>
63
+ <p>Optionally, a text file can be split into lines, outputting one message per line, or a binary file
64
+ split into smaller buffer chunks - the chunk size being operating system dependant, but typically 64k (Linux/Mac) or 41k (Windows).</p>
65
+ <p>When split into multiple messages, each message will have a <code>parts</code>
66
+ property set, forming a complete message sequence.</p>
67
+ <p>Encoding of input data can be specified from list of encodings if output format is string.</p>
68
+ <p>Errors should be caught and handled using a Catch node.</p>
69
+ </script>
@@ -0,0 +1,65 @@
1
+ {
2
+ "file": {
3
+ "label": {
4
+ "write": "write file",
5
+ "read": "read file",
6
+ "filename": "Filename",
7
+ "path": "path",
8
+ "action": "Action",
9
+ "addnewline": "Add newline (\\n) to each payload?",
10
+ "createdir": "Create directory if it doesn't exist?",
11
+ "outputas": "Output",
12
+ "breakchunks": "Break into chunks",
13
+ "breaklines": "Break into lines",
14
+ "sendError": "Send message on error (legacy mode)",
15
+ "encoding": "Encoding",
16
+ "deletelabel": "delete __file__",
17
+ "utf8String": "UTF8 string",
18
+ "utf8String_plural": "UTF8 strings",
19
+ "binaryBuffer": "binary buffer",
20
+ "binaryBuffer_plural": "binary buffers",
21
+ "allProps": "include all existing properties in each msg"
22
+ },
23
+ "action": {
24
+ "append": "append to file",
25
+ "overwrite": "overwrite file",
26
+ "delete": "delete file"
27
+ },
28
+ "output": {
29
+ "utf8": "a single utf8 string",
30
+ "buffer": "a single Buffer object",
31
+ "lines": "a msg per line",
32
+ "stream": "a stream of Buffers"
33
+ },
34
+ "status": {
35
+ "wrotefile": "wrote to file: __file__",
36
+ "deletedfile": "deleted file: __file__",
37
+ "appendedfile": "appended to file: __file__"
38
+ },
39
+ "encoding": {
40
+ "none": "default",
41
+ "setbymsg": "set by msg.encoding",
42
+ "native": "Native",
43
+ "unicode": "Unicode",
44
+ "japanese": "Japanese",
45
+ "chinese": "Chinese",
46
+ "korean": "Korean",
47
+ "taiwan": "Taiwan/Hong Kong",
48
+ "windows": "Windows codepages",
49
+ "iso": "ISO codepages",
50
+ "ibm": "IBM codepages",
51
+ "mac": "Mac codepages",
52
+ "koi8": "KOI8 codepages",
53
+ "misc": "Miscellaneous"
54
+ },
55
+ "errors": {
56
+ "nofilename": "No filename specified",
57
+ "invaliddelete": "Warning: Invalid delete. Please use specific delete option in config dialog.",
58
+ "deletefail": "failed to delete file: __error__",
59
+ "writefail": "failed to write to file: __error__",
60
+ "appendfail": "failed to append to file: __error__",
61
+ "createfail": "failed to create file: __error__"
62
+ },
63
+ "tip": "Tip: The filename should be an absolute path, otherwise it will be relative to the working directory of the Node-RED process."
64
+ }
65
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@flowfuse/nr-file-nodes",
3
+ "version": "0.0.3",
4
+ "description": "Node-RED file nodes packaged for FlowFuse",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "npm run test:files && npm run test:memory",
8
+ "test:memory": "mocha 'test/memory_spec.js' --timeout 5000",
9
+ "test:files": "mocha 'test/file_spec.js' --timeout 5000",
10
+ "lint": "eslint -c .eslintrc *.js",
11
+ "lint:fix": "eslint -c .eslintrc *.js --fix"
12
+ },
13
+ "keywords": [
14
+ "FlowFuse",
15
+ "node-red",
16
+ "filesystem"
17
+ ],
18
+ "node-red": {
19
+ "version": ">=3.0.0",
20
+ "nodes": {
21
+ "file": "file.js"
22
+ }
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/FlowFuse/nr-file-nodes.git"
27
+ },
28
+ "author": {
29
+ "name": "FlowFuse Inc."
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/FlowFuse/nr-file-nodes/issues"
33
+ },
34
+ "homepage": "https://github.com/FlowFuse/nr-file-nodes#readme",
35
+ "dependencies": {
36
+ "got": "11.8.5",
37
+ "iconv-lite": "0.6.3"
38
+ },
39
+ "engines": {
40
+ "node": ">=16.x"
41
+ },
42
+ "devDependencies": {
43
+ "@flowforge/file-server": "^0.0.5",
44
+ "eslint": "^8.25.0",
45
+ "eslint-config-standard": "^17.0.0",
46
+ "eslint-plugin-no-only-tests": "^3.1.0",
47
+ "fs-extra": "^10.1.0",
48
+ "mocha": "^10.1.0",
49
+ "mocha-cli": "^1.0.1",
50
+ "node-red": "^3.1.0",
51
+ "node-red-node-test-helper": "^0.3.0",
52
+ "sinon": "^14.0.2"
53
+ }
54
+ }