# FileBrowser Node 
A widget to access SMB/CIFS Shares and FTP Repositories 


![alt text](https://user-images.githubusercontent.com/35899018/49729734-94f19180-fc76-11e8-80b1-7843598d96fe.png)


![alt text](https://user-images.githubusercontent.com/35899018/49740659-050f1000-fc95-11e8-87c3-ca98f13961a7.png)



# Installation
The node can be installed via the Node-RED Palette manager. Search for "node-red-contrib-filebrowser" and install the node: 

![alt text](https://user-images.githubusercontent.com/35899018/49729747-9c189f80-fc76-11e8-9854-5e77f45b23b5.png)

The Palette manager will install the new node and add a new storage-type "filebrowser":

![alt text](https://user-images.githubusercontent.com/35899018/49730307-42b17000-fc78-11e8-94b8-aff7fe2ee755.png)


The following flow, includes a configuration and a localozation node as well as a caching mechanism ( [see Caching for more details](#Caching) ) to re-use retrieved data:

![screenshot_12](https://user-images.githubusercontent.com/35899018/49739330-efe4b200-fc91-11e8-9c3c-b1287486fbc5.png)

```javascript
[{"id":"5f4ab59d.f0ea7c","type":"http in","z":"d47cd009.b88c6","name":"","url":"/repositories/filters","method":"get","upload":false,"swaggerDoc":"","x":120,"y":980,"wires":[["dbd46b1c.c55198"]]},{"id":"dbd46b1c.c55198","type":"template","z":"d47cd009.b88c6","name":"params page","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<form class=\"ui form\">\n\n  <div class=\"ui toggle checkbox\">\n      <input name=\"useFulltext\" type=\"checkbox\" checked=\"checked\">\n      <label>{{flow.LOCALIZED.useIndex}}</label>\n     \n    </div>\n\n <div></div>\n\n   <div class=\"field\">\n      <label>{{flow.LOCALIZED.itemType}}</label>\n      <select name=\"itemType\" class=\"ui dropdown\">\n         <option value=\"all\" selected=\"selected\">{{flow.LOCALIZED.itemAll}}</option>\n         <option value=\"folder\">{{flow.LOCALIZED.itemFolder}}</option>\n         <option value=\"file\">{{flow.LOCALIZED.itemFile}}</option>\n      </select>\n   </div>\n \n   <div class=\"field\">\n      <label>{{flow.LOCALIZED.maxResults}}</label>\n      <select name=\"maxResults\" class=\"ui dropdown\">\n         <option value=\"30\" selected=\"selected\">30</option>\n         <option value=\"50\">50</option>\n         <option value=\"100\">100</option>\n         <option value=\"*\">*</option>\n \n      </select>\n   </div>\n\n</form>","output":"str","x":510,"y":980,"wires":[["84189d6d.939e4"]]},{"id":"84189d6d.939e4","type":"http response","z":"d47cd009.b88c6","name":"","statusCode":"","headers":{},"x":970,"y":980,"wires":[]},{"id":"944203de.39ed","type":"http in","z":"d47cd009.b88c6","name":"","url":"/repositories/sort","method":"get","upload":false,"swaggerDoc":"","x":120,"y":1020,"wires":[["d833d1d7.12e02"]]},{"id":"d833d1d7.12e02","type":"template","z":"d47cd009.b88c6","name":"params page","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<form class=\"ui form\">\n   <div class=\"field\">\n      <label>{{flow.LOCALIZED.sort}}</label>\n      <select name=\"order\" class=\"ui dropdown\">\n        <option value=\"lastmod\">{{flow.LOCALIZED.sort_default}}</option>\n         <option value=\"lastmod\">{{flow.LOCALIZED.sort_date}}</option>\n         <option value=\"name\">{{flow.LOCALIZED.sort_title}}</option>\n         <option value=\"size\">{{flow.LOCALIZED.sort_size}}</option>\n         <option value=\"type\">{{flow.LOCALIZED.sort_type}}</option>\n      </select>\n   </div>\n   \n   <div class=\"field\">\n      <label>{{flow.LOCALIZED.sortOrder}}</label>\n      <select name=\"sortorder\" class=\"ui dropdown\">\n         <option value=\"asc\">{{flow.LOCALIZED.sortOrderAsc}}</option>\n         <option value=\"desc\">{{flow.LOCALIZED.sortOrderDesc}}</option>\n      </select>\n   </div>\n</form>\n\n\n\n","output":"str","x":510,"y":1020,"wires":[["e7bf4e91.3116e"]]},{"id":"e7bf4e91.3116e","type":"http response","z":"d47cd009.b88c6","name":"","statusCode":"","headers":{},"x":970,"y":1020,"wires":[]},{"id":"15e97351.3ceffd","type":"inject","z":"d47cd009.b88c6","name":"Once","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":"0.1","x":90,"y":740,"wires":[["29e23322.d6437c"]]},{"id":"29e23322.d6437c","type":"function","z":"d47cd009.b88c6","name":"Localization","func":"var localization = {\n    \"en_EN\": {\n        \"useIndex\" : \"Use fulltext index\",\n        \"maxResults\": \"Results per page\",\n        \"itemType\"  : \"Item type\",\n        \"itemAll\"   : \"All\",\n        \"itemFolder\" : \"Folder\",\n        \"itemFile\" : \"File\",\n        \"sort\":\"Sort by\",\n        \"sort_default\": \"Default\",\n        \"sort_date\":\"Date\",\n        \"sort_type\":\"Type\",\n        \"sort_size\":\"Size\",\n        \"sort_title\":\"Name\",\n        \"sortOrder\" : \"Sort order\",\n        \"sortOrderAsc\" : \"Ascending\",\n        \"sortOrderDesc\" : \"Descending\",\n       \n    },\n    \"de_DE\": {\n        \"useIndex\" : \"Volltextsuche verwenden\",\n        \"maxResults\": \"Ergebnisse pro Seite\",\n        \"itemType\"  : \"Typ\",\n        \"itemAll\"   : \"Alle\",\n        \"itemFolder\" : \"Verzeichnisse\",\n        \"itemFile\" : \"Dateien\",\n        \"sort\":\"Soriteren nach\",\n        \"sort_default\": \"Standart\",\n        \"sort_date\":\"Datum\",\n        \"sort_type\":\"Typ\",\n        \"sort_size\":\"Größe\",\n        \"sort_title\":\"Name\",\n        \"sortOrder\" : \"Sortiereihenfolge\",\n        \"sortOrderAsc\" : \"aufsteigend\",\n        \"sortOrderDesc\" : \"absteigend\",\n    },\n    \"fr_FR\": {\n        \"useIndex\" : \" Use fulltext index\",\n        \"maxResults\": \"Results per page\",\n        \"license\": \"License\",\n        \"ignore\": \"Ignore\",\n        \"lang\": \"Relevance language\",\n        \"lang_ar\": \"Arabic\",\n        \"lang_bg\": \"Bulgarian\",\n        \"lang_da\": \"Dänisch\",\n        \"lang_nl\": \"Niederländisch, Flämisch\",\n        \"lang_en\": \"Englisch\",\n        \"lang_fr\": \"Französisch\",\n        \"lang_de\": \"Deutsch\",\n        \"lang_es\": \"Spanisch\",\n        \"sort\":\"Sort by\",\n        \"sort_date\":\"Date\",\n        \"sort_type\":\"Rating\",\n        \"sort_size\":\"Video count\",\n        \"sort_title\":\"Title\",\n    }\n}\nvar language = flow.get(\"LANGUAGE\");\nvar localized = localization[language];\nflow.set(\"LOCALIZED\", localized);\n","outputs":1,"noerr":0,"x":230,"y":740,"wires":[[]]},{"id":"77aa25e7.87e26c","type":"config","z":"d47cd009.b88c6","name":"CONFIG","properties":[{"p":"LANGUAGE","pt":"flow","to":"de_DE","tot":"str"},{"p":"OPENMEDIA_MAPPING","pt":"flow","to":"{\"templateId\":{\"default\":1388635,\"audio\":1388637,\"image\":1388639,\"video\":1388638},\"poolId\":101,\"folderId\":22219,\"systemId\":\"6290b86c-3cdd-4d52-8340-df3b9bf2aa15\",\"OMISCommandAfterCreation\":{\"pluginID\":123,\"commandID\":132},\"fields\":{\"title\":8,\"id\":3201,\"url\":401}}","tot":"json"},{"p":"FULLTEXTENGINE_URL","pt":"flow","to":"http://127.0.0.1:8090","tot":"str"}],"active":true,"x":100,"y":800,"wires":[]},{"id":"45fcec2e.8cd0d4","type":"http response","z":"d47cd009.b88c6","name":"","statusCode":"200","headers":{},"x":1200,"y":780,"wires":[]},{"id":"77336ef7.0f9ac","type":"filebrowser in","z":"d47cd009.b88c6","filebrowser":"dc6eab6d.8c7af8","rules":[{"t":"dc6eab6d.8c7af8"},{"t":"18c25d51.7c73e3"}],"x":750,"y":880,"wires":[["d446bb38.b2df88"]]},{"id":"d446bb38.b2df88","type":"function","z":"d47cd009.b88c6","name":"Pivot format","func":"var responseData = msg.payload;\nvar OPENMEDIA_MAPPING = flow.get(\"OPENMEDIA_MAPPING\");\n//we have to regenerate the fullpath for each item in the breadcrumb\n\n\nvar breadcrumb = [];\n    for (i=0;i<msg.fullPath.length;i++){\n        breadcrumb.push({\n                name:msg.fullPath[i].name,\n                payload:{\n                 remotePath: msg.fullPath[i].remotePath,\n                 fullPath: msg.fullPath.slice(0, i+1)\n             },\n             active:(i<msg.fullPath.length -1) ? false:true\n            })     \n    }\n\n\n\n\nmsg.payload = {\n  resultCount: responseData.items.length + \" results\",\n  navigation: {\n    breadcrumb: breadcrumb\n  },\n  dropdown: [],\n  items: []\n};\n\n\n\nif (responseData.items.length > 0) {\n    responseData.items =  sortResults(responseData, msg.sort, msg.sort_ascending);\n    node.warn(responseData);\n  for (var i = 0; i < responseData.items.length; i++) {\n      var type = responseData.items[i].type;\n      var mime = responseData.items[i].mime;\n      var iconName = \"file\"; // default icon \n      var destTempateType = \"default\";\n  if  (mime) {\n    if (mime.startsWith(\"audio\")) {\n        iconName = \"music\";\n        destTempateType =\"audio\"\n    }\n    if (mime.startsWith(\"video\")) {\n        iconName = \"video\"; \n        destTempateType =\"video\"\n    }\n    if (mime.startsWith(\"image\")) {\n        iconName = \"image\";   \n        destTempateType =\"image\"\n    }\n  }\n      \n    msg.payload.items.push({\n      key: responseData.items[i].id.videoId,\n      rawData: responseData.items[i],\n      presentation: {\n        title: responseData.items[i].name,\n        iconName: type===\"folder\" ? \"folder\" : iconName,\n        \n        iconColor:  type===\"folder\" ? \"yellow\" : \"grey\",\n        mediaType: null,\n    //    thumbnail: responseData.items[i].snippet.thumbnails.medium.url,\n    //    highres: responseData.items[i].snippet.thumbnails.high.url,\n        meta: [\n          {\n            name: \"Type\",\n            value: type===\"folder\" ? \"FOLDER\" : responseData.items[i].filename.split(\".\").pop().toUpperCase(),\n            icon: null\n          },\n          {\n            name: \"Last modified\",\n            value: timeAgo(responseData.items[i].lastmod),\n            icon: \"clock\"\n          },\n          {\n            name: \"Size\",\n           value: formattedSize(responseData.items[i].size),\n            icon: null\n          }\n          \n          \n        ]\n      },\n      navigation: {\n        type: type===\"folder\" ?\"query\" : \"external\", // possible value: external then value must be a string self, query must be a JSON object, openmedia\n         payload: type===\"folder\" ? {\n             remotePath: responseData.items[i].id,\n             fullPath: msg.fullPath.concat(\n                 {\n                     name:responseData.items[i].name,\n                     remotePath: responseData.items[i].id\n                 })\n         } : responseData.items[i].url\n        //type:\"query\",\n        //value:nextResultsPayload\n      },\n      openmedia: {\n        templateId: OPENMEDIA_MAPPING.templateId[destTempateType],\n        poolId: OPENMEDIA_MAPPING.poolId,\n        folderId: OPENMEDIA_MAPPING.folderId,\n        systemId: OPENMEDIA_MAPPING.systemId,\n        OMISCommandAfterCreation: {\n          pluginID: OPENMEDIA_MAPPING.OMISCommandAfterCreation.pluginID,\n          commandID: OPENMEDIA_MAPPING.OMISCommandAfterCreation.commandID\n        },\n        fields: [\n          {\n              //title\n            fieldId: OPENMEDIA_MAPPING.fields.title,\n            valueType:1,\n            value: responseData.items[i].name\n          },\n           {\n               //External Unique ID\n            fieldId: OPENMEDIA_MAPPING.fields.id,\n            valueType:1,\n           value: responseData.items[i].url\n          },\n           {\n               //url\n            fieldId: OPENMEDIA_MAPPING.fields.url,\n            valueType:1,\n           value: responseData.items[i].url\n          }\n        ]\n      },\n      dragAndDrop: []\n    });\n    \n   \n  }\n}\n\nreturn msg;\n\n\n\nfunction sortResults(responseData, prop, asc) {\n    \n    return responseData.items.sort(function(a, b) {\n        if (asc) {\n            return (a[prop] > b[prop]) ? 1 : ((a[prop] < b[prop]) ? -1 : 0);\n        } else {\n            return (b[prop] > a[prop]) ? 1 : ((b[prop] < a[prop]) ? -1 : 0);\n        }\n    });\n\n \n    \n}\n\n\n\n\nfunction getFormattedDate(date, prefomattedDate = false, hideYear = false) {\n        const MONTH_NAMES = [\n  'January', 'February', 'March', 'April', 'May', 'June',\n  'July', 'August', 'September', 'October', 'November', 'December'\n];\n    \n  const day = date.getDate();\n  const month = MONTH_NAMES[date.getMonth()];\n  const year = date.getFullYear();\n  const hours = date.getHours();\n  let minutes = date.getMinutes();\n\n  if (minutes < 10) {\n    // Adding leading zero to minutes\n    minutes = `0${ minutes }`;\n  }\n\n  if (prefomattedDate) {\n    // Today at 10:20\n    // Yesterday at 10:20\n    return `${ prefomattedDate } at ${ hours }:${ minutes }`;\n  }\n\n  if (hideYear) {\n    // 10. January at 10:20\n    return `${ day }. ${ month } at ${ hours }:${ minutes }`;\n  }\n\n  // 10. January 2017. at 10:20\n  return `${ day }. ${ month } ${ year }. at ${ hours }:${ minutes }`;\n}\n\n\n// --- Main function\nfunction timeAgo(dateParam) {\n\n\n  if (!dateParam) {\n    return null;\n  }\n\n  const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam);\n  const DAY_IN_MS = 86400000; // 24 * 60 * 60 * 1000\n  const today = new Date();\n  const yesterday = new Date(today - DAY_IN_MS);\n  const seconds = Math.round((today - date) / 1000);\n  const minutes = Math.round(seconds / 60);\n  const isToday = today.toDateString() === date.toDateString();\n  const isYesterday = yesterday.toDateString() === date.toDateString();\n  const isThisYear = today.getFullYear() === date.getFullYear();\n\n\n  if (seconds < 5) {\n    return 'now';\n  } else if (seconds < 60) {\n    return `${ seconds } seconds ago`;\n  } else if (seconds < 90) {\n    return 'about a minute ago';\n  } else if (minutes < 60) {\n    return `${ minutes } minutes ago`;\n  } else if (isToday) {\n    return getFormattedDate(date, 'Today'); // Today at 10:20\n  } else if (isYesterday) {\n    return getFormattedDate(date, 'Yesterday'); // Yesterday at 10:20\n  } else if (isThisYear) {\n    return getFormattedDate(date, false, true); // 10. January at 10:20\n  }\n\n  return getFormattedDate(date); // 10. January 2017. at 10:20\n}\n\nfunction formattedSize(size) {\n    //dividing by 10 afterwards give the first decimal\n    if (size > 1000000000000) return Math.round(size/100000000000)/10 + \" TB\"\n    if (size > 1000000000) return Math.round(size/100000000)/10 + \" GB\"\n    if (size > 1000000) return Math.round(size/100000)/10 + \" MB\"\n    if (size > 1000) return Math.round(size/100)/10 + \" KB\"\n    else return size + \" KB\"\n}","outputs":1,"noerr":0,"x":990,"y":880,"wires":[["45fcec2e.8cd0d4","67009b70.ae2884"]]},{"id":"5bec960c.c18a38","type":"function","z":"d47cd009.b88c6","name":"Found in cache ?","func":"//first output uses cache data\nif (msg.hasOwnProperty(\"payload\") && !!msg.payload){\n    return [msg,null];\n}else{\n    //second output goes to actual repository search\n    return [null, msg];\n}\n\n","outputs":2,"noerr":0,"x":730,"y":820,"wires":[["45fcec2e.8cd0d4"],["77336ef7.0f9ac"]]},{"id":"d16ff06.c3c371","type":"Cache in","z":"d47cd009.b88c6","name":"rep1","cache":"1708125e.da84de","keyType":"msg","keyProperty":"cached_query","valueType":"msg","valueProperty":"payload","useString":false,"x":530,"y":860,"wires":[["5bec960c.c18a38"]]},{"id":"67009b70.ae2884","type":"Cache out","z":"d47cd009.b88c6","name":"rep1 ","cache":"1708125e.da84de","keyType":"msg","keyProperty":"cached_query","valueType":"msg","valueProperty":"payload","ttlType":"msg","ttlProperty":"","useString":false,"x":1170,"y":920,"wires":[]},{"id":"5984edf3.7f6424","type":"function","z":"d47cd009.b88c6","name":"Prepare Query","func":"msg.cached_query= JSON.stringify(msg.payload);\nmsg.fullPath = msg.payload.fullPath||[{name:\"Home\",remotePath:\"\"}]\nmsg.remotePath  = msg.payload.remotePath||\"\";\nmsg.fulltextEngineURL = flow.get(\"FULLTEXTENGINE_URL\");\nif (msg.payload.useFulltext===\"on\") {\nmsg.useFulltext =  \"true\";\nmsg.repIx  = msg.payload.repIx||\"1\";\nnode.warn(\"Rep IX :\" + msg.repIx)\n    \n}\nelse \n{\n    msg.useFulltext =  \"false\"\n}\n\n\nmsg.query       = msg.payload.query||\"\";\nif (msg.query  ===\"\") {\n    msg.operation   = \"list\";\n}\nelse {\n    msg.operation   = \"find\";\n    if (msg.payload.hasOwnProperty(\"breadcrumb\")){\n    msg.remotePath = msg.payload.breadcrumb[msg.payload.breadcrumb.length-1].payload.remotePath;\n    }\n}\nmsg.sort            = msg.payload.sort;\"lastmod\"; //type, name, size, mime, lastmod\nif (msg.payload.sortorder == \"asc\"){\n    msg.sort_ascending = true;\n}\nelse {\n    msg.sort_ascending = false;  \n}\n\nmsg.payload = {};\nmsg.payload.filedata= '{}'; // Needs to be a string\nreturn msg;","outputs":1,"noerr":0,"x":320,"y":860,"wires":[["d16ff06.c3c371"]]},{"id":"c19d0fd2.f6761","type":"http in","z":"d47cd009.b88c6","name":"repository","url":"/repository2","method":"get","upload":false,"swaggerDoc":"","x":80,"y":860,"wires":[["5984edf3.7f6424"]]},{"id":"dc6eab6d.8c7af8","type":"filebrowser","z":"","repositoryType":"SMB","share":"\\\\myComputer\\public","port":"445","domain":"","username":"MyUsername","password":"MyPassword","workdir":"","name":"My Computer","sshKeyPath":"","additionalLftpCommands":""},{"id":"1708125e.da84de","type":"Cache","z":"","name":"rep1","defaultTtl":"60","checkPeriod":"60"}]
```

# Configuration #

## Add/Configure Repositories

Each Repository can be configured within the filebrowser node. The first drowdown "Repositories" allows to create or modify existing repository entries. 

![screenshot](https://user-images.githubusercontent.com/35899018/49732022-7e026d80-fc7d-11e8-8cb0-ec0444b5db37.png)

Once a repository has been created (and the filebrowser widget has been relaunched ) the repository can be added to the list of accessible repositories. Each accessible repository has an unique (ascending) number. This is required to identify the approperiate repository later on. The order of accessible repositores can be changed. Please take note that this will also require an to adjust the URL to the repository (Admin Tool)

### SMB/CIFS Shares 

![screenshot](https://user-images.githubusercontent.com/35899018/49731401-83f74f00-fc7b-11e8-98b5-c3bc5f518fc3.png)



- Type: Select Type to "SMB"
- Name: Select a Display name which will appear in the list of avaliable repositories
- Share: Set the SMB\CIFS\UNC Share
- Port: Default Port is 445 
- Working Directory: Set entry path/working directory if needed
- User/Password/Domain: Enter the user credentials. **ATTENTION:** Please ensure, that the account has R/RW access to the Share on OS-Level (Windows Credentails Manager):

![screenshot](https://user-images.githubusercontent.com/35899018/49731751-a89ff680-fc7c-11e8-803d-792a07a8978d.png)

#### Indexing 

"Everything" is search engine (tool) that locates files and folders by filename instantly for Windows.  "Everything" provides a built-in HTTP web server that allows searching and accessing files from a web browser. Search parameters can  be sent via http queries and the respone is a JSON. 
This mechanism is being used by the filebrowser node. It can query the content of a specific repsoitory instead of re-scanning all subfolders again, which safes time (especially with huge repositories)

In order to configure the search engine for SMB/CIFS repositories (FTP/SFTP/FTPS not supported) do the following:

- Install "Everything" (https://www.voidtools.com/downloads/). Select following options during installation

![screenshot](https://user-images.githubusercontent.com/35899018/49744414-02181d80-fc9d-11e8-8b36-a62ffeb33e48.png)


- Add the SMB/CIFS Repository under Tools>>>Options>Folders >> Add
![screenshot](https://user-images.githubusercontent.com/35899018/49738377-8b285800-fc8f-11e8-9445-408a5f8c40ab.png)

- New Shares can be added by simply typing the full share name (e.g. \\\SHARE1). Please ensure that the Share-name matches with the configured repository name in the filebrowser node 
- Set an approperiate port number 
- Start "Everything" from the TaskManager, using the spec. user account (where  credentials are set in the windows credential manager):

![screenshot](https://user-images.githubusercontent.com/35899018/49738988-0fc7a600-fc91-11e8-9cb8-7a2ea19c42a1.png)

- To test try to connect to the HTTP Endpoint:



![screenshot](https://user-images.githubusercontent.com/35899018/49739155-7baa0e80-fc91-11e8-8b21-c94eab9b5163.png)

### FTP Shares 

To create a new FTP repository set the Repsoitory Type to "FTP":

![screenshot](https://user-images.githubusercontent.com/35899018/49739548-6ed9ea80-fc92-11e8-8beb-fca6081a8e27.png)

- Name: Select a Display name which will appear in the list of avaliable repositories
- FTP Protocol: Set a protocol type (ftp/ftps/sftp)
- Share: Set FTP Host name or IP-Adress Share
- Port: Default Port is 21 for FTP, 22 for SFTP and 990 for FTPS 
- Working Directory: Set entry path/working directory if needed
- Additional FTP Commands: Additional commands to pass to lftp, splitted by ';' 

# Caching #
In order to improve performance and to limit your API key usage, the cache node allows you to configure the duration during which a request and its answer remain in the cache. The default is 60 seconds.
![screenshot_11](https://user-images.githubusercontent.com/30046324/48259178-78fc9500-e417-11e8-90f9-d37c4232442d.png)

# Security #
If your flow is available on the public internet, don't forget to [secure your Node-Red instance](https://nodered.org/docs/security) ! 

