├── .gitignore ├── .nojekyll ├── .travis.yml ├── API.md ├── LICENSE ├── README.md ├── bridges ├── java │ ├── resources │ │ ├── log4j2.xml │ │ └── spring-security.xml │ ├── src │ │ └── main │ │ │ └── com │ │ │ └── monosand │ │ │ └── drinkout │ │ │ └── web │ │ │ └── servlet │ │ │ └── AngularFileManagerServlet.java │ └── webapp │ │ ├── META-INF │ │ └── context.xml │ │ └── WEB-INF │ │ └── web.xml ├── openwrt-lua │ ├── etc │ │ └── config │ │ │ └── filemanager │ ├── usr │ │ └── lib │ │ │ └── lua │ │ │ ├── cgi.lua │ │ │ └── luafm.lua │ └── www │ │ └── cgi-bin │ │ ├── fs │ │ ├── fs-download │ │ └── fs-upload ├── php-local │ ├── LocalBridge │ │ ├── FileManagerApi.php │ │ ├── Response.php │ │ ├── Rest.php │ │ └── Translate.php │ ├── index.php │ └── lang │ │ ├── de.json │ │ ├── en.json │ │ ├── fr.json │ │ ├── it.json │ │ ├── ko.json │ │ └── sk.json └── php │ ├── handler.php │ └── includes │ ├── ExceptionCatcher.php │ └── Ftp.php ├── docs └── index.html ├── gulpfile.js ├── header.txt ├── index.html ├── package-lock.json ├── package.json └── src ├── css ├── animations.scss ├── dialogs.scss ├── main.scss ├── sprite.scss ├── style.scss └── variables.scss ├── js ├── app.js ├── controllers │ ├── main.js │ └── selector-controller.js ├── directives │ └── directives.js ├── entities │ ├── chmod.js │ └── item.js ├── filters │ └── filters.js ├── providers │ ├── config.js │ └── translations.js └── services │ ├── apihandler.js │ ├── apimiddleware.js │ └── filenavigator.js ├── svg ├── chevron-down.svg ├── download.svg ├── ext │ ├── css.svg │ ├── doc.svg │ ├── html.svg │ ├── jpg.svg │ ├── js.svg │ ├── pdf.svg │ ├── png.svg │ ├── txt.svg │ ├── unknown.svg │ └── zip.svg ├── folder.svg ├── folder_closed.svg ├── folder_open.svg ├── grid.svg ├── language.svg ├── list.svg ├── more.svg ├── positive.svg ├── search.svg └── upload.svg └── templates ├── current-folder-breadcrumb.html ├── item-context-menu.html ├── main-icons.html ├── main-table-modal.html ├── main-table.html ├── main.html ├── modals.html ├── navbar.html ├── sidebar.html └── spinner.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /nbproject/private/ 3 | nbproject/ 4 | node_modules/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | 5 | before_script: 6 | - npm install 7 | 8 | script: 9 | - gulp lint 10 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## angular-filemanager API docs [multiple file support] 2 | 3 | #### Listing (URL: fileManagerConfig.listUrl, Method: POST) 4 | 5 | **JSON Request content** 6 | ```json 7 | { 8 | "action": "list", 9 | "path": "/public_html" 10 | } 11 | ``` 12 | **JSON Response** 13 | ```json 14 | { "result": [ 15 | { 16 | "name": "magento", 17 | "rights": "drwxr-xr-x", 18 | "size": "4096", 19 | "date": "2016-03-03 15:31:40", 20 | "type": "dir" 21 | }, { 22 | "name": "index.php", 23 | "rights": "-rw-r--r--", 24 | "size": "549923", 25 | "date": "2016-03-03 15:31:40", 26 | "type": "file" 27 | } 28 | ]} 29 | ``` 30 | -------------------- 31 | #### Rename (URL: fileManagerConfig.renameUrl, Method: POST) 32 | **JSON Request content** 33 | ```json 34 | { 35 | "action": "rename", 36 | "item": "/public_html/index.php", 37 | "newItemPath": "/public_html/index2.php" 38 | } 39 | ``` 40 | **JSON Response** 41 | ```json 42 | { "result": { "success": true, "error": null } } 43 | ``` 44 | -------------------- 45 | #### Move (URL: fileManagerConfig.moveUrl, Method: POST) 46 | **JSON Request content** 47 | ```json 48 | { 49 | "action": "move", 50 | "items": ["/public_html/libs", "/public_html/config.php"], 51 | "newPath": "/public_html/includes" 52 | } 53 | ``` 54 | **JSON Response** 55 | ```json 56 | { "result": { "success": true, "error": null } } 57 | ``` 58 | -------------------- 59 | #### Copy (URL: fileManagerConfig.copyUrl, Method: POST) 60 | **JSON Request content** 61 | ```json 62 | { 63 | "action": "copy", 64 | "items": ["/public_html/index.php", "/public_html/config.php"], 65 | "newPath": "/includes", 66 | "singleFilename": "renamed.php" <-- (only present in single selection copy) 67 | } 68 | ``` 69 | **JSON Response** 70 | ```json 71 | { "result": { "success": true, "error": null } } 72 | ``` 73 | -------------------- 74 | #### Remove (URL: fileManagerConfig.removeUrl, Method: POST) 75 | **JSON Request content** 76 | ```json 77 | { 78 | "action": "remove", 79 | "items": ["/public_html/index.php"], 80 | } 81 | ``` 82 | **JSON Response** 83 | ```json 84 | { "result": { "success": true, "error": null } } 85 | ``` 86 | -------------------- 87 | #### Edit file (URL: fileManagerConfig.editUrl, Method: POST) 88 | **JSON Request content** 89 | ```json 90 | { 91 | "action": "edit", 92 | "item": "/public_html/index.php", 93 | "content": "" 94 | } 95 | ``` 96 | **JSON Response** 97 | ```json 98 | { "result": { "success": true, "error": null } } 99 | ``` 100 | -------------------- 101 | #### Get content of a file (URL: fileManagerConfig.getContentUrl, Method: POST) 102 | **JSON Request content** 103 | ```json 104 | { 105 | "action": "getContent", 106 | "item": "/public_html/index.php" 107 | } 108 | ``` 109 | **JSON Response** 110 | ```json 111 | { "result": "" } 112 | ``` 113 | -------------------- 114 | #### Create folder (URL: fileManagerConfig.createFolderUrl, Method: POST) 115 | **JSON Request content** 116 | ```json 117 | { 118 | "action": "createFolder", 119 | "newPath": "/public_html/new-folder" 120 | } 121 | ``` 122 | **JSON Response** 123 | ```json 124 | { "result": { "success": true, "error": null } } 125 | ``` 126 | -------------------- 127 | #### Set permissions (URL: fileManagerConfig.permissionsUrl, Method: POST) 128 | **JSON Request content** 129 | ```json 130 | { 131 | "action": "changePermissions", 132 | "items": ["/public_html/root", "/public_html/index.php"], 133 | "perms": "rw-r-x-wx", 134 | "permsCode": "653", 135 | "recursive": true 136 | } 137 | ``` 138 | **JSON Response** 139 | ```json 140 | { "result": { "success": true, "error": null } } 141 | ``` 142 | -------------------- 143 | #### Compress file (URL: fileManagerConfig.compressUrl, Method: POST) 144 | **JSON Request content** 145 | ```json 146 | { 147 | "action": "compress", 148 | "items": ["/public_html/photos", "/public_html/docs"], 149 | "destination": "/public_html/backups", 150 | "compressedFilename": "random-files.zip" 151 | }} 152 | ``` 153 | **JSON Response** 154 | ```json 155 | { "result": { "success": true, "error": null } } 156 | ``` 157 | -------------------- 158 | #### Extract file (URL: fileManagerConfig.extractUrl, Method: POST) 159 | **JSON Request content** 160 | ```json 161 | { 162 | "action": "extract", 163 | "destination": "/public_html/extracted-files", 164 | "item": "/public_html/compressed.zip", 165 | "folderName": "extract_dir" 166 | } 167 | ``` 168 | **JSON Response** 169 | ```json 170 | { "result": { "success": true, "error": null } } 171 | ``` 172 | -------------------- 173 | #### Upload file (URL: fileManagerConfig.uploadUrl, Method: POST, Content-Type: multipart/form-data) 174 | 175 | **Http post request payload** 176 | ``` 177 | ------WebKitFormBoundaryqBnbHc6RKfXVAf9j 178 | Content-Disposition: form-data; name="destination" 179 | / 180 | 181 | ------WebKitFormBoundaryqBnbHc6RKfXVAf9j 182 | Content-Disposition: form-data; name="file-0"; filename="github.txt" 183 | Content-Type: text/plain 184 | ``` 185 | **JSON Response** 186 | ```json 187 | { "result": { "success": true, "error": null } } 188 | ``` 189 | 190 | Unlimited file items to upload, each item will be enumerated as file-0, file-1, etc. 191 | 192 | For example, you may retrieve the file in PHP using: 193 | ```php 194 | $destination = $_POST['destination']; 195 | $_FILES['file-0'] or foreach($_FILES) 196 | ``` 197 | -------------------- 198 | #### Download / Preview file (URL: fileManagerConfig.downloadMultipleUrl, Method: GET) 199 | **Http query params** 200 | ``` 201 | [fileManagerConfig.downloadFileUrl]?action=download&path=/public_html/image.jpg 202 | ``` 203 | **Response** 204 | ``` 205 | -File content 206 | ``` 207 | -------------------- 208 | #### Download multiples files in ZIP/TAR (URL: fileManagerConfig.downloadFileUrl, Method: GET) 209 | **JSON Request content** 210 | ```json 211 | { 212 | "action": "downloadMultiple", 213 | "items": ["/public_html/image1.jpg", "/public_html/image2.jpg"], 214 | "toFilename": "multiple-items.zip" 215 | }} 216 | ``` 217 | **Response** 218 | ``` 219 | -File content 220 | ``` 221 | -------------------- 222 | ##### Errors / Exceptions 223 | Any backend error should be with an error 500 HTTP code. 224 | 225 | Btw, you can also report errors with a 200 response both using this json structure 226 | ```json 227 | { "result": { 228 | "success": false, 229 | "error": "Access denied to remove file" 230 | }} 231 | ``` 232 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 joni2back 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularJS File Manager 2 | 3 | 4 | [![Build Status](https://travis-ci.com/azerafati/angularjs-filemanager.svg?branch=master)](https://travis-ci.com/azerafati/angularjs-filemanager) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8750d04905bd484fa7d422536558f492)](https://www.codacy.com/manual/azerafati/angularjs-filemanager?utm_source=github.com&utm_medium=referral&utm_content=azerafati/angularjs-filemanager&utm_campaign=Badge_Grade) 6 | [![npm version](https://img.shields.io/npm/v/@azerafati/angularjs-filemanager.svg)](https://www.npmjs.com/package/angularjs-filemanager) 7 | [![GitHub issues](https://img.shields.io/github/issues/azerafati/angularjs-filemanager)](https://github.com/azerafati/angularjs-filemanager/issues) 8 | ![Dependency Status](https://img.shields.io/david/azerafati/angularjs-filemanager) 9 | [![Coverage Status](https://coveralls.io/repos/github/azerafati/angularjs-filemanager/badge.svg?branch=master)](https://coveralls.io/github/azerafati/angularjs-filemanager?branch=master) 10 | ![npm](https://img.shields.io/npm/dt/@azerafati/angularjs-filemanager) 11 | [![GitHub license](https://img.shields.io/github/license/azerafati/angularjs-filemanager)](https://github.com/azerafati/angularjs-filemanager/blob/master/LICENSE) 12 | 13 | 14 | This is a minimal and light-weight file manager to explore your files on the server developed in AngularJS using Bootstrap 4 15 | 16 | This project is based on excellent work of [Jonas Sciangula Street](https://github.com/joni2back), but since he's not planning to maintain it I'm moving forward with my own fork. 17 | 18 | *There are example backend connectors in (e.g. PHP, PHP-FTP, Python, Java) or you can create your own backend connector following the [connector API](API.md).* 19 | 20 | 21 | 22 | 23 | ### [Try the DEMO](https://angularjs-filemanager.azerafati.com/) 24 | ![](https://repository-images.githubusercontent.com/59879464/8605c980-e2f2-11e9-8d42-57f40cd27d8c "AngularJS File Manager") 25 | --------- 26 | 27 | ### Install 28 | 29 | **1) Run `npm i @azerafati/angularjs-filemanager --save`** 30 | 31 | **2) Install dependencies** 32 | 33 | **3) Include the dependencies in your project** 34 | ```html 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ``` 46 | **4) Use the angular directive in your HTML** 47 | ```html 48 | 49 | ``` 50 | ```javascript 51 | angular.module('AngularJS-FileManager').config(['fileManagerConfigProvider', function (config) { 52 | var defaults = config.$get(); 53 | config.setBridge('/bridges/php-local/index.php'); 54 | config.set({ 55 | appName: 'AngularJS File Manager', 56 | pickCallback: function(item) { 57 | var msg = 'Picked %s "%s" for external use' 58 | .replace('%s', item.type) 59 | .replace('%s', item.fullPath()); 60 | window.alert(msg); 61 | }, 62 | 63 | allowedActions: angular.extend(defaults.allowedActions, { 64 | pickFiles: true, 65 | pickFolders: false, 66 | }), 67 | }); 68 | }]); 69 | ``` 70 | 71 | --------- 72 | 73 | ### Features 74 | - Multiple file support 75 | - Multilanguage 76 | - List and Icon view 77 | - Multiple file upload 78 | - Pick files callback for third parties apps 79 | - Search files 80 | - Directory tree navigation 81 | - Copy, Move, Rename (Interactive UX) 82 | - Delete, Edit, Preview, Download 83 | - File permissions (Unix chmod style) 84 | - Mobile support 85 | 86 | ### TODO 87 | - Drag and drop 88 | - Dropbox and Google Drive connectors 89 | - Remove usage of jQuery 90 | 91 | ### Backend API 92 | [Read the docs](API.md) 93 | 94 | --------- 95 | 96 | ### Create a new build with your changes 97 | ```sh 98 | gulp build || node node_modules/gulp/bin/gulp.js build 99 | ``` 100 | 101 | You can do many things by extending the configuration. Like hide the sidebar or the search button. See [the list of default configurations](/src/js/providers/config.js). 102 | 103 | --------- 104 | 105 | ### Contribute 106 | To contribute to the project you can simply fork this repo. To build a minified version, you can simply run the Gulp 107 | task `gulp build`. The minified/uglified files are created in the `dist` folder. 108 | 109 | ### Versioning 110 | For transparency into our release cycle and in striving to maintain backward compatibility, angular-filemanager is maintained under [the Semantic Versioning guidelines](http://semver.org/). 111 | 112 | ### Copyright and license 113 | Code and documentation released under [the MIT license](https://github.com/joni2back/angular-filemanager/blob/master/LICENSE). 114 | -------------------------------------------------------------------------------- /bridges/java/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | %d %p [%t] %C{1.}.%M(%L) %m%n%ex{full}%n 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /bridges/java/resources/spring-security.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /bridges/java/webapp/META-INF/context.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bridges/java/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | angular-filemanager 5 | 6 | contextConfigLocation 7 | classpath:spring-security.xml 8 | 9 | 10 | Spring Security filter 11 | springSecurityFilterChain 12 | org.springframework.web.filter.DelegatingFilterProxy 13 | 14 | 15 | springSecurityFilterChain 16 | /* 17 | 18 | 19 | org.springframework.web.context.ContextLoaderListener 20 | 21 | 22 | 23 | AngularFileManagerServlet 24 | com.monosand.drinkout.web.servlet.AngularFileManagerServlet 25 | 26 | repository.base.path 27 | /tmp 28 | 29 | 30 | date.format 31 | yyyy-MM-dd hh:mm:ss 32 | 33 | 34 | enabled.action 35 | rename,move,remove,edit,createFolder,changePermissions,compress,extract,copy,upload 36 | 37 | 1 38 | 39 | 40 | AngularFileManagerServlet 41 | /bridges/php/handler.php 42 | 43 | 44 | index.html 45 | 46 | 47 | -------------------------------------------------------------------------------- /bridges/openwrt-lua/etc/config/filemanager: -------------------------------------------------------------------------------- 1 | 2 | config filemanager 'config' 3 | option basedir '/mnt' 4 | option tmpdir '/tmp' 5 | -------------------------------------------------------------------------------- /bridges/openwrt-lua/usr/lib/lua/cgi.lua: -------------------------------------------------------------------------------- 1 | -- cgi util module 2 | 3 | local uci = require("uci") 4 | local u_c = uci.cursor() 5 | local tmppath = u_c.get("filemanager","config","tmpdir") 6 | 7 | local prevbuf = "" 8 | local blocksize = 4096 9 | local _M = {} 10 | 11 | _M.statusmsg = { 12 | [200] = "OK", 13 | [206] = "Partial Content", 14 | [301] = "Moved Permanently", 15 | [302] = "Found", 16 | [304] = "Not Modified", 17 | [400] = "Bad Request", 18 | [403] = "Forbidden", 19 | [404] = "Not Found", 20 | [405] = "Method Not Allowed", 21 | [408] = "Request Time-out", 22 | [411] = "Length Required", 23 | [412] = "Precondition Failed", 24 | [416] = "Requested range not satisfiable", 25 | [500] = "Internal Server Error", 26 | [503] = "Server Unavailable", 27 | } 28 | 29 | -- call this function passing an empy table. use that table for successive calls. 30 | function _M.new(req) 31 | req.content_length = os.getenv("CONTENT_LENGTH") 32 | req.request_method = os.getenv("REQUEST_METHOD") 33 | if req.request_method == "POST" then 34 | req.content_type, req.boundary = string.match(os.getenv("CONTENT_TYPE"),"^(multipart/form%-data); boundary=\"?(.+)\"?$") 35 | req.boundary = "--" .. req.boundary 36 | end 37 | -- this is useful only if you have /tmp on tmpfs like in openwrt. otherwise can be set to "" 38 | req.tempdir = tmppath 39 | req.post = {} 40 | end 41 | 42 | -- this function is needed to clean temp file since and hide implementation details 43 | function _M.cleanup(req) 44 | for k, v in pairs(req.post) do 45 | for j, v in pairs(req.post[k]) do 46 | if req.post[k][j].tempname then 47 | os.remove(req.post[k][j].tempname) -- if file unused 48 | os.remove("/tmp/" .. string.match(req.post[k][j].tempname,"^" .. req.tempdir .. "(.+)")) 49 | end 50 | end 51 | end 52 | end 53 | 54 | -- del: delimiter 55 | -- return chunk (string), found (boolean) 56 | local function chunkread(del) 57 | local buf, found = 0 58 | local del = del or "\r\n" 59 | 60 | buf = io.read(math.max(0,blocksize + #del - #prevbuf)) 61 | if prevbuf ~= "" then buf = prevbuf .. ( buf or "" ); prevbuf = "" end 62 | if not buf then return end 63 | 64 | s, e = string.find(buf,del,1,true) 65 | if s and e then 66 | found = 1 67 | prevbuf = string.sub(buf,e+1) 68 | buf = string.sub(buf,1,s-1) 69 | else 70 | prevbuf = string.sub(buf,math.min(blocksize,#buf)+1) 71 | buf = string.sub(buf,1,math.min(blocksize,#buf)) 72 | end 73 | 74 | return buf, found 75 | end 76 | 77 | 78 | function _M.parse_request_body (req) 79 | local chunk, found, type, tempname, tempfile 80 | local param = {} 81 | 82 | -- read first boundary line 83 | chunk, found = chunkread(req.boundary) 84 | chunk, found = chunkread("\r\n") 85 | while chunk == "" do 86 | -- read part headers and get parameters value 87 | repeat 88 | chunk, found = chunkread("\r\n") 89 | if not found then return 400, "Malformed POST. Missing Part Header or Part Header too long." end 90 | string.gsub(chunk, ';%s*([^%s=]+)="(.-[^\\])"', function(k, v) param[k] = v end) 91 | param.type = param.type or string.match(chunk, "^Content%-Type: (.+)$") 92 | until chunk == "" 93 | 94 | -- prepare file data read 95 | if not param.name then return 400, "Malformed POST. Check Header parameters." end 96 | param.size=0 97 | param.tempname = req.tempdir .. string.match(os.tmpname(), "^/tmp(.+)") 98 | tempfile = io.open(param.tempname, "w") 99 | 100 | -- read part body content until boundary 101 | repeat 102 | chunk, found = chunkread("\r\n" .. req.boundary) 103 | if not chunk then return 400, "Malformed POST. Incomplete Part received." end 104 | tempfile:write(chunk) 105 | param.size = param.size + #chunk 106 | until found 107 | tempfile:close() 108 | req.post[param.name] = req.post[param.name] or {} 109 | table.insert(req.post[param.name], 1, param) 110 | param = {} 111 | 112 | -- read after boundary. if CRLF ("") repeat. if "--" end POST processing 113 | chunk, found = chunkread("\r\n") 114 | end 115 | 116 | if found and chunk == "--" then return 0, "OK" end 117 | return 400, "Malformed POST. Boundary not properly ended with CRLF or --." 118 | end 119 | 120 | return _M 121 | -------------------------------------------------------------------------------- /bridges/openwrt-lua/usr/lib/lua/luafm.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local uci = require "uci" 4 | local fs = require "nixio.fs" 5 | local u_c = uci.cursor() 6 | local basepath = u_c.get("filemanager","config","basedir") 7 | 8 | local _M = {} 9 | 10 | local bp = basepath:match("(.*)/") 11 | if bp and bp ~= "" then 12 | basepath = bp 13 | end 14 | _M.basepath = basepath; 15 | 16 | function _M.is_in_dir(file, dir) 17 | if file == dir then 18 | return true 19 | else 20 | return file:sub(1, #dir) == dir 21 | end 22 | end 23 | 24 | function _M.path_valid(path) 25 | return _M.is_in_dir(path,_M.basepath) and fs.access(path,"r") 26 | end 27 | 28 | function _M.dir_path_valid(path) 29 | return _M.is_in_dir(path,_M.basepath) and fs.stat(path,"type")=="dir" and fs.access(path,"w") 30 | end 31 | 32 | function _M.new_path_valid(path) 33 | local dirpath = fs.dirname(path) 34 | return _M.is_in_dir(dirpath,_M.basepath) and fs.access(dirpath,"w") 35 | end 36 | 37 | function _M.make_path(path) 38 | local realpath = fs.realpath(_M.basepath..'/'..path) 39 | if _M.path_valid(realpath) then 40 | return realpath 41 | else 42 | return nil 43 | end 44 | end 45 | 46 | function _M.make_new_path(path) 47 | local realpath = fs.realpath(fs.dirname(_M.basepath..'/'..path))..'/'..fs.basename(path) 48 | if _M.new_path_valid(realpath) then 49 | return realpath 50 | else 51 | return nil 52 | end 53 | end 54 | 55 | function _M.make_dir_path(path) 56 | local realpath = fs.realpath(_M.basepath..'/'..path) 57 | if _M.dir_path_valid(realpath) then 58 | return realpath 59 | else 60 | return nil 61 | end 62 | end 63 | 64 | function _M.rm(item) 65 | local ftype = fs.stat(item,"type") 66 | if ftype == "reg" then 67 | return fs.remove(item) 68 | elseif ftype == "dir" then 69 | local dir = fs.dir(item) 70 | for file in dir do 71 | if not _M.rm(item..'/'..file) then 72 | return false 73 | end 74 | end 75 | return fs.rmdir(item) 76 | else 77 | return false 78 | end 79 | end 80 | 81 | function _M.chmod(item,mode,recursive) 82 | local result = fs.chmod(item,mode) 83 | if result and recursive then 84 | local dir = fs.dir(item) 85 | if dir then 86 | for file in dir do 87 | local ftype = fs.stat(item..'/'..file,"type") 88 | if ftype == "dir" then 89 | result = _M.chmod(item..'/'..file,mode,recursive) 90 | elseif ftype == "reg" then 91 | result = _M.chmod(item..'/'..file,string.gsub(mode,"x","-"),false) 92 | end 93 | if not result then 94 | break 95 | end 96 | end 97 | end 98 | end 99 | return result 100 | end 101 | 102 | return _M 103 | -------------------------------------------------------------------------------- /bridges/openwrt-lua/www/cgi-bin/fs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local os = require "os" 4 | local fs = require "nixio.fs" 5 | local json = require "json" 6 | local luafm = require("luafm") 7 | 8 | print("Content-type: text/html; charset=utf-8") 9 | print("Cache-control: no-cache") 10 | print("Pragma: no-cache") 11 | 12 | -- prepare the browser for content: 13 | print("\r") 14 | 15 | local result 16 | 17 | local http_headers = nixio.getenv() 18 | local request=io.read("*all") 19 | 20 | local params=json.decode(request) 21 | local action=params["action"] 22 | 23 | local path 24 | local items 25 | local item 26 | local newItemPath 27 | local newPath 28 | local singleFilename 29 | local content 30 | local mode 31 | local destination 32 | local compressedFilename 33 | local recursive 34 | local folderName 35 | 36 | local files 37 | local fstat 38 | local ftype 39 | local basename 40 | local realitem 41 | local command 42 | 43 | if action == "list" then 44 | 45 | path = luafm.make_path(params["path"]) 46 | 47 | if path then 48 | files = {} 49 | local rec 50 | for name in fs.dir(path) do 51 | basename=fs.realpath(path..'/'..name) 52 | if basename then 53 | fstat=fs.stat(basename) 54 | if fstat["type"]=="reg" then 55 | ftype="file" 56 | elseif fstat["type"]=="dir" then 57 | ftype="dir" 58 | else 59 | ftype="" 60 | end 61 | if ftype then 62 | rec={name=name,rights=fstat["modestr"],size=fstat["size"],type=ftype,date=os.date('%Y-%m-%d %H:%M:%S',fstat["mtime"])} 63 | table.insert(files,rec) 64 | end 65 | end 66 | end 67 | result = { result = files } 68 | else 69 | result = { result = {} } 70 | end 71 | 72 | elseif action == "rename" then 73 | 74 | item = luafm.make_path(params["item"]) 75 | newItemPath = luafm.make_new_path(params["newItemPath"]) 76 | 77 | if item and newItemPath and item ~= luafm.basepath and newItemPath ~= luafm.basepath then 78 | result = fs.rename(item,newItemPath) 79 | if result then 80 | result = { result = { success=true, error="" } } 81 | else 82 | result = { result = { success=false, error="Cannot rename requested file/directory" } } 83 | end 84 | else 85 | result = { result = { success=false, error="Invalid path request" } } 86 | end 87 | 88 | elseif action == "move" then 89 | 90 | items = params["items"] 91 | newPath = luafm.make_dir_path(params["newPath"]) 92 | 93 | if newPath then 94 | 95 | result = true 96 | 97 | for key,item in pairs(items) do 98 | 99 | item = luafm.make_path(item) 100 | 101 | if item and item ~= luafm.basepath then 102 | basename = fs.basename(item) 103 | result = fs.move(item,newPath.."/"..basename) 104 | if not result then 105 | break 106 | end 107 | else 108 | result = false 109 | break 110 | end 111 | 112 | end 113 | 114 | if result then 115 | result = { result = { success=true, error="" } } 116 | else 117 | result = { result = { success=false, error="Cannot move requested file/directory" } } 118 | end 119 | 120 | else 121 | 122 | result = { result = { success=false, error="Invalid destination request" } } 123 | 124 | end 125 | 126 | elseif action == "copy" then 127 | 128 | items = params["items"] 129 | newPath = luafm.make_dir_path(params["newPath"]) 130 | 131 | if newPath then 132 | 133 | singleFilename = params["singleFilename"] 134 | if singleFilename then 135 | singleFilename = fs.basename(singleFilename) 136 | end 137 | 138 | result = true 139 | 140 | for key,item in pairs(items) do 141 | 142 | item = luafm.make_path(item) 143 | 144 | if item and item ~= luafm.basepath then 145 | if singleFilename then 146 | basename = singleFilename 147 | else 148 | basename = fs.basename(item) 149 | end 150 | result = fs.copy(item,newPath.."/"..basename) 151 | if not result then 152 | break 153 | end 154 | else 155 | result = false 156 | break 157 | end 158 | 159 | end 160 | 161 | if result then 162 | result = { result = { success=true, error="" } } 163 | else 164 | result = { result = { success=false, error="Cannot copy requested file/directory" } } 165 | end 166 | 167 | else 168 | 169 | result = { result = { success=false, error="Invalid destination request" } } 170 | 171 | end 172 | 173 | elseif action == "remove" then 174 | 175 | items = params["items"] 176 | 177 | result = true 178 | 179 | for key,item in pairs(items) do 180 | 181 | item = luafm.make_path(item) 182 | 183 | if item and item ~= luafm.basepath then 184 | result = luafm.rm(item) 185 | if not result then 186 | break 187 | end 188 | else 189 | result = false 190 | break 191 | end 192 | 193 | end 194 | 195 | if result then 196 | result = { result = { success=true, error="" } } 197 | else 198 | result = { result = { success=false, error="Cannot remove requested file/directory" } } 199 | end 200 | 201 | elseif action == "getContent" then 202 | 203 | item = luafm.make_path(params["item"]) 204 | 205 | if item and item ~= luafm.basepath then 206 | content = fs.readfile(item) 207 | result = { result = content } 208 | else 209 | result = { result = { success=false, error="Invalid path request" } } 210 | end 211 | 212 | elseif action == "edit" then 213 | 214 | item = luafm.make_path(params["item"]) 215 | content = params["content"] 216 | 217 | if item and item ~= luafm.basepath then 218 | result = fs.writefile(item,content) 219 | if result then 220 | result = { result = { success=true, error="" } } 221 | else 222 | result = { result = { success=false, error="Cannot write requested file content" } } 223 | end 224 | else 225 | result = { result = { success=false, error="Invalid path request" } } 226 | end 227 | 228 | elseif action == "createFolder" then 229 | 230 | newPath = luafm.make_new_path(params["newPath"]) 231 | 232 | if newPath and newPath ~= luafm.basepath then 233 | result = fs.mkdir(newPath) 234 | if result then 235 | result = { result = { success=true, error="" } } 236 | else 237 | result = { result = { success=false, error="Cannot create folder" } } 238 | end 239 | else 240 | result = { result = { success=false, error="Invalid path request" } } 241 | end 242 | 243 | elseif action == "changePermissions" then 244 | 245 | items = params["items"] 246 | 247 | if params["perms"] then 248 | mode = params["perms"] 249 | else 250 | mode = params["permsCode"] 251 | end 252 | 253 | if mode then 254 | 255 | recursive = params["recursive"] 256 | 257 | result = true 258 | 259 | for key,item in pairs(items) do 260 | 261 | item = luafm.make_path(item) 262 | 263 | if item and item ~= luafm.basepath then 264 | result = luafm.chmod(item,mode,recursive) 265 | if not result then 266 | break 267 | end 268 | else 269 | result = false 270 | break 271 | end 272 | 273 | end 274 | 275 | if result then 276 | result = { result = { success=true, error="" } } 277 | else 278 | result = { result = { success=false, error="Cannot change permissions" } } 279 | end 280 | 281 | else 282 | 283 | result = { result = { success=false, error="No permission requested" } } 284 | 285 | end 286 | 287 | elseif action == "compress" then 288 | 289 | items = params["items"] 290 | destination = params["destination"] 291 | compressedFilename = params["compressedFilename"] 292 | 293 | newPath = luafm.make_new_path(destination..'/'..fs.basename(params["compressedFilename"])) 294 | 295 | result = true 296 | files = "" 297 | 298 | for key,item in pairs(items) do 299 | 300 | realitem = luafm.make_path(item) 301 | 302 | if realitem and realitem ~= luafm.basepath then 303 | item = item:match("/(.*)") 304 | files = files.." "..item 305 | else 306 | result = false 307 | break 308 | end 309 | 310 | end 311 | 312 | if files then 313 | 314 | command = "cd "..luafm.basepath.."; zip -r "..newPath..files 315 | result = os.execute(command) 316 | 317 | if result then 318 | result = { result = { success=true, error="" } } 319 | else 320 | result = { result = { success=false, error="Archiver returned error" } } 321 | end 322 | 323 | else 324 | 325 | result = { result = { success=false, error="No files selected" } } 326 | 327 | end 328 | 329 | elseif action == "extract" then 330 | 331 | item = luafm.make_path(params["item"]) 332 | destination = params["destination"] 333 | folderName = params["folderName"] 334 | 335 | newPath = luafm.make_new_path(destination..'/'..fs.basename(params["folderName"])) 336 | 337 | result = true 338 | files = "" 339 | 340 | if item and newPath and item ~= luafm.basepath then 341 | command = "unzip "..item.." -d "..newPath 342 | result = os.execute(command) 343 | if result then 344 | result = { result = { success=true, error="" } } 345 | else 346 | result = { result = { success=false, error="Archiver returned error" } } 347 | end 348 | else 349 | result = { result = { success=false, error="Invalid path request" } } 350 | end 351 | 352 | 353 | else 354 | 355 | result = { result = { success=false, error="Operation not impolemented yet" } } 356 | 357 | end 358 | 359 | print(json.encode(result)) 360 | -------------------------------------------------------------------------------- /bridges/openwrt-lua/www/cgi-bin/fs-download: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | nixio = require "nixio" 4 | fs = require "nixio.fs" 5 | util = require "luci.util" 6 | ltn12 = require "ltn12" 7 | luafm = require "luafm" 8 | 9 | args = {} 10 | 11 | for dummy,param in pairs(util.split(nixio.getenv('QUERY_STRING'),'&')) do 12 | val = util.split(param,'=') 13 | args[val[1]] = util.urldecode(val[2],true) 14 | end 15 | 16 | path = luafm.make_path(args["path"]) 17 | if path then 18 | file = io.open(path,"rb") 19 | else 20 | file = nil 21 | end 22 | 23 | if file then 24 | print("Content-Type: application/octet-stream") 25 | print('Content-Disposition: attachment; filename="'..fs.basename(args["path"])..'"') 26 | print("\r") 27 | ltn12.pump.all( 28 | ltn12.source.file(file), 29 | ltn12.sink.file(io.stdout) 30 | ) 31 | else 32 | print("Status: 404 Not Found") 33 | print("Content-Type: text/plain") 34 | print("\r") 35 | print("File not found") 36 | end 37 | -------------------------------------------------------------------------------- /bridges/openwrt-lua/www/cgi-bin/fs-upload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | 3 | local cgi = require "cgi" 4 | local fs = require "nixio.fs" 5 | local luafm = require "luafm" 6 | 7 | local req = {} 8 | 9 | print("\r") 10 | 11 | cgi.new(req) 12 | 13 | if req.boundary then 14 | ret, msg = cgi.parse_request_body(req) 15 | end 16 | 17 | destfile=io.open(req.post["destination"][1]["tempname"],"r") 18 | destination=destfile:read("*all") 19 | 20 | for k, v in pairs(req.post) do 21 | for j, v in pairs(req.post[k]) do 22 | if v["filename"] then 23 | path = luafm.make_new_path(destination..'/'..v.filename) 24 | if path then 25 | fs.copy(req.post[k][j].tempname, path) 26 | end 27 | end 28 | end 29 | end 30 | 31 | cgi.cleanup(req) 32 | 33 | print('{ "result": { "success": true, "error": null } }') 34 | -------------------------------------------------------------------------------- /bridges/php-local/LocalBridge/Response.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Response 12 | { 13 | /** 14 | * @var integer 15 | */ 16 | private $statusCode = 200; 17 | 18 | /** 19 | * @var string 20 | */ 21 | private $status = 'OK'; 22 | 23 | /** 24 | * @var array 25 | */ 26 | private $additionalHeaders = []; 27 | 28 | /** 29 | * @var mixed 30 | */ 31 | private $data = null; 32 | 33 | /** 34 | * Body which will be used instead of data if set 35 | * @var string 36 | */ 37 | private $body = null; 38 | 39 | /** 40 | * HTTP Status according to http://tools.ietf.org/html/rfc7231 41 | * @param integer $statusCode 42 | * @param string $status without status code 43 | * @return object this 44 | */ 45 | public function setStatus($statusCode, $status) 46 | { 47 | $this->statusCode = $statusCode; 48 | $this->status = $status; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Add additional HTTP header 55 | * @param string $header 56 | * @return object this 57 | */ 58 | public function addHeaders($header) 59 | { 60 | $this->additionalHeaders[] = $header; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @param mixed $body used instead of data if set 67 | */ 68 | public function setBody($body) 69 | { 70 | $this->body = $body; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @param mixed $data any data which should be later encoded to body 77 | */ 78 | public function setData($data) 79 | { 80 | $this->data = $data; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @return mixed data which should be later encoded to body 87 | */ 88 | public function getData() 89 | { 90 | return $this->data; 91 | } 92 | 93 | /** 94 | * @return mixed used instead of data if set 95 | */ 96 | public function getBody() 97 | { 98 | if ($this->body !== null) { 99 | return $this->body; 100 | } 101 | 102 | if ($this->data === null) { 103 | return null; 104 | } 105 | 106 | return json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); 107 | } 108 | 109 | /** 110 | * HTTP Status according to http://tools.ietf.org/html/rfc7231 in form of array 111 | * @return array associative, e.g. ['code' => 200, 'status' => 'OK'] 112 | */ 113 | public function getStatus() 114 | { 115 | return [ 116 | 'code' => $this->statusCode, 117 | 'status' => $this->status 118 | ]; 119 | } 120 | 121 | /** 122 | * @return array list of all headers including first status line 123 | */ 124 | public function getHeaders() 125 | { 126 | return array_merge( 127 | [$_SERVER['SERVER_PROTOCOL'].' '.$this->statusCode.' '.$this->status], 128 | $this->additionalHeaders 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /bridges/php-local/LocalBridge/Rest.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Rest 12 | { 13 | /** 14 | * List of callbacks which are assigned and later called in handle method: post, get, put, delete, before and after 15 | * @var array 16 | */ 17 | private $callbacks = []; 18 | 19 | /** 20 | * @var boolean 21 | */ 22 | private $requireAuthentication = false; 23 | 24 | /** 25 | * Add callback for specific HTTP method (post, get, put, delete) 26 | * @param string $method name of the HTTP method 27 | * @param array $arguments expects only one argument, callback, with number of arguments based on request method ($queries, $body['data'], $body['files']) 28 | * @return object this 29 | */ 30 | public function __call($method, $arguments) 31 | { 32 | if (!in_array($method, ['post', 'get', 'put', 'delete'])) { 33 | throw new Exception('REST method "'.$method.'" not supported.'); 34 | } 35 | 36 | $this->callbacks[$method] = $arguments[0]; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Should authentication be required 43 | * @param boolean $option defaults to true 44 | */ 45 | public function setRequireAuthentication($option = true) 46 | { 47 | $this->requireAuthentication = $option; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Add callback called before every request 54 | * @param callable $callback arguments: $queries, $body['data'], $body['files'] 55 | * @return object this 56 | */ 57 | public function before($callback) 58 | { 59 | $this->callbacks['before'] = $callback; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Add callback called after every request 66 | * @param callable $callback arguments: $queries, $body['data'], $body['files'] 67 | * @return object this 68 | */ 69 | public function after($callback) 70 | { 71 | $this->callbacks['after'] = $callback; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Should be called manually as last method 78 | */ 79 | public function handle() 80 | { 81 | if ($this->requireAuthentication) { 82 | $authenticateResponse = $this->verifyAuthentication(); 83 | if ($authenticateResponse instanceof Response) { 84 | $this->respond($response); 85 | return; 86 | } 87 | } 88 | 89 | $request_method = $_SERVER['REQUEST_METHOD']; 90 | 91 | $body = [ 92 | 'data' => null, 93 | 'files' => [] 94 | ]; 95 | $queries = []; 96 | $parameters = []; 97 | 98 | //Get body data and files only from requests with body 99 | if ($request_method == 'POST' || $request_method == 'PUT') { 100 | $body = $this->parseBody(); 101 | } 102 | 103 | if (isset($_GET)) { 104 | $queries = $_GET; 105 | } 106 | 107 | if (isset($this->callbacks['before'])) { 108 | $this->callbacks['before']($queries, $body['data'], $body['files']); 109 | } 110 | 111 | switch ($request_method) { 112 | case 'POST': 113 | $response = $this->callbacks['post']($queries, $body['data'], $body['files']); 114 | break; 115 | 116 | case 'GET': 117 | $response = $this->callbacks['get']($queries); 118 | break; 119 | 120 | case 'PUT': 121 | $response = $this->callbacks['put']($queries, $body['data'], $body['files']); 122 | break; 123 | 124 | case 'DELETE': 125 | $response = $this->callbacks['delete']($queries); 126 | break; 127 | 128 | default: 129 | //Not supported 130 | $response = new Response(); 131 | $response->setStatus(501, 'Not Implemented'); 132 | break; 133 | } 134 | 135 | if (isset($this->callbacks['after'])) { 136 | $this->callbacks['after']($queries, $body['data'], $body['files']); 137 | } 138 | 139 | $this->respond($response); 140 | } 141 | 142 | /** 143 | * Uses _POST and _FILES superglobals if available, 144 | * otherwise tries to parse JSON body if Content Type header is set to application/json, 145 | * otherwise manually parses body as form data 146 | * @return array associative array with data and files ['data' => ?, 'files' => ?] 147 | */ 148 | private function parseBody() 149 | { 150 | $data = null; 151 | $files = null; 152 | 153 | if (isset($_POST) && !empty($_POST)) { 154 | $data = $_POST; 155 | } 156 | 157 | if (isset($_FILES) && !empty($_FILES)) { 158 | $files = $_FILES; 159 | } 160 | 161 | //In case of request with json body 162 | if ($data === null) { 163 | if (isset($_SERVER["CONTENT_TYPE"]) && strpos($_SERVER["CONTENT_TYPE"], 'application/json') !== false) { 164 | $input = file_get_contents('php://input'); 165 | 166 | $data = json_decode($input, true); 167 | } 168 | } 169 | 170 | return [ 171 | 'data' => $data, 172 | 'files' => $files 173 | ]; 174 | } 175 | 176 | /** 177 | * Check wheter client is authorized and returns Response object with autorization request if not 178 | * @return mixed Response object if client is not authorized, otherwise nothing 179 | */ 180 | private function verifyAuthentication() 181 | { 182 | $authenticated = false; 183 | $headers = getallheaders(); 184 | 185 | if (isset($headers['Authorization'])) { 186 | $token = str_replace('Token ', '', $headers['Authorization']); 187 | 188 | $authenticated = token::verify($token); 189 | } 190 | 191 | if ($authenticated === false) { 192 | $response = new Response(); 193 | $response->setStatus(401, 'Unauthorized') 194 | ->addHeaders('WWW-Authenticate: Token'); 195 | 196 | return $response; 197 | } 198 | } 199 | 200 | /** 201 | * Use Response object to modify headers and output body 202 | * @param Response $response 203 | */ 204 | private function respond(Response $response) 205 | { 206 | foreach ($response->getHeaders() as $header) { 207 | header($header); 208 | } 209 | 210 | echo $response->getBody(); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /bridges/php-local/LocalBridge/Translate.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Translate 11 | { 12 | private $strings = []; 13 | 14 | public function __construct($lang) 15 | { 16 | if (!file_exists('lang/'.$lang.'.json')) { 17 | throw new \Exception('No language file for chosen language'); 18 | return; 19 | } 20 | 21 | $json = file_get_contents('lang/'.$lang.'.json'); 22 | 23 | $this->strings = json_decode($json, true); 24 | } 25 | 26 | public function __get($name) 27 | { 28 | if (isset($this->strings[$name])) { 29 | return $this->strings[$name]; 30 | } else { 31 | return $name; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bridges/php-local/index.php: -------------------------------------------------------------------------------- 1 | 8 | * @version 0.2.0 9 | */ 10 | include 'LocalBridge/Response.php'; 11 | include 'LocalBridge/Rest.php'; 12 | include 'LocalBridge/Translate.php'; 13 | include 'LocalBridge/FileManagerApi.php'; 14 | 15 | /** 16 | * Takes two arguments 17 | * - base path without last slash (default: '$currentDirectory/../files') 18 | * - language (default: 'en'); mute_errors (default: true, will call ini_set('display_errors', 0)) 19 | */ 20 | $fileManagerApi = new FileManagerApi("/"); 21 | 22 | $rest = new Rest(); 23 | $rest->post([$fileManagerApi, 'postHandler']) 24 | ->get([$fileManagerApi, 'getHandler']) 25 | ->handle(); 26 | -------------------------------------------------------------------------------- /bridges/php-local/lang/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "function_not_implemented": "Funktion nicht implementiert", 3 | "file_not_found": "Datei nicht gefunden", 4 | "listing_failed": "Auflistung fehlgeschlagen", 5 | "moving_failed": "Verschiebung fehlgeschlagen", 6 | "renaming_failed": "Umbenennung fehlgeschlagen", 7 | "copying_failed": "Kopieren fehlgeschlagen", 8 | "removing_failed": "Löschen fehlgeschlagen", 9 | "removing_failed_directory_not_empty": "Entfernen fehlgeschlagen, der Ordner, welchen Sie versuchen zu löschen, ist nicht leer.", 10 | "saving_failed": "Speichern fehlgeschlagen", 11 | "folder_already_exists": "Ordner existiert bereits", 12 | "folder_creation_failed": "Ordner Anlage fehlgeschlagen", 13 | "permissions_change_failed": "Berechtigungsänderung fehlgeschlagen", 14 | "compression_failed": "Kompression fehlgeschlagen", 15 | "archive_opening_failed": "Öffnen des Archivs fehlgeschlagen, es ist entweder korrupt oder nicht unterstützt.", 16 | "extraction_failed": "Auspacken fehlgeschlagen", 17 | "upload_failed": "Hochladen fehlgeschlagen" 18 | } 19 | -------------------------------------------------------------------------------- /bridges/php-local/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "function_not_implemented": "Function not implemented", 3 | "file_not_found": "File not found", 4 | "listing_failed": "Listing failed", 5 | "moving_failed": "Moving failed", 6 | "renaming_failed": "Renaming failed", 7 | "copying_failed": "Copying failed", 8 | "removing_failed": "Removing failed", 9 | "removing_failed_directory_not_empty": "Removing failed, the directory you are trying to remove is not empty", 10 | "saving_failed": "Saving failed", 11 | "folder_already_exists": "Folder already exists", 12 | "folder_creation_failed": "Folder creation failed", 13 | "permissions_change_failed": "Permissions change failed", 14 | "compression_failed": "Compression failed", 15 | "archive_opening_failed": "Could not open the archive, it is either corrupted or unsupported", 16 | "extraction_failed": "Extraction failed", 17 | "upload_failed": "Upload failed" 18 | } 19 | -------------------------------------------------------------------------------- /bridges/php-local/lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "function_not_implemented": "Fonction non implémentée", 3 | "file_not_found": "Fichier non trouvé", 4 | "listing_failed": "Le listing a échoué", 5 | "moving_failed": "Le déplacement a échoué", 6 | "renaming_failed": "Le renommage a échoué", 7 | "copying_failed": "La copie a échouée", 8 | "removing_failed": "La suppression a échouée", 9 | "removing_failed_directory_not_empty": "La suppression a échouée, le répertoire que vous voulez supprimer n'est pas vide", 10 | "saving_failed": "L'enregistrement a échoué", 11 | "folder_already_exists": "Le répertoire existe déjà", 12 | "folder_creation_failed": "La création du répertoire a échoué", 13 | "permissions_change_failed": "Le changement de permission a échoué", 14 | "compression_failed": "La compression a échouée", 15 | "archive_opening_failed": "L'ouverture de l'archive est impossible, le fichier est corrompu ou non supporté", 16 | "extraction_failed": "L'extraction a échouée", 17 | "upload_failed": "L'upload a échoué" 18 | } 19 | -------------------------------------------------------------------------------- /bridges/php-local/lang/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "function_not_implemented": "Funzione non implementata", 3 | "file_not_found": "File non trovato", 4 | "listing_failed": "Elenco fallito", 5 | "moving_failed": "Spostamento fallito", 6 | "renaming_failed": "Rinomina fallita", 7 | "copying_failed": "Copia fallita", 8 | "removing_failed": "Rimozione fallita", 9 | "removing_failed_directory_not_empty": "Rimozione fallita, la cartella che stai tentando di rimuovere non è vuota", 10 | "saving_failed": "Salvataggio fallito", 11 | "folder_already_exists": "La cartella esiste già", 12 | "folder_creation_failed": "Creazione della cartella fallita", 13 | "permissions_change_failed": "Modifica dei permessi non riuscita", 14 | "compression_failed": "Compressione fallita", 15 | "archive_opening_failed": "Impossibile aprire l'archivio, è danneggiato o non supportato", 16 | "extraction_failed": "Estrazione fallita", 17 | "upload_failed": "Caricamento fallito" 18 | } 19 | -------------------------------------------------------------------------------- /bridges/php-local/lang/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "function_not_implemented": "구현되지 않은 기능입니다", 3 | "file_not_found": "파일을 찾을 수 없습니다", 4 | "listing_failed": "목록 작성 실패", 5 | "moving_failed": "이동 실패", 6 | "renaming_failed": "이름 변경 실패", 7 | "copying_failed": "복사 실패", 8 | "removing_failed": "삭제 실패", 9 | "removing_failed_directory_not_empty": "폴더가 비어 있지 않아, 삭제할 수 없습니다", 10 | "saving_failed": "저장 실패", 11 | "folder_already_exists": "폴더가 이미 존재합니다", 12 | "folder_creation_failed": "폴더 생성 실패", 13 | "permissions_change_failed": "권한 변경 실패", 14 | "compression_failed": "압축 실패", 15 | "archive_opening_failed": "파일이 손상되었거나 지원되지 않아, 압축 파일을 열 수 없습니다", 16 | "extraction_failed": "추출 실패", 17 | "upload_failed": "업로드 실패" 18 | } 19 | -------------------------------------------------------------------------------- /bridges/php-local/lang/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "function_not_implemented": "Funkcia nie je implementovaná", 3 | "file_not_found": "Súbor nenájdený", 4 | "listing_failed": "Výpis súborov zlyhal", 5 | "moving_failed": "Presunutie zlyhalo", 6 | "renaming_failed": "Premenovanie zlyhalo", 7 | "copying_failed": "Kopírovanie zlyhalo", 8 | "removing_failed": "Odstraňovanie zlyhalo", 9 | "removing_failed_directory_not_empty": "Odstraňovanie zlyhalo, priečinok, ktorý sa snažíte odstrániť nie je prázdny", 10 | "saving_failed": "Ukladanie zlyhalo", 11 | "folder_already_exists": "Priečinok s daným názvom už existuje", 12 | "folder_creation_failed": "Priečinok sa nepodarilo vytvoriť", 13 | "permissions_change_failed": "Nepodarilo sa zmeniť oprávnenia", 14 | "compression_failed": "Komprimovanie zlyhalo", 15 | "archive_opening_failed": "Nepodarilo sa otvoriť archív je buď poškodený alebo nepodporovaného formátu", 16 | "extraction_failed": "Extrahovanie zlyhalo", 17 | "upload_failed": "Nahrávanie zlyhalo" 18 | } 19 | -------------------------------------------------------------------------------- /bridges/php/handler.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | require_once __DIR__ . '/includes/Ftp.php'; 8 | require_once __DIR__ . '/includes/ExceptionCatcher.php'; 9 | 10 | use PHPClassic\ExceptionCatcher; 11 | use PHPClassic\Ftp; 12 | 13 | class ExceptionCatcherJSON extends ExceptionCatcher 14 | { 15 | public static function draw(\Exception $oExp) 16 | { 17 | @header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500); 18 | $oResponse = new Response; 19 | $oResponse->setData(array( 20 | 'success' => false, 21 | 'error' => $oExp->getMessage(), 22 | 'errorDetail' => array( 23 | 'type' => get_class($oExp), 24 | 'code' => $oExp->getCode() 25 | ) 26 | )); 27 | return $oResponse->flushJson(); 28 | } 29 | 30 | public static function register() 31 | { 32 | set_exception_handler(array(__CLASS__, 'handle')); 33 | } 34 | } 35 | 36 | abstract class Request 37 | { 38 | public static function getQuery($param = null, $default = null) 39 | { 40 | if ($param) { 41 | return isset($_GET[$param]) ? 42 | $_GET[$param] : $default; 43 | } 44 | return $_GET; 45 | } 46 | 47 | public static function getPost($param = null, $default = null) 48 | { 49 | if ($param) { 50 | return isset($_POST[$param]) ? 51 | $_POST[$param] : $default; 52 | } 53 | return $_POST; 54 | } 55 | 56 | public static function getFile($param = null, $default = null) 57 | { 58 | if ($param) { 59 | return isset($_FILES[$param]) ? 60 | $_FILES[$param] : $default; 61 | } 62 | return $_FILES; 63 | } 64 | 65 | public static function getPostContent() 66 | { 67 | $rawData = file_get_contents('php://input'); 68 | return json_decode($rawData); 69 | } 70 | 71 | public static function getApiParam($param) 72 | { 73 | $oData = static::getPostContent(); 74 | return isset($oData->$param) ? $oData->$param : null; 75 | } 76 | 77 | public static function getApiOrQueryParam($param) 78 | { 79 | return Request::getApiParam($param) ? 80 | Request::getApiParam($param) : Request::getQuery($param); 81 | } 82 | } 83 | 84 | class Response 85 | { 86 | protected $data; 87 | 88 | public function __construct($data = null) 89 | { 90 | $this->setData($data); 91 | } 92 | 93 | public function flushJson() 94 | { 95 | $this->data = json_encode(array('result' => $this->data)); 96 | return $this->flush(); 97 | } 98 | 99 | public function flush() 100 | { 101 | echo $this->data; 102 | exit; 103 | } 104 | 105 | public function setData($data) 106 | { 107 | $this->data = $data; 108 | return $this; 109 | } 110 | 111 | public function setHeaders($params) 112 | { 113 | if (! headers_sent()) { 114 | if (is_scalar($params)) { 115 | header($params); 116 | } else { 117 | foreach($params as $key => $value) { 118 | header(sprintf('%s: %s', $key, $value)); 119 | } 120 | } 121 | } 122 | return $this; 123 | } 124 | } 125 | 126 | class FileManager extends Ftp 127 | { 128 | public function downloadTemp($path) 129 | { 130 | $localPath = tempnam(sys_get_temp_dir(), 'fmanager_'); 131 | if ($this->download($path, $localPath)) { 132 | return $localPath; 133 | } 134 | } 135 | 136 | public function getContent($path) 137 | { 138 | $localPath = $this->downloadTemp($path); 139 | if ($localPath) { 140 | return @file_get_contents($localPath); 141 | } 142 | } 143 | 144 | } 145 | 146 | ExceptionCatcherJSON::register(); 147 | $oResponse = new Response(); 148 | $oFtp = new FileManager(array( 149 | 'hostname' => '', 150 | 'username' => '', 151 | 'password' => '' 152 | )); 153 | 154 | if (! $oFtp->connect()) { 155 | throw new Exception("Cannot connect to the FTP server"); 156 | } 157 | 158 | if (Request::getFile() && $dest = Request::getPost('destination')) { 159 | $errors = array(); 160 | foreach (Request::getFile() as $file) { 161 | $filePath = $file['tmp_name']; 162 | $destPath = $dest .'/'. $file['name']; 163 | $result = $oFtp->upload($filePath, $destPath); 164 | if (! $result) { 165 | $errors[] = $file['name']; 166 | } 167 | } 168 | 169 | if ($errors) { 170 | throw new Exception("Unknown error uploading: \n\n" . implode(", \n", $errors)); 171 | } 172 | 173 | $oResponse->setData($result); 174 | $oResponse->flushJson(); 175 | } 176 | 177 | if (Request::getApiParam('action') === 'list') { 178 | $list = $oFtp->listFilesRaw(Request::getApiParam('path')); 179 | $list = is_array($list) ? $list : array(); 180 | $list = array_map(function($item) { 181 | $date = new \DateTime('now'); 182 | $item['date'] = $date->format('Y-m-d H:i:s'); 183 | return $item; 184 | }, $list); 185 | $oResponse->setData($list); 186 | $oResponse->flushJson(); 187 | } 188 | 189 | if (Request::getApiParam('action') === 'getContent') { 190 | $oResponse->setData($oFtp->getContent(Request::getApiParam('item'))); 191 | $oResponse->flushJson(); 192 | } 193 | 194 | if (Request::getApiParam('action') === 'rename') { 195 | $item = Request::getApiParam('item'); 196 | $newItemPath = Request::getApiParam('newItemPath'); 197 | $result = $oFtp->move($item, $newItemPath); 198 | if (! $result) { 199 | throw new Exception("Unknown error renaming this item"); 200 | } 201 | $oResponse->setData($result); 202 | $oResponse->flushJson(); 203 | } 204 | 205 | if (Request::getApiParam('action') === 'move') { 206 | $items = Request::getApiParam('items'); 207 | $newPath = Request::getApiParam('newPath'); 208 | $errors = array(); 209 | 210 | foreach($items as $item) { 211 | $fileName = explode('/', $item); 212 | $fileName = end($fileName); 213 | $finalPath = $newPath . '/' . $fileName; 214 | $result = $item ? $oFtp->move($item, $finalPath) : false; 215 | if (! $result) { 216 | $errors[] = $item . ' to ' . $finalPath; 217 | } 218 | } 219 | if ($errors) { 220 | throw new Exception("Unknown error moving: \n\n" . implode(", \n", $errors)); 221 | } 222 | $oResponse->setData($result); 223 | $oResponse->flushJson(); 224 | } 225 | 226 | if (Request::getApiParam('action') === 'remove') { 227 | $items = Request::getApiParam('items'); 228 | $errors = array(); 229 | foreach($items as $item) { 230 | $result = $item ? $oFtp->delete($item) : false; 231 | if (! $result) { 232 | $errors[] = $item; 233 | } 234 | } 235 | if ($errors) { 236 | throw new Exception("Unknown error deleting: \n\n" . implode(", \n", $errors)); 237 | } 238 | $oResponse->setData($result); 239 | $oResponse->flushJson(); 240 | } 241 | 242 | if (Request::getApiParam('action') === 'createFolder') { 243 | $newPath = Request::getApiParam('newPath'); 244 | $result = $oFtp->mkdir($newPath); 245 | if (! $result) { 246 | throw new Exception("Unknown error creating this folder"); 247 | } 248 | $oResponse->setData($result); 249 | $oResponse->flushJson(); 250 | } 251 | 252 | if (Request::getApiParam('action') === 'compress') { 253 | $items = Request::getApiParam('items'); 254 | $destination = Request::getApiParam('destination'); 255 | $compressedFilename = Request::getApiParam('compressedFilename'); 256 | 257 | //example 258 | $temp = tempnam(sys_get_temp_dir(), microtime()); 259 | $finalDest = $destination . '/' . $compressedFilename; 260 | 261 | $finalDest = preg_match('/\.(tar|zip)$/', $finalDest) ? $finalDest : $finalDest . '.zip'; 262 | $result = $oFtp->upload($temp, $finalDest); 263 | if (! $result) { 264 | throw new Exception("Unknown error compressing these file(s)"); 265 | } 266 | 267 | $oResponse->setData(true); 268 | $oResponse->flushJson(); 269 | } 270 | 271 | if (Request::getQuery('action') === 'download') { 272 | $download = Request::getQuery('preview') === 'true' ? '' : 'attachment;'; 273 | $filePath = Request::getQuery('path'); 274 | $fileName = explode('/', $filePath); 275 | $fileName = end($fileName); 276 | $tmpFilePath = $oFtp->downloadTemp($filePath); 277 | 278 | @register_shutdown_function(function() use ($tmpFilePath) { 279 | if (file_exists($tmpFilePath)) { 280 | @unlink($tmpFilePath); 281 | } 282 | }); 283 | 284 | if ($fileContent = @file_get_contents($tmpFilePath)) { 285 | $oResponse->setData($fileContent); 286 | $oResponse->setHeaders(array( 287 | 'Content-Type' => @mime_content_type($tmpFilePath), 288 | 'Content-disposition' => sprintf('%s filename="%s"', $download, $fileName) 289 | )); 290 | } 291 | $oResponse->flush(); 292 | } 293 | 294 | if (Request::getQuery('action') === 'downloadMultiple') { 295 | $items = Request::getApiOrQueryParam('items'); 296 | $toFilename = Request::getApiOrQueryParam('toFilename'); 297 | $errors = array(); 298 | 299 | $fileContent = is_array($items) ? implode($items, ", \n") : ''; 300 | if ($errors) { 301 | throw new Exception("Unknown compressing to download: \n\n" . implode(", \n", $errors)); 302 | } 303 | 304 | if ($fileContent) { 305 | $oResponse->setData($fileContent); 306 | $oResponse->setHeaders(array( 307 | 'Content-Type' => @mime_content_type($fileContent), 308 | 'Content-disposition' => sprintf('attachment; filename="%s"', $toFilename) 309 | )); 310 | } 311 | $oResponse->flush(); 312 | } 313 | 314 | throw new \Exception('This action is not available in the demo'); 315 | -------------------------------------------------------------------------------- /bridges/php/includes/ExceptionCatcher.php: -------------------------------------------------------------------------------- 1 | 11 | * @todo Method to set view template and remove html code from draw() method 12 | * @todo Avoid the use of highlight_string() to make php syntax highlight 13 | */ 14 | 15 | abstract class ExceptionCatcher 16 | { 17 | 18 | /** 19 | * @param Exception $oExp 20 | */ 21 | public static function handle(Exception $oExp) 22 | { 23 | echo static::draw($oExp); 24 | } 25 | 26 | /** 27 | * @param Exception $oExp 28 | * @return string 29 | */ 30 | public static function draw(Exception $oExp) 31 | { 32 | $details = static::buildDetails($oExp); 33 | $content = sprintf('

Exception: %s

', $oExp->getMessage()); 34 | $content .= sprintf('
At: %s:%s
', $oExp->getFile(), $oExp->getLine()); 35 | $content .= sprintf('

Trace

'); 36 | foreach ($details as $items) { 37 | if (isset($items['trace'])) { 38 | $content .= sprintf("
%s:%d
\n", $items['file'], $items['line']); 39 | $content .= static::getHighlightedCode($items['trace'], true); 40 | } 41 | } 42 | return $content; 43 | } 44 | 45 | /** 46 | * @param array $trace 47 | * @return string 48 | */ 49 | protected function getHighlightedCode(array $trace, $showLineNumber = false) 50 | { 51 | $content = ''; 52 | foreach ($trace as $line) { 53 | if (! isset($line['lineContent'])) { 54 | return; 55 | } 56 | $content .= $showLineNumber ? 57 | sprintf("%d %s\n", $line['lineNumber'], $line['lineContent']) : 58 | sprintf("%s\n", $line['lineContent']); 59 | } 60 | $str = highlight_string('getFile(), $oExp->getLine()); 72 | foreach ($oExp->getTrace() as $files) { 73 | $arr[] = static::getDetails($files['file'], $files['line']); 74 | } 75 | return $arr; 76 | } 77 | 78 | /** 79 | * @param string $file 80 | * @param int $line 81 | * @return array 82 | */ 83 | protected function getDetails($file, $line) 84 | { 85 | $lines = file_get_contents($file); //validate 86 | $lines = explode("\n", $lines); 87 | $trace = array(); 88 | foreach (range($line - 4, $line + 2) as $fetchLine) { 89 | if (isset($lines[$fetchLine])) { 90 | $n = strlen($fetchLine + 10); 91 | $lineKey = sprintf('%0'.$n.'d', $fetchLine + 1); 92 | $trace[$lineKey] = array( 93 | 'lineContent' => $lines[$fetchLine], 94 | 'lineNumber' => $lineKey 95 | ); 96 | } 97 | } 98 | return array( 99 | 'file' => $file, 100 | 'line' => $line, 101 | 'trace' => $trace, 102 | ); 103 | } 104 | 105 | /** 106 | * @return void 107 | */ 108 | public static function register() 109 | { 110 | set_exception_handler(array(__CLASS__, 'handle')); 111 | } 112 | 113 | /** 114 | * @return void 115 | */ 116 | public static function unregister() 117 | { 118 | restore_exception_handler(); 119 | } 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | angular-filemanager 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const browserSync = require('browser-sync').create(); 3 | const sass = require('gulp-sass'); 4 | const concat = require('gulp-concat'); 5 | const sourcemaps = require('gulp-sourcemaps'); 6 | const path = require("path"); 7 | const del = require('del'); 8 | const header = require('gulp-header'); 9 | const cleanCSS = require('gulp-clean-css'); 10 | const htmlmin = require('gulp-htmlmin'); 11 | const templateCache = require('gulp-angular-templatecache'); 12 | const package = require('./package.json') 13 | const iife = require("gulp-iife"); 14 | const uglifyes = require('uglify-es'); 15 | const composer = require('gulp-uglify/composer'); 16 | const minifyes = composer(uglifyes, console); 17 | const fs = require('fs'); 18 | const replace = require('gulp-replace'); 19 | const bulkSass = require('gulp-sass-bulk-import'); 20 | const babel = require('gulp-babel'); 21 | const eslint = require('gulp-eslint'); 22 | 23 | const distributionFolder = 'dist/'; 24 | 25 | gulp.task('clean', function () { 26 | return del('./' + distributionFolder + '**'); 27 | }); 28 | 29 | /* 30 | gulp.task('copy-files', function () { 31 | return gulp.src([ 32 | 'index.html', 33 | 34 | ], {base: ".", nodir: true}) 35 | .pipe(gulp.dest(distributionFolder)); 36 | }); 37 | */ 38 | 39 | 40 | // Static server 41 | gulp.task('browser-sync', function (done) { 42 | 43 | historyApiFallback = require('connect-history-api-fallback'); 44 | browserSync.init({ 45 | ui: false, 46 | middleware: [historyApiFallback()], 47 | //browser: "google chrome" 48 | }); 49 | 50 | done(); 51 | }); 52 | 53 | gulp.task('javascript', function (done) { 54 | var sources = ['src/js/app.js', 'src/js/**/*.js']; 55 | 56 | return gulp.src(sources, {base: '.'}) 57 | .pipe(sourcemaps.init()) 58 | .pipe(concat('angularjs-filemanager.js')) 59 | .on('error', logError) 60 | .pipe(babel({presets: ['@babel/env']})) 61 | .pipe(sourcemaps.write({includeContent: false, sourceRoot: '/'})) 62 | .pipe(gulp.dest(distributionFolder)); 63 | }); 64 | 65 | gulp.task('scss', function () { 66 | return gulp.src('src/css/style.scss') 67 | .pipe(bulkSass()) 68 | .pipe(sourcemaps.init()) 69 | .pipe(sass({importer: importer})) 70 | .on('error', logError) 71 | // since gulp-sass doesn't let you rename the file 72 | .pipe(sourcemaps.write({includeContent: false, sourceRoot: '/src/css'})) 73 | .pipe(concat('angularjs-filemanager.css')) 74 | .pipe(gulp.dest(distributionFolder)) 75 | .pipe(browserSync.stream()); 76 | }); 77 | 78 | 79 | let svgSprite = require("gulp-svg-sprite"); 80 | let svgIconConfig = { 81 | shape: { 82 | id: { // SVG shape ID related options 83 | generator: "svg-", // SVG shape ID generator callback 84 | }, 85 | dimension: { // Set maximum dimensions 86 | maxWidth: 24, 87 | maxHeight: 24 88 | }, 89 | spacing: { // Add padding 90 | padding: 0, 91 | box: 'icon' 92 | }, 93 | transform: ['svgo'], 94 | meta: null, 95 | 96 | }, 97 | mode: { 98 | symbol: { // Activate the «view» mode 99 | common: 'svg', 100 | bust: false, 101 | dimensions: false, 102 | sprite: 'angularjs-filemanager.svg', 103 | //example: true, 104 | render: { 105 | css: false 106 | }, 107 | inline: true, 108 | dest: "" 109 | } 110 | }, 111 | svg: { // General options for created SVG files 112 | xmlDeclaration: false, // Add XML declaration to SVG sprite 113 | doctypeDeclaration: true, // Add DOCTYPE declaration to SVG sprite 114 | namespaceIDs: true, // Add namespace token to all IDs in SVG shapes 115 | namespaceClassnames: true, // Add namespace token to all CSS class names in SVG shapes 116 | dimensionAttributes: false // Width and height attributes on the sprite 117 | } 118 | 119 | }; 120 | 121 | gulp.task('sprites_icon', function () { 122 | return gulp.src("src/svg/**/*.svg").pipe(svgSprite(svgIconConfig)).pipe(gulp.dest(distributionFolder)); 123 | }); 124 | 125 | 126 | function browserSyncReload(done) { 127 | browserSync.reload(); 128 | done(); 129 | } 130 | 131 | 132 | gulp.task('watch', gulp.series(gulp.parallel('scss', 'javascript', 'sprites_icon'), function watchTask(done) { 133 | gulp.watch(['src/**/*css'], gulp.parallel('scss')); 134 | gulp.watch(['src/svg/*.svg'], gulp.series('sprites_icon', browserSyncReload)); 135 | gulp.watch(['src/js/**/*.js'], gulp.series('javascript', browserSyncReload)); 136 | gulp.watch(["src/**/*.html", "index.html"], gulp.parallel(browserSyncReload)); 137 | 138 | done(); 139 | })); 140 | 141 | 142 | gulp.task('default', gulp.series(gulp.parallel('clean'), 'watch', 'browser-sync')); 143 | 144 | function importer(url, prev, done) { 145 | if (url[0] === '~') { 146 | url = path.resolve('./node_modules/' + url.substr(1)); 147 | } else if (url[0] === '/') { 148 | //url = path.resolve( url.substr(1)); 149 | } 150 | return {file: url}; 151 | } 152 | 153 | function logError(error) { 154 | 155 | // If you want details of the error in the console 156 | console.log(error.toString()); 157 | this.emit('end') 158 | } 159 | 160 | gulp.task('ng-templates', function () { 161 | return gulp.src(['src/**/*.html']) 162 | .pipe(htmlmin({ 163 | collapseWhitespace: true, 164 | removeComments: true, 165 | sortAttributes: true, 166 | sortClassName: true, 167 | ignoreCustomFragments: [/<%[\s\S]*?%>/, /<\?[\s\S]*?(\?>|$)/], 168 | trimCustomFragments: true 169 | })) 170 | .pipe(templateCache({root: 'src/'})) 171 | .pipe(replace("angular.module('templates')", 'app')) 172 | .pipe(gulp.dest(distributionFolder)); 173 | }); 174 | 175 | 176 | gulp.task('concat-ng-templates', function () { 177 | return gulp.src(['angularjs-filemanager.js', 'templates.js'], {base: distributionFolder, cwd: distributionFolder}) 178 | .pipe(concat('angularjs-filemanager.js')) 179 | .pipe(gulp.dest(distributionFolder)); 180 | }); 181 | 182 | 183 | gulp.task('scripts-minify', gulp.series('javascript', 'sprites_icon', 'ng-templates', 'concat-ng-templates', function minify(done) { 184 | // Minify and copy all JavaScript (except vendor scripts) 185 | // with sourcemaps all the way down 186 | del('./' + distributionFolder + 'templates.js'); 187 | return gulp.src(['angularjs-filemanager.js'], {base: distributionFolder, cwd: distributionFolder}) 188 | .pipe(iife({ 189 | useStrict: false 190 | })) 191 | .pipe(minifyes().on('error', function (e) { 192 | console.log(e); 193 | //callback(e); 194 | })) 195 | .pipe(header(fs.readFileSync('header.txt', 'utf8'), {pkg: package})) 196 | .pipe(gulp.dest(distributionFolder)); 197 | })); 198 | 199 | 200 | gulp.task('styles-minify', gulp.series(gulp.parallel('scss'), function () { 201 | return gulp.src(['angularjs-filemanager.css'], {base: distributionFolder, cwd: distributionFolder}) 202 | //.pipe(sourcemaps.init()) 203 | .pipe(cleanCSS({ 204 | level: { 205 | 1: { 206 | specialComments: 'none' 207 | }, 208 | 2: { 209 | //normalizeUrls: false 210 | } 211 | }, 212 | //inline: ['local'], 213 | rebase: false 214 | })) 215 | //.pipe(sourcemaps.write()) 216 | .pipe(header(fs.readFileSync('header.txt', 'utf8'), {pkg: package})) 217 | .pipe(gulp.dest(distributionFolder)); 218 | })); 219 | 220 | 221 | gulp.task('production-replace', function (done) { 222 | 223 | return gulp.src(['angularjs-filemanager.css', 'angularjs-filemanager.js'], { 224 | base: distributionFolder, 225 | cwd: distributionFolder 226 | }) 227 | //adding version to stop caching 228 | .pipe(replace('debugInfoEnabled(!0)', 'debugInfoEnabled(false)')) 229 | .pipe(replace('[[version]]', package.version)) 230 | .pipe(replace(/<\/ng-include>/g, function (match, p1) { 231 | const svg = fs.readFileSync(path.resolve('.' + p1)); 232 | del('.' + p1); 233 | return svg; 234 | })) 235 | .pipe(replace('/dist/', '/')) 236 | .pipe(replace('/assets/app/', '/')) 237 | 238 | .pipe(gulp.dest(distributionFolder)); 239 | 240 | }); 241 | 242 | gulp.task('distribute', gulp.series('clean', 243 | gulp.parallel('styles-minify', 'scripts-minify'), 244 | 'production-replace' 245 | )); 246 | 247 | 248 | gulp.task('lint', function () { 249 | let src = 'src/'; 250 | return gulp.src([src + 'js/app.js', src + 'js/*/*.js']) 251 | .pipe(eslint({ 252 | 'rules': { 253 | 'quotes': [2, 'single'], 254 | //'linebreak-style': [2, 'unix'], 255 | 'semi': [2, 'always'] 256 | }, 257 | 'env': { 258 | 'browser': true 259 | }, 260 | 'globals': [ 261 | 'angular', 262 | 'jQuery' 263 | ], 264 | "parser": "babel-eslint", 265 | "parserOptions": { 266 | "sourceType": "module", 267 | "allowImportExportEverywhere": false, 268 | "codeFrame": true 269 | } 270 | })) 271 | .pipe(eslint.format()) 272 | .pipe(eslint.failOnError()); 273 | }); 274 | -------------------------------------------------------------------------------- /header.txt: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | _____ __________ _____ __ __ 4 | / _ \ \____ /________________ _/ ____\____ _/ |_|__| ____ ____ _____ 5 | / /_\ \ / // __ \_ __ \__ \\ __\\__ \\ __\ | _/ ___\/ _ \ / \ 6 | / | \/ _/\ ___/| | \// __ \| | / __ \| | | | \ \__( <_> ) Y Y \ 7 | \____|__ /_______ \___ >__| (____ /__| (____ /__| |__| /\ \___ >____/|__|_| / 8 | \/ \/ \/ \/ \/ \/ \/ \/ 9 | 10 | * ${pkg.name} 11 | * @version v${pkg.version} 12 | * @website ${pkg.homepage} 13 | */ 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | angular-filemanager 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 59 | 60 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 |
75 |
76 |
77 |
78 | Sample Wrapper 79 |
80 |
81 | 82 |
83 |
84 |
85 |
86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@azerafati/angularjs-filemanager", 3 | "version": "2.1.3", 4 | "description": "A very smart filemanager to manage your files in the browser.", 5 | "main": "src/js/app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "develop": "/usr/bin/php -S localhost:8080 & gulp", 9 | "distribute": "gulp distribute" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/azerafati/angularjs-filemanager.git" 14 | }, 15 | "keywords": [ 16 | "filemanager" 17 | ], 18 | "author": "Alireza Zerafati based on excellent work from Jonas Sciangula Street", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/azerafati/angular-filemanager/issues" 22 | }, 23 | "files": [ 24 | "dist/*" 25 | ], 26 | "homepage": "https://angularjs-filemanager.azerafati.com/", 27 | "devDependencies": { 28 | "@babel/core": "^7.8.4", 29 | "@babel/preset-env": "^7.8.4", 30 | "babel-eslint": "^10.0.3", 31 | "browser-sync": "^2.26.3", 32 | "connect-history-api-fallback": "^1.6.0", 33 | "del": "^5.1.0", 34 | "gulp": "^4.0.0", 35 | "gulp-angular-templatecache": "^3.0.0", 36 | "gulp-babel": "^8.0.0", 37 | "gulp-clean-css": "^4.0.0", 38 | "gulp-concat": "^2.6.1", 39 | "gulp-eslint": "^6.0.0", 40 | "gulp-header": "^2.0.9", 41 | "gulp-htmlmin": "^5.0.1", 42 | "gulp-iife": "^0.4.0", 43 | "gulp-replace": "^1.0.0", 44 | "gulp-sass": "^4.0.2", 45 | "gulp-sass-bulk-import": "^1.0.1", 46 | "gulp-sourcemaps": "^2.6.5", 47 | "gulp-svg-sprite": "^1.5.0", 48 | "gulp-uglify": "^3.0.2", 49 | "gulp-useref": "^4.0.1", 50 | "uglify-es": "^3.3.9" 51 | }, 52 | "dependencies": { 53 | "angular": "^1.7.9", 54 | "angular-translate": "^2.18.2", 55 | "bootstrap": "^4.4.1", 56 | "jquery": "^3.4.0", 57 | "ng-file-upload": "^12.2.13" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/css/animations.scss: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes fadeIn { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 6 | 100% { 7 | opacity: 1; 8 | }; 9 | } 10 | 11 | @keyframes fadeIn { 12 | 0% { 13 | opacity: 0; 14 | } 15 | 16 | 100% { 17 | opacity: 1; 18 | }; 19 | } 20 | 21 | @-webkit-keyframes fadeInDown { 22 | 0% { 23 | opacity: 0; 24 | -webkit-transform: translate3d(0, -100%, 0); 25 | transform: translate3d(0, -100%, 0); 26 | } 27 | 28 | 100% { 29 | opacity: 1; 30 | -webkit-transform: none; 31 | transform: none; 32 | }; 33 | } 34 | 35 | @keyframes fadeInDown { 36 | 0% { 37 | opacity: 0; 38 | -webkit-transform: translate3d(0, -100%, 0); 39 | transform: translate3d(0, -100%, 0); 40 | } 41 | 42 | 100% { 43 | opacity: 1; 44 | -webkit-transform: none; 45 | transform: none; 46 | }; 47 | } 48 | 49 | @keyframes rotate { 50 | 100% { 51 | transform: rotate(360deg); 52 | }; 53 | } 54 | 55 | @-webkit-keyframes rotate { 56 | 100% { 57 | -webkit-transform: rotate(360deg); 58 | }; 59 | } 60 | 61 | @keyframes colors { 62 | 0% { 63 | stroke: #4285F4; 64 | } 65 | 66 | 25% { 67 | stroke: #DE3E35; 68 | } 69 | 70 | 50% { 71 | stroke: #F7C223; 72 | } 73 | 74 | 75% { 75 | stroke: #1B9A59; 76 | } 77 | 78 | 100% { 79 | stroke: #4285F4; 80 | }; 81 | } 82 | 83 | @keyframes dash { 84 | 0% { 85 | stroke-dasharray: 1,150; 86 | stroke-dashoffset: 0; 87 | stroke: red; 88 | } 89 | 90 | 50% { 91 | stroke-dasharray: 90,150; 92 | stroke-dashoffset: -35; 93 | stroke: yellow; 94 | } 95 | 96 | 100% { 97 | stroke-dasharray: 90,150; 98 | stroke-dashoffset: -124; 99 | stroke: green; 100 | }; 101 | } 102 | 103 | @-webkit-keyframes dash { 104 | 0% { 105 | stroke-dasharray: 1,150; 106 | stroke-dashoffset: 0; 107 | } 108 | 109 | 50% { 110 | stroke-dasharray: 90,150; 111 | stroke-dashoffset: -35; 112 | } 113 | 114 | 100% { 115 | stroke-dasharray: 90,150; 116 | stroke-dashoffset: -124; 117 | }; 118 | } 119 | 120 | .animated { 121 | -webkit-animation-duration: .7s; 122 | animation-duration: .7s; 123 | -webkit-animation-fill-mode: both; 124 | animation-fill-mode: both; 125 | } 126 | 127 | .modal.animated, 128 | .animated.fast { 129 | -webkit-animation-duration: .2s; 130 | animation-duration: .2s; 131 | } 132 | 133 | .animated.slow { 134 | -webkit-animation-duration: 1.1s; 135 | animation-duration: 1.1s; 136 | } 137 | 138 | .animated.fadeInDown { 139 | -webkit-animation-name: fadeInDown; 140 | animation-name: fadeInDown; 141 | } 142 | 143 | .animated.fadeIn { 144 | -webkit-animation-name: fadeIn; 145 | animation-name: fadeIn; 146 | } 147 | 148 | .spinner-container { 149 | -webkit-animation: rotate 2s linear infinite; 150 | animation: rotate 2s linear infinite; 151 | z-index: 2; 152 | width: 65px; 153 | height: 65px; 154 | } 155 | 156 | .spinner-container .path { 157 | stroke-dasharray: 1,150; 158 | stroke-dashoffset: 0; 159 | stroke: #2196F3; 160 | stroke-linecap: round; 161 | -webkit-animation: dash 1.5s ease-in-out infinite, colors 5.6s ease-in-out infinite; 162 | animation: dash 1.5s ease-in-out infinite, colors 5.6s ease-in-out infinite; 163 | } -------------------------------------------------------------------------------- /src/css/dialogs.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | word-wrap: break-word; 3 | } 4 | 5 | .modal .label.error-msg { 6 | display: block; 7 | font-size: 12px; 8 | margin-top: 5px; 9 | padding: 0; 10 | padding: 5px; 11 | margin-top: 10px; 12 | text-align: left; 13 | } 14 | 15 | .modal .label.error-msg > span { 16 | white-space: pre-wrap; 17 | } 18 | 19 | .modal .breadcrumb { 20 | margin: 0; 21 | background: #00bcd4; 22 | font-size: 16px; 23 | max-height: inherit; 24 | padding: 0 10px; 25 | margin-bottom: 5px; 26 | } 27 | 28 | .modal-fullscreen .modal-dialog, 29 | .modal-fullscreen .modal-content { 30 | bottom: 0; 31 | left: 0; 32 | position: absolute; 33 | right: 0; 34 | top: 0; 35 | } 36 | 37 | .modal-fullscreen .modal-dialog { 38 | margin: 0; 39 | width: 100%; 40 | } 41 | 42 | .modal-fullscreen .modal-content { 43 | border: none; 44 | -moz-border-radius: 0; 45 | border-radius: 0; 46 | -webkit-box-shadow: inherit; 47 | -moz-box-shadow: inherit; 48 | -o-box-shadow: inherit; 49 | box-shadow: inherit; 50 | } 51 | 52 | .modal-fullscreen textarea.code { 53 | min-height: 450px; 54 | } 55 | 56 | .modal img.preview { 57 | max-width: 100%; 58 | max-height: 640px; 59 | border-radius: 3px; 60 | } 61 | 62 | .modal img.preview.loading { 63 | width: 100%; 64 | height: 1px; 65 | opacity: 0; 66 | } 67 | 68 | 69 | .modal .modal-content { 70 | border-radius: 10px 10px 4px 4px; 71 | } 72 | 73 | .modal .modal-header { 74 | border-radius: 4px 4px 0 0; 75 | background: #2196F3; 76 | padding: 1.3em; 77 | } 78 | 79 | .modal .modal-header .modal-title { 80 | font-size: 20px; 81 | line-height: 100%; 82 | color: #D4E5F5; 83 | margin: 0; 84 | } 85 | 86 | .modal .modal-header .close { 87 | opacity: 1; 88 | color: #D4E5F5; 89 | } 90 | 91 | .modal .modal-header .close.fullscreen { 92 | font-size: 14px; 93 | position: relative; 94 | top: 4px; 95 | margin-right: .8em; 96 | } -------------------------------------------------------------------------------- /src/css/main.scss: -------------------------------------------------------------------------------- 1 | 2 | *, 3 | *:focus { 4 | outline: 0 !important; 5 | } 6 | 7 | .navbar { 8 | min-height: 32px; 9 | margin-bottom: 0; 10 | border: 0; 11 | border-radius: 0; 12 | color: #fff; 13 | } 14 | 15 | .navbar .navbar-collapse { 16 | overflow: visible; 17 | padding: 0; 18 | } 19 | 20 | .navbar .navbar-toggle { 21 | padding: 5px 10px; 22 | } 23 | 24 | .navbar .navbar-brand { 25 | font-size: inherit; 26 | height: 55px; 27 | line-height: 100%; 28 | } 29 | 30 | .btn.btn-default { 31 | color: #444; 32 | background-color: #FAFAFA; 33 | } 34 | 35 | .btn { 36 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26); 37 | font-weight: 500; 38 | letter-spacing: .01em; 39 | border: none; 40 | } 41 | 42 | textarea.code { 43 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 44 | font-size: 13px; 45 | min-height: 250px; 46 | resize: vertical; 47 | color: #000; 48 | } 49 | 50 | .sub-header { 51 | padding-bottom: 10px; 52 | border-bottom: 1px solid #eee; 53 | } 54 | 55 | .sidebar { 56 | display: none; 57 | background: #fafafa; 58 | margin-top: 2px; 59 | padding: 0; 60 | overflow-x: hidden; 61 | overflow-y: auto; 62 | border-right: 1px solid #eee; 63 | } 64 | 65 | .btn-go-back { 66 | margin-top: -5px; 67 | } 68 | 69 | .main .page-header { 70 | margin-top: 0; 71 | } 72 | 73 | .file-tree-root { 74 | margin-left: 0; 75 | button.btn { 76 | border: none; 77 | box-shadow: none; 78 | svg.icon { 79 | width: 1.5rem; 80 | height: 1.5rem; 81 | } 82 | } 83 | .nav-sidebar { 84 | margin-right: -21px; 85 | margin-left: 20px; 86 | 87 | svg.icon.chevron { 88 | color: $text-color; 89 | width: 1rem; 90 | height: 1rem; 91 | } 92 | } 93 | 94 | } 95 | 96 | .table td { 97 | vertical-align: middle; 98 | } 99 | 100 | .iconset { 101 | padding: 10px; 102 | } 103 | 104 | .col-120 { 105 | width: 100px; 106 | max-height: 100px; 107 | float: left; 108 | margin-bottom: 9px; 109 | margin-right: 9px; 110 | } 111 | 112 | .col-120:last-child { 113 | margin-right: 0; 114 | } 115 | 116 | .noselect { 117 | -webkit-touch-callout: none; /* iOS Safari */ 118 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 119 | -khtml-user-select: none; /* Konqueror */ 120 | -moz-user-select: none; /* Firefox */ 121 | -ms-user-select: none; /* IE/Edge */ 122 | user-select: none; /* non-prefixed version, currently */ 123 | } 124 | 125 | .iconset .thumbnail { 126 | border-radius: 0; 127 | overflow: hidden; 128 | margin: 0; 129 | padding: 10px 0; 130 | border: none; 131 | background: none; 132 | display: block; 133 | svg.icon { 134 | width: 4rem; 135 | height: 4rem; 136 | margin-bottom: 1px; 137 | } 138 | } 139 | 140 | .table-files .selected, 141 | .iconset .thumbnail.selected { 142 | background: #2196F3; 143 | } 144 | 145 | .iconset .thumbnail.selected, 146 | .table-files .selected td, 147 | .table-files .selected td a { 148 | color: #fff; 149 | } 150 | 151 | .detail-sources { 152 | text-overflow: ellipsis; 153 | overflow: hidden; 154 | word-wrap: break-word; 155 | } 156 | 157 | .upload-dragover .main { 158 | opacity: .4; 159 | } 160 | 161 | .upload-dragover:before { 162 | content: "\e198"; 163 | position: absolute; 164 | left: 50%; 165 | top: 50%; 166 | transform: translate(-50%, -50%); 167 | z-index: 100; 168 | color: #2196F3; 169 | font-size: 8em; 170 | font-family: 'Glyphicons Halflings'; 171 | } 172 | 173 | .upload-list { 174 | margin-top: 20px; 175 | } 176 | 177 | .spinner-wrapper { 178 | margin: 0 auto; 179 | text-align: center; 180 | margin-top: 8%; 181 | } 182 | 183 | a:hover, 184 | a:active, 185 | a:focus, 186 | table th > a:hover, 187 | table th > a:active, 188 | table th > a:focus { 189 | text-decoration: none; 190 | } 191 | 192 | .sortorder:after { 193 | color: #2196f3; 194 | content: '\25bc'; 195 | } 196 | 197 | .sortorder.reverse:after { 198 | color: #2196f3; 199 | content: '\25b2'; 200 | } 201 | 202 | [ng\:cloak], [ng-cloak], 203 | [data-ng-cloak], [x-ng-cloak], 204 | .ng-cloak, .x-ng-cloak { 205 | display: none !important; 206 | } 207 | 208 | .mr2 { 209 | margin-right: 2px; 210 | } 211 | 212 | .mr5 { 213 | margin-right: 5px; 214 | } 215 | 216 | .mt10 { 217 | margin-top: 10px; 218 | } 219 | 220 | .mb0 { 221 | margin-bottom: 0; 222 | } 223 | 224 | .pointer { 225 | cursor: pointer; 226 | } 227 | 228 | .block { 229 | display: block; 230 | } 231 | 232 | .ellipsis { 233 | overflow: hidden; 234 | text-overflow: ellipsis; 235 | white-space: nowrap; 236 | } 237 | 238 | .bold { 239 | font-weight: bold; 240 | } 241 | 242 | .main { 243 | overflow-y: auto; 244 | } 245 | 246 | @media (min-width: 768px) { 247 | .main { 248 | padding-right: 0; 249 | padding-left: 0; 250 | } 251 | 252 | /* The view should fill all available vertical space */ 253 | angular-filemanager > div, .row, .main, .sidebar { 254 | height: 100%; 255 | } 256 | 257 | .container-fluid { 258 | height: -webkit-calc(100% - 58px); 259 | height: -moz-calc(100% - 58px); 260 | height: calc(100% - 58px); 261 | } 262 | 263 | .sidebar { 264 | display: block; 265 | } 266 | } 267 | 268 | .selected-file-details { 269 | padding-left: 20px; 270 | } 271 | 272 | .item-extension::after { 273 | font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; 274 | content: attr(data-ext); 275 | left: 4px; 276 | position: absolute; 277 | color: #fff; 278 | font-size: 9px; 279 | text-transform: uppercase; 280 | top: 21px; 281 | } 282 | 283 | .selected .item-extension::after { 284 | color: #2196F3; 285 | } 286 | 287 | .form-control.search-input { 288 | max-width: 20em; 289 | display: inline; 290 | } 291 | 292 | .like-code { 293 | display: inline; 294 | } 295 | 296 | .point { 297 | margin-right: 8px; 298 | font-size: 10px; 299 | } 300 | 301 | .navbar .btn.btn-flat { 302 | padding: 2px; 303 | width: 32px; 304 | height: 30px; 305 | margin-left: 5px; 306 | } 307 | 308 | .navbar-inverse .navbar-toggle .icon-bar { 309 | background: #fff; 310 | } 311 | 312 | .navbar-inverse .navbar-form input[type="text"] { 313 | color: #7a7a7a; 314 | box-shadow: none; 315 | margin: 0 10px; 316 | } 317 | 318 | .navbar .navbar-form { 319 | border-bottom: none; 320 | border-top: none; 321 | box-shadow: none; 322 | padding: 0; 323 | margin: 12px 0; 324 | } 325 | 326 | .breadcrumb { 327 | background: none; 328 | padding: 0; 329 | font-size: 17px; 330 | margin: 12px 0; 331 | overflow: hidden; 332 | max-height: 30px 333 | } 334 | 335 | .breadcrumb > .active, 336 | .breadcrumb a { 337 | color: #fff; 338 | } 339 | 340 | .breadcrumb-item + .breadcrumb-item::before { 341 | color: #ffffff; 342 | } 343 | 344 | .scrollable-menu { 345 | height: auto; 346 | max-height: 200px; 347 | overflow-x: hidden; 348 | } 349 | 350 | .btn.btn-flat { 351 | background: none; 352 | color: #fff; 353 | } 354 | 355 | .btn-group.open > .btn-flat, 356 | .btn.btn-flat, 357 | .btn.btn-flat:active { 358 | box-shadow: none; 359 | } 360 | 361 | .btn.btn-flat > i { 362 | font-size: 18px; 363 | width: 18px; 364 | height: 18px; 365 | line-height: 100%; 366 | } 367 | -------------------------------------------------------------------------------- /src/css/sprite.scss: -------------------------------------------------------------------------------- 1 | svg.icon { 2 | width: 1rem; 3 | height: 1rem; 4 | display: inline-block; 5 | position: relative; 6 | vertical-align: middle; 7 | fill: currentColor; 8 | transition: fill 0.15s ease-in-out; 9 | &.r-90 { 10 | transform: rotate(90deg); 11 | } 12 | &.r-180 { 13 | transform: rotate(180deg); 14 | } 15 | &.r-270 { 16 | transform: rotate(270deg); 17 | } 18 | &.icon-lg { 19 | width: 2rem; 20 | height: 2rem; 21 | } 22 | &.disabled { 23 | filter: grayscale(100%); 24 | -webkit-filter: grayscale(100%); 25 | } 26 | } 27 | 28 | @-moz-document url-prefix() { 29 | svg.icon { 30 | top: 0; 31 | } 32 | } 33 | 34 | .btn-icon { 35 | border-radius: 500px; 36 | padding: 0; 37 | svg { 38 | margin: 0; 39 | top: 0; 40 | width: 35px; 41 | height: 35px; 42 | } 43 | &.btn-sm { 44 | svg { 45 | width: 25px; 46 | height: 25px; 47 | } 48 | } 49 | } 50 | 51 | h2 { 52 | svg.icon { 53 | width: 30px; 54 | height: 30px; 55 | top: 8px; 56 | } 57 | } 58 | 59 | h3, .h3 { 60 | svg.icon { 61 | width: 1.75rem; 62 | height: 1.75rem; 63 | line-height: 1.2; 64 | top: 0.5rem; 65 | } 66 | } -------------------------------------------------------------------------------- /src/css/style.scss: -------------------------------------------------------------------------------- 1 | @import "animations"; 2 | 3 | angularjs-filemanager { // name-spacing all the styles prevents conflicts when imported in other projects 4 | 5 | @import "variables"; 6 | @import "dialogs"; 7 | @import "main"; 8 | @import "sprite"; 9 | 10 | position: relative; 11 | display: block; 12 | height: 100%; 13 | overflow: hidden; 14 | color: $text-color; 15 | a { 16 | color: $text-color; 17 | } 18 | 19 | } 20 | 21 | #angularjs-filemanager-context-menu { 22 | position: absolute; 23 | display: none; 24 | z-index: 9999; 25 | 26 | .dropdown-menu > li > a { 27 | padding: 6px 20px; 28 | } 29 | 30 | .dropdown-menu > li > a > i { 31 | margin-right: 4px; 32 | } 33 | 34 | .dropdown-menu.dropdown-right-click { 35 | display: block; 36 | position: static; 37 | margin-bottom: 5px; 38 | } 39 | 40 | .dropdown-menu.dropdown-right-click .divider { 41 | margin: 3px 0; 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/css/variables.scss: -------------------------------------------------------------------------------- 1 | $text-color: #474747; 2 | $angularjs-primary-color: #3067cd -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | const app = angular.module('AngularJS-FileManager', ['pascalprecht.translate', 'ngFileUpload']); 2 | 3 | /** 4 | * jQuery inits 5 | */ 6 | angular.element(window.document).on('shown.bs.modal', '.modal', function () { 7 | window.setTimeout(function () { 8 | angular.element('[autofocus]', this).focus(); 9 | }.bind(this), 100); 10 | }); 11 | 12 | angular.element(window.document).on('click', function () { 13 | angular.element('#angularjs-filemanager-context-menu').hide(); 14 | }); 15 | 16 | angular.element(window.document).on('contextmenu', '.main-navigation .table-files tr.item-list:has("td"), .item-list', function (e) { 17 | var menu = angular.element('#angularjs-filemanager-context-menu'); 18 | 19 | if (e.pageX >= window.innerWidth - menu.width()) { 20 | e.pageX -= menu.width(); 21 | } 22 | if (e.pageY >= window.innerHeight - menu.height()) { 23 | e.pageY -= menu.height(); 24 | } 25 | 26 | menu.hide().css({ 27 | left: e.pageX, 28 | top: e.pageY 29 | }).appendTo('body').show(); 30 | e.preventDefault(); 31 | }); 32 | -------------------------------------------------------------------------------- /src/js/controllers/selector-controller.js: -------------------------------------------------------------------------------- 1 | app.controller('ModalFileManagerCtrl', 2 | ['$scope', '$rootScope', 'fileNavigator', function ($scope, $rootScope, FileNavigator) { 3 | 4 | $scope.reverse = false; 5 | $scope.predicate = ['model.type', 'model.name']; 6 | $scope.fileNavigator = new FileNavigator(); 7 | $rootScope.selectedModalPath = []; 8 | 9 | $scope.order = function (predicate) { 10 | $scope.reverse = ($scope.predicate[1] === predicate) ? !$scope.reverse : false; 11 | $scope.predicate[1] = predicate; 12 | }; 13 | 14 | $scope.select = function (item) { 15 | $rootScope.selectedModalPath = item.model.relativePath().split('/').filter(Boolean); 16 | $scope.modal('selector', true); 17 | }; 18 | 19 | $scope.selectCurrent = function () { 20 | $rootScope.selectedModalPath = $scope.fileNavigator.currentPath; 21 | $scope.modal('selector', true); 22 | }; 23 | 24 | $scope.selectedFilesAreChildOfPath = function (item) { 25 | var path = item.model.relativePath(); 26 | return $scope.temps.find(function (item) { 27 | var itemPath = item.model.relativePath(); 28 | if (path === itemPath) { 29 | return true; 30 | } 31 | /* 32 | if (path.startsWith(itemPath)) { 33 | fixme names in same folder like folder-one and folder-one-two 34 | at the moment fixed hidding affected folders 35 | } 36 | */ 37 | }); 38 | }; 39 | 40 | $rootScope.openNavigator = function (path) { 41 | $scope.fileNavigator.currentPath = path; 42 | $scope.fileNavigator.refresh(); 43 | $scope.modal('selector'); 44 | }; 45 | 46 | $rootScope.getSelectedPath = function () { 47 | var path = $rootScope.selectedModalPath.filter(Boolean); 48 | var result = '/' + path.join('/'); 49 | if ($scope.singleSelection() && !$scope.singleSelection().isFolder()) { 50 | result += '/' + $scope.singleSelection().tempModel.name; 51 | } 52 | return result.replace(/\/\//, '/'); 53 | }; 54 | 55 | }]); 56 | -------------------------------------------------------------------------------- /src/js/directives/directives.js: -------------------------------------------------------------------------------- 1 | app.directive('angularjsFilemanager', ['$parse', 'fileManagerConfig', function ($parse, fileManagerConfig) { 2 | return { 3 | restrict: 'E', 4 | templateUrl: fileManagerConfig.tplPath + '/main.html', 5 | scope: { 6 | onSelect: '&' 7 | } 8 | }; 9 | }]); 10 | 11 | app.directive('ngFile', ['$parse', function ($parse) { 12 | return { 13 | restrict: 'A', 14 | link: function (scope, element, attrs) { 15 | var model = $parse(attrs.ngFile); 16 | var modelSetter = model.assign; 17 | 18 | element.bind('change', function () { 19 | scope.$apply(function () { 20 | modelSetter(scope, element[0].files); 21 | }); 22 | }); 23 | } 24 | }; 25 | }]); 26 | 27 | app.directive('ngRightClick', ['$parse', function ($parse) { 28 | return function (scope, element, attrs) { 29 | var fn = $parse(attrs.ngRightClick); 30 | element.bind('contextmenu', function (event) { 31 | scope.$apply(function () { 32 | event.preventDefault(); 33 | fn(scope, {$event: event}); 34 | }); 35 | }); 36 | }; 37 | }]); 38 | 39 | -------------------------------------------------------------------------------- /src/js/entities/chmod.js: -------------------------------------------------------------------------------- 1 | app.service('chmod', function () { 2 | 3 | var Chmod = function (initValue) { 4 | this.owner = this.getRwxObj(); 5 | this.group = this.getRwxObj(); 6 | this.others = this.getRwxObj(); 7 | 8 | if (initValue) { 9 | var codes = isNaN(initValue) ? 10 | this.convertfromCode(initValue) : 11 | this.convertfromOctal(initValue); 12 | 13 | if (!codes) { 14 | throw new Error('Invalid chmod input data (%s)'.replace('%s', initValue)); 15 | } 16 | 17 | this.owner = codes.owner; 18 | this.group = codes.group; 19 | this.others = codes.others; 20 | } 21 | }; 22 | 23 | Chmod.prototype.toOctal = function (prepend, append) { 24 | var result = []; 25 | ['owner', 'group', 'others'].forEach(function (key, i) { 26 | result[i] = this[key].read && this.octalValues.read || 0; 27 | result[i] += this[key].write && this.octalValues.write || 0; 28 | result[i] += this[key].exec && this.octalValues.exec || 0; 29 | }.bind(this)); 30 | return (prepend || '') + result.join('') + (append || ''); 31 | }; 32 | 33 | Chmod.prototype.toCode = function (prepend, append) { 34 | var result = []; 35 | ['owner', 'group', 'others'].forEach(function (key, i) { 36 | result[i] = this[key].read && this.codeValues.read || '-'; 37 | result[i] += this[key].write && this.codeValues.write || '-'; 38 | result[i] += this[key].exec && this.codeValues.exec || '-'; 39 | }.bind(this)); 40 | return (prepend || '') + result.join('') + (append || ''); 41 | }; 42 | 43 | Chmod.prototype.getRwxObj = function () { 44 | return { 45 | read: false, 46 | write: false, 47 | exec: false 48 | }; 49 | }; 50 | 51 | Chmod.prototype.octalValues = { 52 | read: 4, write: 2, exec: 1 53 | }; 54 | 55 | Chmod.prototype.codeValues = { 56 | read: 'r', write: 'w', exec: 'x' 57 | }; 58 | 59 | Chmod.prototype.convertfromCode = function (str) { 60 | str = ('' + str).replace(/\s/g, ''); 61 | str = str.length === 10 ? str.substr(1) : str; 62 | if (!/^[-rwxts]{9}$/.test(str)) { 63 | return; 64 | } 65 | 66 | var result = [], vals = str.match(/.{1,3}/g); 67 | for (var i in vals) { 68 | var rwxObj = this.getRwxObj(); 69 | rwxObj.read = /r/.test(vals[i]); 70 | rwxObj.write = /w/.test(vals[i]); 71 | rwxObj.exec = /x|t/.test(vals[i]); 72 | result.push(rwxObj); 73 | } 74 | 75 | return { 76 | owner: result[0], 77 | group: result[1], 78 | others: result[2] 79 | }; 80 | }; 81 | 82 | Chmod.prototype.convertfromOctal = function (str) { 83 | str = ('' + str).replace(/\s/g, ''); 84 | str = str.length === 4 ? str.substr(1) : str; 85 | if (!/^[0-7]{3}$/.test(str)) { 86 | return; 87 | } 88 | 89 | var result = [], vals = str.match(/.{1}/g); 90 | for (var i in vals) { 91 | var rwxObj = this.getRwxObj(); 92 | rwxObj.read = /[4567]/.test(vals[i]); 93 | rwxObj.write = /[2367]/.test(vals[i]); 94 | rwxObj.exec = /[1357]/.test(vals[i]); 95 | result.push(rwxObj); 96 | } 97 | 98 | return { 99 | owner: result[0], 100 | group: result[1], 101 | others: result[2] 102 | }; 103 | }; 104 | 105 | return Chmod; 106 | }); -------------------------------------------------------------------------------- /src/js/entities/item.js: -------------------------------------------------------------------------------- 1 | app.factory('item', ['fileManagerConfig', 'chmod', function (fileManagerConfig, Chmod) { 2 | 3 | var Item = function (model, path) { 4 | var rawModel = { 5 | name: model && model.name || '', 6 | path: path || [], 7 | type: model && model.type || 'file', 8 | size: model && parseInt(model.size || 0), 9 | date: parseMySQLDate(model && model.date), 10 | perms: new Chmod(model && model.rights), 11 | content: model && model.content || '', 12 | recursive: false, 13 | relativePath: function () { 14 | var path = this.path.filter(Boolean); 15 | return ('/' + path.join('/') + '/' + this.name).replace(/\/\//, '/'); 16 | }, 17 | fullPath: function () { 18 | return (fileManagerConfig.serverUrl + this.relativePath()); 19 | } 20 | }; 21 | 22 | this.error = ''; 23 | this.processing = false; 24 | 25 | this.model = angular.copy(rawModel); 26 | this.tempModel = angular.copy(rawModel); 27 | 28 | function parseMySQLDate(mysqlDate) { 29 | var d = (mysqlDate || '').toString().split(/[- :]/); 30 | return new Date(d[0], d[1] - 1, d[2], d[3], d[4], d[5]); 31 | } 32 | }; 33 | 34 | Item.prototype.update = function () { 35 | angular.extend(this.model, angular.copy(this.tempModel)); 36 | }; 37 | 38 | Item.prototype.revert = function () { 39 | angular.extend(this.tempModel, angular.copy(this.model)); 40 | this.error = ''; 41 | }; 42 | 43 | Item.prototype.isFolder = function () { 44 | return this.model.type === 'dir'; 45 | }; 46 | 47 | Item.prototype.isEditable = function () { 48 | return !this.isFolder() && fileManagerConfig.isEditableFilePattern.test(this.model.name); 49 | }; 50 | 51 | Item.prototype.isImage = function () { 52 | return fileManagerConfig.isImageFilePattern.test(this.model.name); 53 | }; 54 | 55 | Item.prototype.isCompressible = function () { 56 | return this.isFolder(); 57 | }; 58 | 59 | Item.prototype.isExtractable = function () { 60 | return !this.isFolder() && fileManagerConfig.isExtractableFilePattern.test(this.model.name); 61 | }; 62 | 63 | Item.prototype.isSelectable = function () { 64 | return (this.isFolder() && fileManagerConfig.allowedActions.pickFolders) || (!this.isFolder() && fileManagerConfig.allowedActions.pickFiles); 65 | }; 66 | 67 | return Item; 68 | }]); -------------------------------------------------------------------------------- /src/js/filters/filters.js: -------------------------------------------------------------------------------- 1 | app.filter('strLimit', ['$filter', function ($filter) { 2 | return function (input, limit, more) { 3 | if (input.length <= limit) { 4 | return input; 5 | } 6 | return $filter('limitTo')(input, limit) + (more || '...'); 7 | }; 8 | }]); 9 | 10 | app.filter('fileExtension', ['$filter', function ($filter) { 11 | return function (input) { 12 | return /\./.test(input) && $filter('strLimit')(input.split('.').pop(), 3, '..') || ''; 13 | }; 14 | }]); 15 | 16 | app.filter('formatDate', ['$filter', function () { 17 | return function (input) { 18 | return input instanceof Date ? 19 | input.toISOString().substring(0, 19).replace('T', ' ') : 20 | (input.toLocaleString || input.toString).apply(input); 21 | }; 22 | }]); 23 | 24 | app.filter('humanReadableFileSize', ['$filter', 'fileManagerConfig', function ($filter, fileManagerConfig) { 25 | // See https://en.wikipedia.org/wiki/Binary_prefix 26 | var decimalByteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']; 27 | var binaryByteUnits = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; 28 | 29 | return function (input) { 30 | var i = -1; 31 | var fileSizeInBytes = input; 32 | 33 | do { 34 | fileSizeInBytes = fileSizeInBytes / 1024; 35 | i++; 36 | } while (fileSizeInBytes > 1024); 37 | 38 | var result = fileManagerConfig.useBinarySizePrefixes ? binaryByteUnits[i] : decimalByteUnits[i]; 39 | return Math.max(fileSizeInBytes, 0.1).toFixed(1) + ' ' + result; 40 | }; 41 | }]); 42 | -------------------------------------------------------------------------------- /src/js/providers/config.js: -------------------------------------------------------------------------------- 1 | app.provider('fileManagerConfig', function () { 2 | var bridgePath = 'bridges/php/handler.php'; 3 | var values = { 4 | appName: 'AngularJS File Manager', 5 | defaultLang: 'en', 6 | multiLang: true, 7 | bridgePath: bridgePath, 8 | listUrl: bridgePath, 9 | uploadUrl: bridgePath, 10 | renameUrl: bridgePath, 11 | copyUrl: bridgePath, 12 | moveUrl: bridgePath, 13 | removeUrl: bridgePath, 14 | editUrl: bridgePath, 15 | getContentUrl: bridgePath, 16 | createFolderUrl: bridgePath, 17 | downloadFileUrl: bridgePath, 18 | downloadMultipleUrl: bridgePath, 19 | compressUrl: bridgePath, 20 | extractUrl: bridgePath, 21 | permissionsUrl: bridgePath, 22 | basePath: '/', 23 | serverUrl: '', // leading string in building public path for files (e.g. https://example.com/uploads) 24 | 25 | searchForm: true, 26 | sidebar: true, 27 | breadcrumb: true, 28 | allowedActions: { 29 | upload: true, 30 | rename: true, 31 | move: true, 32 | copy: true, 33 | edit: true, 34 | changePermissions: true, 35 | compress: true, 36 | compressChooseName: true, 37 | extract: true, 38 | download: true, 39 | downloadMultiple: true, 40 | preview: true, 41 | remove: true, 42 | createFolder: true, 43 | pickFiles: false, 44 | pickFolders: false 45 | }, 46 | 47 | multipleDownloadFileName: 'angularjs-filemanager.zip', 48 | filterFileExtensions: [], 49 | showExtensionIcons: true, 50 | showSizeForDirectories: false, 51 | useBinarySizePrefixes: false, 52 | downloadFilesByAjax: true, 53 | previewImagesInModal: true, 54 | enablePermissionsRecursive: true, 55 | compressAsync: false, 56 | extractAsync: false, 57 | pickCallback: null, 58 | 59 | isEditableFilePattern: /\.(txt|diff?|patch|svg|asc|cnf|cfg|conf|html?|.html|cfm|cgi|aspx?|ini|pl|py|md|css|cs|js|jsp|log|htaccess|htpasswd|gitignore|gitattributes|env|json|atom|eml|rss|markdown|sql|xml|xslt?|sh|rb|as|bat|cmd|cob|for|ftn|frm|frx|inc|lisp|scm|coffee|php[3-6]?|java|c|cbl|go|h|scala|vb|tmpl|lock|go|yml|yaml|tsv|lst)$/i, 60 | isImageFilePattern: /\.(jpe?g|gif|bmp|png|svg|tiff?)$/i, 61 | isExtractableFilePattern: /\.(gz|tar|rar|g?zip)$/i, 62 | tplPath: 'src/templates' 63 | }; 64 | 65 | return { 66 | $get: function () { 67 | return values; 68 | }, 69 | set: function (constants) { 70 | angular.extend(values, constants); 71 | }, 72 | setBridge: function (path) { 73 | 74 | bridgePath = path; 75 | values.bridgePath = path; 76 | values.listUrl = path; 77 | values.uploadUrl = bridgePath; 78 | values.renameUrl = bridgePath; 79 | values.copyUrl = bridgePath; 80 | values.moveUrl = bridgePath; 81 | values.removeUrl = bridgePath; 82 | values.editUrl = bridgePath; 83 | values.getContentUrl = bridgePath; 84 | values.createFolderUrl = bridgePath; 85 | values.downloadFileUrl = bridgePath; 86 | values.downloadMultipleUrl = bridgePath; 87 | values.compressUrl = bridgePath; 88 | values.extractUrl = bridgePath; 89 | values.permissionsUrl = bridgePath; 90 | 91 | } 92 | }; 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /src/js/services/apihandler.js: -------------------------------------------------------------------------------- 1 | app.service('apiHandler', ['$http', '$q', '$window', '$translate', '$httpParamSerializer', 'Upload', 2 | function ($http, $q, $window, $translate, $httpParamSerializer, Upload) { 3 | 4 | $http.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 5 | 6 | var ApiHandler = function () { 7 | this.inprocess = false; 8 | this.asyncSuccess = false; 9 | this.error = ''; 10 | }; 11 | 12 | ApiHandler.prototype.deferredHandler = function (data, deferred, code, defaultMsg) { 13 | if (!data || typeof data !== 'object') { 14 | this.error = 'Error %s - Bridge response error, please check the API docs or this ajax response.'.replace('%s', code); 15 | } 16 | if (code == 404) { 17 | this.error = 'Error 404 - Backend bridge is not working, please check the ajax response.'; 18 | } 19 | if (data.result && data.result.error) { 20 | this.error = data.result.error; 21 | } 22 | if (!this.error && data.error) { 23 | this.error = data.error.message; 24 | } 25 | if (!this.error && defaultMsg) { 26 | this.error = defaultMsg; 27 | } 28 | if (this.error) { 29 | return deferred.reject(data); 30 | } 31 | return deferred.resolve(data); 32 | }; 33 | 34 | ApiHandler.prototype.list = function (apiUrl, path, customDeferredHandler, exts) { 35 | var self = this; 36 | var dfHandler = customDeferredHandler || self.deferredHandler; 37 | var deferred = $q.defer(); 38 | var data = { 39 | action: 'list', 40 | path: path, 41 | fileExtensions: exts && exts.length ? exts : undefined 42 | }; 43 | 44 | self.inprocess = true; 45 | self.error = ''; 46 | 47 | $http.post(apiUrl, data).then(function (response) { 48 | dfHandler(response.data, deferred, response.status); 49 | }, function (response) { 50 | dfHandler(response.data, deferred, response.status, 'Unknown error listing, check the response'); 51 | })['finally'](function () { 52 | self.inprocess = false; 53 | }); 54 | return deferred.promise; 55 | }; 56 | 57 | ApiHandler.prototype.copy = function (apiUrl, items, path, singleFilename) { 58 | var self = this; 59 | var deferred = $q.defer(); 60 | var data = { 61 | action: 'copy', 62 | items: items, 63 | newPath: path 64 | }; 65 | 66 | if (singleFilename && items.length === 1) { 67 | data.singleFilename = singleFilename; 68 | } 69 | 70 | self.inprocess = true; 71 | self.error = ''; 72 | $http.post(apiUrl, data).then(function (response) { 73 | self.deferredHandler(response.data, deferred, response.status); 74 | }, function (response) { 75 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_copying')); 76 | })['finally'](function () { 77 | self.inprocess = false; 78 | }); 79 | return deferred.promise; 80 | }; 81 | 82 | ApiHandler.prototype.move = function (apiUrl, items, path) { 83 | var self = this; 84 | var deferred = $q.defer(); 85 | var data = { 86 | action: 'move', 87 | items: items, 88 | newPath: path 89 | }; 90 | self.inprocess = true; 91 | self.error = ''; 92 | $http.post(apiUrl, data).then(function (response) { 93 | self.deferredHandler(response.data, deferred, response.status); 94 | }, function (response) { 95 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_moving')); 96 | })['finally'](function () { 97 | self.inprocess = false; 98 | }); 99 | return deferred.promise; 100 | }; 101 | 102 | ApiHandler.prototype.remove = function (apiUrl, items) { 103 | var self = this; 104 | var deferred = $q.defer(); 105 | var data = { 106 | action: 'remove', 107 | items: items 108 | }; 109 | 110 | self.inprocess = true; 111 | self.error = ''; 112 | $http.post(apiUrl, data).then(function (response) { 113 | self.deferredHandler(response.data, deferred, response.status); 114 | }, function (response) { 115 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_deleting')); 116 | })['finally'](function () { 117 | self.inprocess = false; 118 | }); 119 | return deferred.promise; 120 | }; 121 | 122 | ApiHandler.prototype.upload = function (apiUrl, destination, files) { 123 | var self = this; 124 | var deferred = $q.defer(); 125 | self.inprocess = true; 126 | self.progress = 0; 127 | self.error = ''; 128 | 129 | var data = { 130 | destination: destination 131 | }; 132 | 133 | for (var i = 0; i < files.length; i++) { 134 | data['file-' + i] = files[i]; 135 | } 136 | 137 | if (files && files.length) { 138 | Upload.upload({ 139 | url: apiUrl, 140 | data: data 141 | }).then(function (data) { 142 | self.deferredHandler(data.data, deferred, data.status); 143 | }, function (data) { 144 | self.deferredHandler(data.data, deferred, data.status, 'Unknown error uploading files'); 145 | }, function (evt) { 146 | self.progress = Math.min(100, parseInt(100.0 * evt.loaded / evt.total)) - 1; 147 | })['finally'](function () { 148 | self.inprocess = false; 149 | self.progress = 0; 150 | }); 151 | } 152 | 153 | return deferred.promise; 154 | }; 155 | 156 | ApiHandler.prototype.getContent = function (apiUrl, itemPath) { 157 | var self = this; 158 | var deferred = $q.defer(); 159 | var data = { 160 | action: 'getContent', 161 | item: itemPath 162 | }; 163 | 164 | self.inprocess = true; 165 | self.error = ''; 166 | $http.post(apiUrl, data).then(function (response) { 167 | self.deferredHandler(response.data, deferred, response.status); 168 | }, function (response) { 169 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_getting_content')); 170 | })['finally'](function () { 171 | self.inprocess = false; 172 | }); 173 | return deferred.promise; 174 | }; 175 | 176 | ApiHandler.prototype.edit = function (apiUrl, itemPath, content) { 177 | var self = this; 178 | var deferred = $q.defer(); 179 | var data = { 180 | action: 'edit', 181 | item: itemPath, 182 | content: content 183 | }; 184 | 185 | self.inprocess = true; 186 | self.error = ''; 187 | 188 | $http.post(apiUrl, data).then(function (response) { 189 | self.deferredHandler(response.data, deferred, response.status); 190 | }, function (response) { 191 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_modifying')); 192 | })['finally'](function () { 193 | self.inprocess = false; 194 | }); 195 | return deferred.promise; 196 | }; 197 | 198 | ApiHandler.prototype.rename = function (apiUrl, itemPath, newPath) { 199 | var self = this; 200 | var deferred = $q.defer(); 201 | var data = { 202 | action: 'rename', 203 | item: itemPath, 204 | newItemPath: newPath 205 | }; 206 | self.inprocess = true; 207 | self.error = ''; 208 | $http.post(apiUrl, data).then(function (response) { 209 | self.deferredHandler(response.data, deferred, response.status); 210 | }, function (response) { 211 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_renaming')); 212 | })['finally'](function () { 213 | self.inprocess = false; 214 | }); 215 | return deferred.promise; 216 | }; 217 | 218 | ApiHandler.prototype.getUrl = function (apiUrl, path) { 219 | var data = { 220 | action: 'download', 221 | path: path 222 | }; 223 | return path && [apiUrl, $httpParamSerializer(data)].join('?'); 224 | }; 225 | 226 | ApiHandler.prototype.download = function (apiUrl, itemPath, toFilename, downloadByAjax, forceNewWindow) { 227 | var self = this; 228 | var url = this.getUrl(apiUrl, itemPath); 229 | 230 | if (!downloadByAjax || forceNewWindow || !$window.saveAs) { 231 | !$window.saveAs && $window.console.log('Your browser dont support ajax download, downloading by default'); 232 | return !!$window.open(url, '_blank', ''); 233 | } 234 | 235 | var deferred = $q.defer(); 236 | self.inprocess = true; 237 | $http.get(url).then(function (response) { 238 | var bin = new $window.Blob([response.data]); 239 | deferred.resolve(response.data); 240 | $window.saveAs(bin, toFilename); 241 | }, function (response) { 242 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_downloading')); 243 | })['finally'](function () { 244 | self.inprocess = false; 245 | }); 246 | return deferred.promise; 247 | }; 248 | 249 | ApiHandler.prototype.downloadMultiple = function (apiUrl, items, toFilename, downloadByAjax, forceNewWindow) { 250 | var self = this; 251 | var deferred = $q.defer(); 252 | var data = { 253 | action: 'downloadMultiple', 254 | items: items, 255 | toFilename: toFilename 256 | }; 257 | var url = [apiUrl, $httpParamSerializer(data)].join('?'); 258 | 259 | if (!downloadByAjax || forceNewWindow || !$window.saveAs) { 260 | !$window.saveAs && $window.console.log('Your browser dont support ajax download, downloading by default'); 261 | return !!$window.open(url, '_blank', ''); 262 | } 263 | 264 | self.inprocess = true; 265 | $http.get(apiUrl).then(function (response) { 266 | var bin = new $window.Blob([response.data]); 267 | deferred.resolve(response.data); 268 | $window.saveAs(bin, toFilename); 269 | }, function (response) { 270 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_downloading')); 271 | })['finally'](function () { 272 | self.inprocess = false; 273 | }); 274 | return deferred.promise; 275 | }; 276 | 277 | ApiHandler.prototype.compress = function (apiUrl, items, compressedFilename, path) { 278 | var self = this; 279 | var deferred = $q.defer(); 280 | var data = { 281 | action: 'compress', 282 | items: items, 283 | destination: path, 284 | compressedFilename: compressedFilename 285 | }; 286 | 287 | self.inprocess = true; 288 | self.error = ''; 289 | $http.post(apiUrl, data).then(function (response) { 290 | self.deferredHandler(response.data, deferred, response.status); 291 | }, function (response) { 292 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_compressing')); 293 | })['finally'](function () { 294 | self.inprocess = false; 295 | }); 296 | return deferred.promise; 297 | }; 298 | 299 | ApiHandler.prototype.extract = function (apiUrl, item, folderName, path) { 300 | var self = this; 301 | var deferred = $q.defer(); 302 | var data = { 303 | action: 'extract', 304 | item: item, 305 | destination: path, 306 | folderName: folderName 307 | }; 308 | 309 | self.inprocess = true; 310 | self.error = ''; 311 | $http.post(apiUrl, data).then(function (response) { 312 | self.deferredHandler(response.data, deferred, response.status); 313 | }, function (response) { 314 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_extracting')); 315 | })['finally'](function () { 316 | self.inprocess = false; 317 | }); 318 | return deferred.promise; 319 | }; 320 | 321 | ApiHandler.prototype.changePermissions = function (apiUrl, items, permsOctal, permsCode, recursive) { 322 | var self = this; 323 | var deferred = $q.defer(); 324 | var data = { 325 | action: 'changePermissions', 326 | items: items, 327 | perms: permsOctal, 328 | permsCode: permsCode, 329 | recursive: !!recursive 330 | }; 331 | 332 | self.inprocess = true; 333 | self.error = ''; 334 | $http.post(apiUrl, data).then(function (response) { 335 | self.deferredHandler(response.data, deferred, response.status); 336 | }, function (response) { 337 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_changing_perms')); 338 | })['finally'](function () { 339 | self.inprocess = false; 340 | }); 341 | return deferred.promise; 342 | }; 343 | 344 | ApiHandler.prototype.createFolder = function (apiUrl, path) { 345 | var self = this; 346 | var deferred = $q.defer(); 347 | var data = { 348 | action: 'createFolder', 349 | newPath: path 350 | }; 351 | 352 | self.inprocess = true; 353 | self.error = ''; 354 | $http.post(apiUrl, data).then(function (response) { 355 | self.deferredHandler(response.data, deferred, response.status); 356 | }, function (response) { 357 | self.deferredHandler(response.data, deferred, response.status, $translate.instant('error_creating_folder')); 358 | })['finally'](function () { 359 | self.inprocess = false; 360 | }); 361 | 362 | return deferred.promise; 363 | }; 364 | 365 | return ApiHandler; 366 | 367 | }]); 368 | -------------------------------------------------------------------------------- /src/js/services/apimiddleware.js: -------------------------------------------------------------------------------- 1 | app.service('apiMiddleware', ['$window', 'fileManagerConfig', 'apiHandler', 2 | function ($window, fileManagerConfig, ApiHandler) { 3 | 4 | var ApiMiddleware = function () { 5 | this.apiHandler = new ApiHandler(); 6 | }; 7 | 8 | ApiMiddleware.prototype.getPath = function (arrayPath) { 9 | return '/' + arrayPath.join('/'); 10 | }; 11 | 12 | ApiMiddleware.prototype.getFileList = function (files) { 13 | return (files || []).map(function (file) { 14 | return file && file.model.relativePath(); 15 | }); 16 | }; 17 | 18 | ApiMiddleware.prototype.getFilePath = function (item) { 19 | return item && item.model.relativePath(); 20 | }; 21 | 22 | ApiMiddleware.prototype.list = function (path, customDeferredHandler) { 23 | return this.apiHandler.list(fileManagerConfig.listUrl, this.getPath(path), customDeferredHandler); 24 | }; 25 | 26 | ApiMiddleware.prototype.copy = function (files, path) { 27 | var items = this.getFileList(files); 28 | var singleFilename = items.length === 1 ? files[0].tempModel.name : undefined; 29 | return this.apiHandler.copy(fileManagerConfig.copyUrl, items, this.getPath(path), singleFilename); 30 | }; 31 | 32 | ApiMiddleware.prototype.move = function (files, path) { 33 | var items = this.getFileList(files); 34 | return this.apiHandler.move(fileManagerConfig.moveUrl, items, this.getPath(path)); 35 | }; 36 | 37 | ApiMiddleware.prototype.remove = function (files) { 38 | var items = this.getFileList(files); 39 | return this.apiHandler.remove(fileManagerConfig.removeUrl, items); 40 | }; 41 | 42 | ApiMiddleware.prototype.upload = function (files, path) { 43 | if (!$window.FormData) { 44 | throw new Error('Unsupported browser version'); 45 | } 46 | 47 | var destination = this.getPath(path); 48 | 49 | return this.apiHandler.upload(fileManagerConfig.uploadUrl, destination, files); 50 | }; 51 | 52 | ApiMiddleware.prototype.getContent = function (item) { 53 | var itemPath = this.getFilePath(item); 54 | return this.apiHandler.getContent(fileManagerConfig.getContentUrl, itemPath); 55 | }; 56 | 57 | ApiMiddleware.prototype.edit = function (item) { 58 | var itemPath = this.getFilePath(item); 59 | return this.apiHandler.edit(fileManagerConfig.editUrl, itemPath, item.tempModel.content); 60 | }; 61 | 62 | ApiMiddleware.prototype.rename = function (item) { 63 | var itemPath = this.getFilePath(item); 64 | var newPath = item.tempModel.relativePath(); 65 | 66 | return this.apiHandler.rename(fileManagerConfig.renameUrl, itemPath, newPath); 67 | }; 68 | 69 | ApiMiddleware.prototype.getUrl = function (item) { 70 | var itemPath = this.getFilePath(item); 71 | return this.apiHandler.getUrl(fileManagerConfig.downloadFileUrl, itemPath); 72 | }; 73 | 74 | ApiMiddleware.prototype.download = function (item, forceNewWindow) { 75 | //TODO: add spinner to indicate file is downloading 76 | var itemPath = this.getFilePath(item); 77 | var toFilename = item.model.name; 78 | 79 | if (item.isFolder()) { 80 | return; 81 | } 82 | 83 | return this.apiHandler.download( 84 | fileManagerConfig.downloadFileUrl, 85 | itemPath, 86 | toFilename, 87 | fileManagerConfig.downloadFilesByAjax, 88 | forceNewWindow 89 | ); 90 | }; 91 | 92 | ApiMiddleware.prototype.downloadMultiple = function (files, forceNewWindow) { 93 | var items = this.getFileList(files); 94 | var timestamp = new Date().getTime().toString().substr(8, 13); 95 | var toFilename = timestamp + '-' + fileManagerConfig.multipleDownloadFileName; 96 | 97 | return this.apiHandler.downloadMultiple( 98 | fileManagerConfig.downloadMultipleUrl, 99 | items, 100 | toFilename, 101 | fileManagerConfig.downloadFilesByAjax, 102 | forceNewWindow 103 | ); 104 | }; 105 | 106 | ApiMiddleware.prototype.compress = function (files, compressedFilename, path) { 107 | var items = this.getFileList(files); 108 | return this.apiHandler.compress(fileManagerConfig.compressUrl, items, compressedFilename, this.getPath(path)); 109 | }; 110 | 111 | ApiMiddleware.prototype.extract = function (item, folderName, path) { 112 | var itemPath = this.getFilePath(item); 113 | return this.apiHandler.extract(fileManagerConfig.extractUrl, itemPath, folderName, this.getPath(path)); 114 | }; 115 | 116 | ApiMiddleware.prototype.changePermissions = function (files, dataItem) { 117 | var items = this.getFileList(files); 118 | var code = dataItem.tempModel.perms.toCode(); 119 | var octal = dataItem.tempModel.perms.toOctal(); 120 | var recursive = !!dataItem.tempModel.recursive; 121 | 122 | return this.apiHandler.changePermissions(fileManagerConfig.permissionsUrl, items, code, octal, recursive); 123 | }; 124 | 125 | ApiMiddleware.prototype.createFolder = function (item) { 126 | var path = item.tempModel.relativePath(); 127 | return this.apiHandler.createFolder(fileManagerConfig.createFolderUrl, path); 128 | }; 129 | 130 | return ApiMiddleware; 131 | 132 | }]); -------------------------------------------------------------------------------- /src/js/services/filenavigator.js: -------------------------------------------------------------------------------- 1 | app.service('fileNavigator', [ 2 | 'apiMiddleware', 'fileManagerConfig', 'item', function (ApiMiddleware, fileManagerConfig, Item) { 3 | 4 | var FileNavigator = function () { 5 | this.apiMiddleware = new ApiMiddleware(); 6 | this.requesting = false; 7 | this.fileList = []; 8 | this.currentPath = this.getBasePath(); 9 | this.history = []; 10 | this.error = ''; 11 | 12 | this.onRefresh = function () { 13 | }; 14 | }; 15 | 16 | FileNavigator.prototype.getBasePath = function () { 17 | var path = (fileManagerConfig.basePath || '').replace(/^\//, ''); 18 | return path.trim() ? path.split('/') : []; 19 | }; 20 | 21 | FileNavigator.prototype.deferredHandler = function (data, deferred, code, defaultMsg) { 22 | if (!data || typeof data !== 'object') { 23 | this.error = 'Error %s - Bridge response error, please check the API docs or this ajax response.'.replace('%s', code); 24 | } 25 | if (code == 404) { 26 | this.error = 'Error 404 - Backend bridge is not working, please check the ajax response.'; 27 | } 28 | if (code == 200) { 29 | this.error = null; 30 | } 31 | if (!this.error && data.result && data.result.error) { 32 | this.error = data.result.error; 33 | } 34 | if (!this.error && data.error) { 35 | this.error = data.error.message; 36 | } 37 | if (!this.error && defaultMsg) { 38 | this.error = defaultMsg; 39 | } 40 | if (this.error) { 41 | return deferred.reject(data); 42 | } 43 | return deferred.resolve(data); 44 | }; 45 | 46 | FileNavigator.prototype.list = function () { 47 | return this.apiMiddleware.list(this.currentPath, this.deferredHandler.bind(this)); 48 | }; 49 | 50 | FileNavigator.prototype.refresh = function () { 51 | var self = this; 52 | if (!self.currentPath.length) { 53 | self.currentPath = this.getBasePath(); 54 | } 55 | var path = self.currentPath.join('/'); 56 | self.requesting = true; 57 | self.fileList = []; 58 | return self.list().then(function (data) { 59 | self.fileList = (data.result || []).map(function (file) { 60 | return new Item(file, self.currentPath); 61 | }); 62 | self.buildTree(path); 63 | self.onRefresh(); 64 | }).finally(function () { 65 | self.requesting = false; 66 | }); 67 | }; 68 | 69 | FileNavigator.prototype.buildTree = function (path) { 70 | var flatNodes = [], selectedNode = {}; 71 | 72 | function recursive(parent, item, path) { 73 | var absName = path ? (path + '/' + item.model.name) : item.model.name; 74 | if (parent.name && parent.name.trim() && path.trim().indexOf(parent.name) !== 0) { 75 | parent.nodes = []; 76 | } 77 | if (parent.name !== path) { 78 | parent.nodes.forEach(function (nd) { 79 | recursive(nd, item, path); 80 | }); 81 | } else { 82 | for (var e in parent.nodes) { 83 | if (parent.nodes[e].name === absName) { 84 | return; 85 | } 86 | } 87 | parent.nodes.push({item: item, name: absName, nodes: []}); 88 | } 89 | 90 | parent.nodes = parent.nodes.sort(function (a, b) { 91 | return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : a.name.toLowerCase() === b.name.toLowerCase() ? 0 : 1; 92 | }); 93 | } 94 | 95 | function flatten(node, array) { 96 | array.push(node); 97 | for (var n in node.nodes) { 98 | flatten(node.nodes[n], array); 99 | } 100 | } 101 | 102 | function findNode(data, path) { 103 | return data.filter(function (n) { 104 | return n.name === path; 105 | })[0]; 106 | } 107 | 108 | //!this.history.length && this.history.push({name: '', nodes: []}); 109 | !this.history.length && this.history.push({name: this.getBasePath()[0] || '', nodes: []}); 110 | flatten(this.history[0], flatNodes); 111 | selectedNode = findNode(flatNodes, path); 112 | selectedNode && (selectedNode.nodes = []); 113 | 114 | for (var o in this.fileList) { 115 | var item = this.fileList[o]; 116 | item instanceof Item && item.isFolder() && recursive(this.history[0], item, path); 117 | } 118 | }; 119 | 120 | FileNavigator.prototype.folderClick = function (item) { 121 | this.currentPath = []; 122 | if (item && item.isFolder()) { 123 | this.currentPath = item.model.relativePath().split('/').splice(1); 124 | } 125 | this.refresh(); 126 | }; 127 | 128 | FileNavigator.prototype.upDir = function () { 129 | if (this.currentPath[0]) { 130 | this.currentPath = this.currentPath.slice(0, -1); 131 | this.refresh(); 132 | } 133 | }; 134 | 135 | FileNavigator.prototype.goTo = function (index) { 136 | this.currentPath = this.currentPath.slice(0, index + 1); 137 | this.refresh(); 138 | }; 139 | 140 | FileNavigator.prototype.fileNameExists = function (fileName) { 141 | return this.fileList.find(function (item) { 142 | return fileName && item.model.name.trim() === fileName.trim(); 143 | }); 144 | }; 145 | 146 | FileNavigator.prototype.listHasFolders = function () { 147 | return this.fileList.find(function (item) { 148 | return item.model.type === 'dir'; 149 | }); 150 | }; 151 | 152 | FileNavigator.prototype.getCurrentFolderName = function () { 153 | return this.currentPath.slice(-1)[0] || '/'; 154 | }; 155 | 156 | return FileNavigator; 157 | }]); 158 | -------------------------------------------------------------------------------- /src/svg/chevron-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/svg/ext/css.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/doc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/html.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/jpg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/js.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/png.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/txt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/unknown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/ext/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 53 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/svg/folder_closed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/svg/folder_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/svg/grid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/svg/language.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/svg/list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/svg/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/svg/positive.svg: -------------------------------------------------------------------------------- 1 | Artboard ۱ -------------------------------------------------------------------------------- /src/svg/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 11 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/svg/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/templates/current-folder-breadcrumb.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/item-context-menu.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/main-icons.html: -------------------------------------------------------------------------------- 1 |
2 | 18 | 19 |
20 |
21 |
22 | 23 |
24 | {{"no_files_in_folder" | translate}}... 25 |
26 | 27 |
28 | {{ fileNavigator.error }} 29 |
30 |
-------------------------------------------------------------------------------- /src/templates/main-table-modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 23 | 26 | 27 | 28 | 31 | 32 | 33 | 39 | 44 | 45 | 46 |
5 | 6 | {{"name" | translate}} 7 | 8 | 9 |
16 |
17 |
21 | {{"no_folders_in_folder" | translate}}... 22 | 24 | 25 |
29 | {{ fileNavigator.error }} 30 |
34 | 35 | 36 | {{item.model.name | strLimit : 32}} 37 | 38 | 40 | 43 |
-------------------------------------------------------------------------------- /src/templates/main-table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 22 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 40 | 41 | 42 | 45 | 46 | 47 | 54 | 59 | 62 | 65 | 66 | 67 |
5 | 6 | {{"name" | translate}} 7 | 8 | 9 |
33 |
34 |
38 | {{"no_files_in_folder" | translate}}... 39 |
43 | {{ fileNavigator.error }} 44 |
48 | 49 | 50 | 51 | {{item.model.name | strLimit : 64}} 52 | 53 |
68 | -------------------------------------------------------------------------------- /src/templates/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | 9 | 10 |
12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 | 23 | 24 | -------------------------------------------------------------------------------- /src/templates/navbar.html: -------------------------------------------------------------------------------- 1 | 179 | -------------------------------------------------------------------------------- /src/templates/sidebar.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/templates/spinner.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
--------------------------------------------------------------------------------