Skip to content

Instantly share code, notes, and snippets.

@nygma2004
Last active March 7, 2023 09:26
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nygma2004/44bc7ad491aacb4253dd8a5f757b5407 to your computer and use it in GitHub Desktop.
Save nygma2004/44bc7ad491aacb4253dd8a5f757b5407 to your computer and use it in GitHub Desktop.
File Browser in Dashboard

I wanted an easier way to access my files on my PI and created this File Browser in dashboard. Main features:

  • Show files in local folders: size, dates
  • Download files
  • Navigate to any folder
  • Delete files
  • Show/Hide hidden files
  • Open CSV files on a Graph

Check the comment node on the top which explains how to set up the flow.

For more info you can check my Youtube video which explains this flow: https://youtu.be/3QgK4IAAqcQ

File lister node: node-red-contrib-fs

[{"id":"4fa73dd9.83cca4","type":"comment","z":"74f191ff.db063","name":"File Browser","info":"1) Update the default folder in the Init node\n2) Update the default folder in the Reset node as well\n3) You can duplicate the Reset nodes and use them as saved shortcuts\n4) Check the Convert Timestamps function node if you want to see your dates in a different format. I just used the Javascript toISOString format.\n5) The Graph button can read any CSV file which have any number of values, but the first column always contains a timestamp column with javascript timestamp of the data point.","x":110,"y":2940,"wires":[]},{"id":"993d7272.843ae","type":"fs-file-lister","z":"74f191ff.db063","name":"Files","start":"/home/pi","pattern":"*.*","folders":"*","hidden":false,"lstype":"files","path":true,"single":true,"depth":0,"stat":true,"showWarnings":false,"x":510,"y":3040,"wires":[["dd88bd23.37cde","3e094f82.15d6e"]]},{"id":"d185a45.2327a58","type":"inject","z":"74f191ff.db063","name":"Init","topic":"change","payload":"/home/pi","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":110,"y":3000,"wires":[["3d4e5e51.bdf952"]]},{"id":"a715f7a4.a398a8","type":"ui_button","z":"74f191ff.db063","name":"","group":"160e81fb.f1c86e","order":1,"width":"2","height":"1","passthru":false,"label":"Refresh","tooltip":"","color":"","bgcolor":"","icon":"refresh","payload":"","payloadType":"str","topic":"refresh","x":100,"y":3040,"wires":[["3d4e5e51.bdf952"]]},{"id":"eb17c7ee.e61988","type":"ui_dropdown","z":"74f191ff.db063","name":"File Selector","label":"","tooltip":"","place":"Select a file","group":"160e81fb.f1c86e","order":9,"width":"5","height":"1","passthru":false,"options":[{"label":"","value":"","type":"str"}],"payload":"","topic":"","x":910,"y":3040,"wires":[["be4830df.5775d"]]},{"id":"dd88bd23.37cde","type":"function","z":"74f191ff.db063","name":"Format data","func":"// format the data for the dropdown\nmsg.options = [];\nfor (var i=0; i<msg.payload.length; i++) {\n // This is a file\n obj = {};\n obj [msg.payload[i].name.replace(/^.*(\\\\|\\/|\\:)/, '')]=msg.payload[i].name;\n msg.options.push(obj);\n}\nmsg.payload={};\nreturn msg;","outputs":1,"noerr":0,"x":710,"y":3040,"wires":[["eb17c7ee.e61988"]]},{"id":"638728aa.cd0b08","type":"ui_template","z":"74f191ff.db063","group":"160e81fb.f1c86e","name":"","order":6,"width":"18","height":"6","format":"<div ng-bind-html=\"msg.payload\" height=\"400\" style=\"height: 400px;\"><br/>\n</div>\n\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1100,"y":3100,"wires":[[]]},{"id":"9d27e846.b31db8","type":"template","z":"74f191ff.db063","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<table width=\"100%\">\n <tr><th>File Name</th><th>Size</th><th>Created</th><th>Changed</th></tr>\n {{#payload}}\n <tr>\n <td><a href=\"/download?filename={{name}}\" target=\"blank\">{{fname}}</a></td>\n <td>{{stat.size}}</td>\n <td>{{stat.created}}</td>\n <td>{{stat.changed}}</td>\n </tr>\n {{/payload}}\n</table>\n","output":"str","x":940,"y":3100,"wires":[["638728aa.cd0b08"]]},{"id":"3e094f82.15d6e","type":"function","z":"74f191ff.db063","name":"Convert timestamps","func":"for (var i=0; i<msg.payload.length; i++) {\n msg.payload[i].stat.created = msg.payload[i].stat.created.toISOString().slice(0, 19).replace('T', ' ');\n msg.payload[i].stat.changed = msg.payload[i].stat.changed.toISOString().slice(0, 19).replace('T', ' ');\n msg.payload[i].stat.accessed = msg.payload[i].stat.accessed.toISOString().slice(0, 19).replace('T', ' ');\n msg.payload[i].stat.statusChanged = msg.payload[i].stat.statusChanged.toISOString().slice(0, 19).replace('T', ' ');\n msg.payload[i].fname = msg.payload[i].name.replace(/^.*(\\\\|\\/|\\:)/, '');\n}\nreturn msg;","outputs":1,"noerr":0,"x":740,"y":3100,"wires":[["9d27e846.b31db8"]]},{"id":"7b7e0d58.05e244","type":"ui_button","z":"74f191ff.db063","name":"","group":"160e81fb.f1c86e","order":10,"width":"2","height":"1","passthru":false,"label":"Delete","tooltip":"","color":"","bgcolor":"","icon":"delete","payload":"","payloadType":"str","topic":"","x":130,"y":3200,"wires":[["8da8147a.999af8"]]},{"id":"7d490dd1.8458b4","type":"function","z":"74f191ff.db063","name":"Get filename","func":"// Get the filename from the flow context\nlet filename = flow.get(\"fileselected\");\n\n// check, if the filename is undefined that means it does not exist yet, nothing is selected yet\n// return: do not output anything\nif (filename===undefined) {\n return;\n}\n\n// return the filename to the file-in node to delete\nmsg.filename = filename;\n\nif (msg.filename.replace(/^.*(\\\\|\\/|\\:)/, '')[0]!==\".\") {\n // Only do this if this is a file, we don't delete folders\n // and delete the context/selection as we are deleting the file as well\n flow.set(\"fileselected\");\n return msg;\n}","outputs":1,"noerr":0,"x":330,"y":3260,"wires":[["372ee262.fc94de"]]},{"id":"372ee262.fc94de","type":"file","z":"74f191ff.db063","name":"Delete file","filename":"","appendNewline":true,"createDir":false,"overwriteFile":"delete","encoding":"none","x":540,"y":3260,"wires":[["993d7272.843ae","5219875b.c070d8"]]},{"id":"aa8c482.93734b8","type":"http in","z":"74f191ff.db063","name":"","url":"/download","method":"get","upload":false,"swaggerDoc":"","x":140,"y":3400,"wires":[["d969ba04.e24028"]]},{"id":"d19cc7d8.646328","type":"http response","z":"74f191ff.db063","name":"","statusCode":"","headers":{},"x":930,"y":3400,"wires":[]},{"id":"d969ba04.e24028","type":"function","z":"74f191ff.db063","name":"Get the file name","func":"msg.filename = msg.req.query.filename;\nmsg.contentdisposition = \"attachment; filename=\\\"\" + msg.req.query.filename.replace(/^.*(\\\\|\\/|\\:)/, '') + \"\\\"\";\nreturn msg;\n","outputs":1,"noerr":0,"x":390,"y":3400,"wires":[["e92381c3.c4cd2"]],"outputLabels":["Folder selected","File selected"]},{"id":"e92381c3.c4cd2","type":"file in","z":"74f191ff.db063","name":"","filename":"","format":"","chunk":false,"sendError":false,"encoding":"none","x":580,"y":3400,"wires":[["99ff4953.d0d5c8"]]},{"id":"99ff4953.d0d5c8","type":"change","z":"74f191ff.db063","name":"Set Headers","rules":[{"t":"set","p":"headers","pt":"msg","to":"{}","tot":"json"},{"t":"set","p":"headers.content-type","pt":"msg","to":"text/csv","tot":"str"},{"t":"set","p":"headers.Content-Disposition","pt":"msg","to":"contentdisposition","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":750,"y":3400,"wires":[["d19cc7d8.646328"]]},{"id":"2519dd0d.54d6b2","type":"ui_button","z":"74f191ff.db063","name":"","group":"160e81fb.f1c86e","order":11,"width":"2","height":"1","passthru":false,"label":"Graph","tooltip":"","color":"","bgcolor":"","icon":"show_chart","payload":"","payloadType":"str","topic":"","x":90,"y":3520,"wires":[["30f41c01.358a54"]]},{"id":"30f41c01.358a54","type":"function","z":"74f191ff.db063","name":"Get filename","func":"// Get the filename from the flow context\nlet filename = flow.get(\"fileselected\");\n\n// check, if the filename is undefined that means it does not exist yet, nothing is selected yet\n// return: do not output anything\nif (filename===undefined) {\n return;\n}\n\n// return the filename to the file-in node to delete\nmsg.filename = filename;\n\nif (msg.filename.replace(/^.*(\\\\|\\/|\\:)/, '')[0]!==\".\") {\n // Only do this if this is a file, we don't delete folders\n return msg;\n}","outputs":1,"noerr":0,"x":260,"y":3520,"wires":[["4f4072b6.0c320c"]]},{"id":"4f4072b6.0c320c","type":"file in","z":"74f191ff.db063","name":"","filename":"","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":440,"y":3520,"wires":[["4d254665.d508e8"]]},{"id":"4d254665.d508e8","type":"csv","z":"74f191ff.db063","name":"","sep":",","hdrin":true,"hdrout":"","multi":"mult","ret":"\\n","temp":"","skip":"0","strings":true,"x":590,"y":3520,"wires":[["7dcec769.580718"]]},{"id":"84810f0e.43f4e","type":"debug","z":"74f191ff.db063","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1010,"y":3480,"wires":[]},{"id":"7dcec769.580718","type":"function","z":"74f191ff.db063","name":"Format data for chart","func":"var chart = [{\n \"series\":[],\n \"data\":[],\n \"labels\":[msg.filename]\n}];\n\n\n/*\nvar pressure = [];\nvar out2 = [];\n\nfor (var i=0; i<msg.payload.length; i++) {\n pressure.push({\"x\":msg.payload[i].timestamp, \"y\":msg.payload[i].pressure});\n out2.push({\"x\":msg.payload[i].timestamp, \"y\":msg.payload[i].out2*200});\n\n}\nchart[0].data.push(pressure);\nchart[0].data.push(out2);\n*/\n\nlet columns = 0;\n\nfor(var series in msg.payload[0]) {\n if(series!==\"timestamp\") {\n chart[0].series.push(series);\n chart[0].data.push([]);\n columns++;\n }\n}\n\n\nfor (var j=0; j<msg.payload.length; j++) {\n for(var i=0;i<columns;i++) {\n chart[0].data[i].push({\"x\":msg.payload[j].timestamp, \"y\":msg.payload[j][chart[0].series[i]]});\n } \n}\n\n\n\n\nmsg.payload = chart;\n\nreturn msg;","outputs":1,"noerr":0,"x":800,"y":3520,"wires":[["5022cb53.8adc44","84810f0e.43f4e"]]},{"id":"5022cb53.8adc44","type":"ui_chart","z":"74f191ff.db063","name":"","group":"160e81fb.f1c86e","order":12,"width":"18","height":"6","label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1000,"y":3520,"wires":[[]]},{"id":"554f0095.ac1b2","type":"ui_toast","z":"74f191ff.db063","position":"dialog","displayTime":"3","highlight":"","sendall":false,"outputs":1,"ok":"Yes","cancel":"No","raw":false,"topic":"","name":"Confirmation","x":510,"y":3200,"wires":[["24df84fe.ebf45c"]]},{"id":"8da8147a.999af8","type":"change","z":"74f191ff.db063","name":"Set message","rules":[{"t":"set","p":"topic","pt":"msg","to":"Delete confirmation","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"Are you sure you want to delete this file?","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":3200,"wires":[["554f0095.ac1b2"]]},{"id":"24df84fe.ebf45c","type":"switch","z":"74f191ff.db063","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"Yes","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":710,"y":3200,"wires":[["7d490dd1.8458b4"]]},{"id":"3d4e5e51.bdf952","type":"function","z":"74f191ff.db063","name":"Folder handling","func":"let folder = context.get(\"folder\");\nif (folder===undefined) {\n folder=\"/\";\n context.set(\"folder\", folder);\n}\n\nlet hidden = context.get(\"hidden\");\nif (hidden===undefined) {\n hidden=false;\n context.set(\"hidden\", hidden);\n}\n\nif (msg.topic===\"up\") {\n var the_arr = folder.split('/');\n the_arr.pop();\n folder=the_arr.join('/'); \n context.set(\"folder\", folder);\n}\nif (msg.topic===\"change\") {\n folder=msg.payload; \n context.set(\"folder\", folder);\n}\nif (msg.topic===\"hidden\") {\n hidden=msg.payload; \n context.set(\"hidden\", hidden);\n}\n\nmsg.payload = {\"start\":folder, \"hidden\": hidden};\nreturn msg;","outputs":1,"noerr":0,"x":300,"y":3100,"wires":[["993d7272.843ae","399ac341.7d43bc","5219875b.c070d8"]]},{"id":"399ac341.7d43bc","type":"ui_text","z":"74f191ff.db063","group":"160e81fb.f1c86e","order":4,"width":"9","height":"1","name":"","label":"Folder:","format":"{{msg.payload.start}}","layout":"row-left","x":500,"y":2980,"wires":[]},{"id":"b40ea1d8.c700a","type":"ui_button","z":"74f191ff.db063","name":"","group":"160e81fb.f1c86e","order":3,"width":"2","height":"1","passthru":false,"label":"Up","tooltip":"","color":"","bgcolor":"","icon":"arrow_upwards","payload":"","payloadType":"str","topic":"up","x":110,"y":3120,"wires":[["3d4e5e51.bdf952"]]},{"id":"80940e39.5035b","type":"ui_button","z":"74f191ff.db063","name":"","group":"160e81fb.f1c86e","order":8,"width":"2","height":"1","passthru":false,"label":"Open","tooltip":"","color":"","bgcolor":"","icon":"folder_open","payload":"","payloadType":"str","topic":"","x":130,"y":3320,"wires":[["8c1dfaac.979588"]]},{"id":"8c1dfaac.979588","type":"function","z":"74f191ff.db063","name":"Change folder","func":"// Get the filename from the flow context\nlet folderselected = flow.get(\"folderselected\");\n\n// check, if the filename is undefined that means it does not exist yet, nothing is selected yet\n// return: do not output anything\nif (folderselected===undefined) {\n return;\n}\n\nmsg.topic = \"change\";\nmsg.payload = folderselected;\n\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":3320,"wires":[["3d4e5e51.bdf952"]]},{"id":"58414ec4.c716e","type":"ui_button","z":"74f191ff.db063","name":"","group":"160e81fb.f1c86e","order":2,"width":"2","height":"1","passthru":false,"label":"Reset","tooltip":"","color":"","bgcolor":"","icon":"autorenew","payload":"/home/pi","payloadType":"str","topic":"change","x":110,"y":3080,"wires":[["3d4e5e51.bdf952"]]},{"id":"5219875b.c070d8","type":"fs-file-lister","z":"74f191ff.db063","name":"Folders","start":"/home/pi","pattern":"*.*","folders":"*","hidden":false,"lstype":"directories","path":true,"single":true,"depth":0,"stat":true,"showWarnings":false,"x":520,"y":3140,"wires":[["ebc234be.d53fe8"]]},{"id":"37485c24.212054","type":"ui_dropdown","z":"74f191ff.db063","name":"Folder Selector","label":"","tooltip":"","place":"Select a folder","group":"160e81fb.f1c86e","order":7,"width":"5","height":"1","passthru":false,"options":[{"label":"","value":"","type":"str"}],"payload":"","topic":"","x":920,"y":3140,"wires":[["2875c79f.531558"]]},{"id":"ebc234be.d53fe8","type":"function","z":"74f191ff.db063","name":"Format data","func":"// format the data for the dropdown\nmsg.options = [];\nfor (var i=0; i<msg.payload.length; i++) {\n // This is a foler\n obj = {};\n obj [\"[\"+msg.payload[i].name.replace(/^.*(\\\\|\\/|\\:)/, '')+\"]\"]=msg.payload[i].name;\n msg.options.push(obj);\n}\nmsg.payload={};\nreturn msg;","outputs":1,"noerr":0,"x":710,"y":3140,"wires":[["37485c24.212054"]]},{"id":"be4830df.5775d","type":"change","z":"74f191ff.db063","name":"Save selection","rules":[{"t":"set","p":"fileselected","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1120,"y":3040,"wires":[[]]},{"id":"2875c79f.531558","type":"change","z":"74f191ff.db063","name":"Save selection","rules":[{"t":"set","p":"folderselected","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1120,"y":3140,"wires":[[]]},{"id":"928e60a7.994be","type":"ui_switch","z":"74f191ff.db063","name":"","label":"Hidden","tooltip":"Show hidden files or not","group":"160e81fb.f1c86e","order":5,"width":"2","height":"1","passthru":false,"decouple":"false","topic":"hidden","style":"","onvalue":"true","onvalueType":"bool","onicon":"check_box","oncolor":"#000000","offvalue":"false","offvalueType":"bool","officon":"check_box_outline_blank","offcolor":"#000000","x":100,"y":3160,"wires":[["3d4e5e51.bdf952"]]},{"id":"160e81fb.f1c86e","type":"ui_group","z":"","name":"File Browser","tab":"b63d1f91.68095","order":1,"disp":true,"width":"18","collapse":false},{"id":"b63d1f91.68095","type":"ui_tab","z":"","name":"Files","icon":"dashboard","disabled":false,"hidden":false}]
@jabrockert
Copy link

Trying to play with this node, I'm getting a missing node fs-file-lister, and I can't find it in the nodered library, Where can I find fs-file-lister?

@nygma2004
Copy link
Author

It is called node-red-contrib-fs, and it is available in the palette manager. Let me add this to the readme.

@Pongpun364
Copy link

Pongpun364 commented Oct 12, 2020

i keep getting an error ""TypeError: msg.filename.replace is not a function"" ,please helpp...

@Pongpun364
Copy link

it's normal now, i don't know why .

@lorenzozaccomer
Copy link

lorenzozaccomer commented Dec 17, 2021

Hello,
I tried your flow, it generate the chart but I can't see the line on the chart.

And also the points are in the chart, but are invisible too.

How I can solve?

image

@nygma2004
Copy link
Author

@lorenzozaccomer that is a good question. What is in the file you are trying to display? Did you get any errors in the debug window?

@lorenzozaccomer
Copy link

@nygma2004 I tried to display a csv file.

Nope, no errors.
What I have observed is that the payload out from "from data to chart" (using a debug node) is this:

image

That is different from the standard payload for the chart, following this example:

image

@lorenzozaccomer
Copy link

lorenzozaccomer commented Dec 19, 2021

Hello, I solved in this way:

image

On your version you put on the chart (data array) the texts (watch on the previous message), and this create some data visualization error.

You need only put an if to check if the payload is a number, so in the end you have on data array:
x: timestamp, y: value

image

@nygma2004
Copy link
Author

Thanks a lot, sorry I forgot about this. I have migrated to a new server a few weeks ago, and also upgrade to the latest Node-Red. I started noticing similar issues where codes I written a few years back would not work the same way. Like I was reading a timestamp value like let lastupdate = context.get("lastupdate") | 0; And I could see the context value in the editor, but I was not getting the same value in the my variable in the code. And it looks like the | 0 part caused some type casting that made it unable to read the number. So need to pay more attention to this now.

@Aggebitter
Copy link

Aggebitter commented Nov 4, 2022

Think I found a bug in the functions "format data" for file and "format data" for folders using node-red v3.0.2
As soon as I open the function node the node gets broken with error "Cannot find name 'obj'.(2304)".
But if I just run your flow without open the node it runs as it should.
I'm a complete noob on javascript and node-red but the issue is in this part of the functions and I'm totally lost in that syntax

obj = {};
obj ["["+msg.payload[i].name.replace(/^.*(\|/|:)/, '')+"]"]=msg.payload[i].name;
msg.options.push(obj);

@nygma2004
Copy link
Author

Hmmm interesting. I have just updated to v3.0.2 today, and I can see the issue. I forgot to type the let declaration statement.
It should be like this:

let obj = {};

And this will make the error disappear.

@Aggebitter
Copy link

Aggebitter commented Nov 4, 2022

That made the it. I suppose that the syntax check in node-red just stops the function but java-script auto declare it.

I have just remade your file browser to just show four "user"accessible folders by removing "up" node and adding hard paths to init and new buttons. ( and the background color changes on current folder "button" for easy navigation.

One improvement I made was to link output from "delete" node to input on "refresh" node.
Then delete also auto updates the current folder after deleting a file ( Backup routine not fixed yet)

Thanks for a perfect flow that made my day
// David

@Gigicn70
Copy link

Gigicn70 commented Mar 7, 2023

Good day to all,
I'm starting to use nodered now and I have a problem on using on windows platform
I've foung anc correct the problem on the let statend
let obj = {};
I've also changed the path in "C:\Test"
but when I call the file or folder function the follwong error appear

7 Mar 09:44:44 - [error] TypeError: cb.apply is not a function
at C:\Users\user.node-red\node_modules\npm\node_modules\graceful-fs.js:287:18
at FSReqCallback.oncomplete (node:fs:208:5)

this is mi node version :
7 Mar 09:44:33 - [info] Node-RED version: v3.0.2
7 Mar 09:44:33 - [info] Node.js version: v18.14.2
7 Mar 09:44:33 - [info] Windows_NT 10.0.19044 x64 LE
7 Mar 09:44:35 - [info] Loading palette nodes
7 Mar 09:44:39 - [info] Dashboard version 2.13.2 started at /ui

I've cleared npm repository but the problem remains
Ive noted that If i remove the tip on "Return file details?" nodered don't crash, but the flow don't works correctly

immagine

How can I solve?
Regards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment