├── run ├── public ├── favicon.ico ├── icons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── mstile-150x150.png │ ├── android-chrome-192x192.png │ ├── android-chrome-384x384.png │ ├── browserconfig.xml │ ├── manifest.json │ └── safari-pinned-tab.svg ├── index.html ├── css │ ├── main.css │ └── bootstrap.min.css ├── text_only │ └── index.html ├── js │ └── main.js └── bootstrap_config.json ├── nodejs lan file upload.png ├── package.json ├── .gitignore ├── LICENSE ├── README.md ├── main.js └── fileshare.js /run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | node main.js -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barissenkal/Node.js-LAN-File-Sharing/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /nodejs lan file upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barissenkal/Node.js-LAN-File-Sharing/HEAD/nodejs lan file upload.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barissenkal/Node.js-LAN-File-Sharing/HEAD/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barissenkal/Node.js-LAN-File-Sharing/HEAD/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barissenkal/Node.js-LAN-File-Sharing/HEAD/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barissenkal/Node.js-LAN-File-Sharing/HEAD/public/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barissenkal/Node.js-LAN-File-Sharing/HEAD/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/icons/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barissenkal/Node.js-LAN-File-Sharing/HEAD/public/icons/android-chrome-384x384.png -------------------------------------------------------------------------------- /public/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #00a300 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "icons": [ 4 | { 5 | "src": "\/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image\/png" 8 | }, 9 | { 10 | "src": "\/android-chrome-384x384.png", 11 | "sizes": "384x384", 12 | "type": "image\/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "display": "standalone" 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs_lan_file_sharing", 3 | "version": "1.0.0", 4 | "description": "A small Node.js app designed for sharing files while on the same network. Uses express for file uploads and (static) file downloads.", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "node main.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Baris Senkal (http://etekmekketcap.com)", 11 | "license": "ISC", 12 | "dependencies": { 13 | "chokidar": "^3.3.1", 14 | "express": "^4.14.0", 15 | "formidable": "^1.2.2", 16 | "qr-image": "^3.2.0", 17 | "vue": "^2.6.11" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Directory used by the app for uploaded files 40 | /files 41 | .idea/ 42 | public/qr_codes/ 43 | 44 | # npm package-lock 45 | package-lock.json 46 | 47 | # Forever related 48 | forever-config.json 49 | forever.log 50 | forever_err.log -------------------------------------------------------------------------------- /public/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Barış Şenkal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js-LAN-File-Sharing ![version: beta](https://img.shields.io/badge/version-beta-orange) 2 | A small Node.js app designed for sharing files while on the same network. Especially useful when you are trying to get a file from a friend and their device has a single broken USB port. 3 | 4 | ## Features 5 | - Easy to use Drag and Drop file upload. 6 | - Faster than uploading to a server then downloading since you are the server. 7 | - Works with large files (tested with >2gb). 8 | - The page opens fast due to being lightweight. (Native JS + ![vue 2.x](https://img.shields.io/badge/vue-2.x-brightgreen.svg)) 9 | 10 | ## How to use 11 | 0. Clone (or download as zip) this project from github 12 | 0. Open terminal (or command line) in the project's folder. 13 | 0. Run ```npm install``` and then ```node main.js```. 14 | 0. The project will display your LAN ip and port (8080 by default) in the terminal once running. 15 | 0. On another device in the LAN, go to the *url:port*. 16 | 0. Drag & Drop any file in browser to transfer. 17 | 18 | Project saves sent files in "files" folder. Also files in "files" folder can be downloaded from the browser. 19 | 20 | PORT environment variable is used to change the port. e.g. ```export PORT=3030; node main.js``` 21 | 22 | ## Screenshots 23 | 24 | ![Imgur](http://i.imgur.com/fxuSrmE.png) 25 | 26 | Your LAN IP is shown on the page in addition to console output. Files in "files" directory are also listed for download. 27 | 28 | ![Imgur](http://i.imgur.com/U4IFJsj.png) 29 | 30 | Drag and Drop file upload. Archived without any javascript library. 31 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var fileShare = require('./fileshare')({ 3 | filesFolderPath: (process.argv[3] || null), 4 | port: (process.argv[2] || process.env.PORT), 5 | allowDeletion: false, 6 | multiUpload: false, 7 | folderUpload: false, 8 | progressCallback: function (progress, doneFileName) { 9 | //TODO: connect to UI when writing the electron app. 10 | if(progress != null) { 11 | console.log("Progress: " + Math.floor(progress) + "%"); 12 | } else { 13 | console.log("Done file", doneFileName); 14 | } 15 | }, 16 | errorCallback: function (url, err) { 17 | if (err.status == 404) { 18 | console.log("(Not Found) " + url); 19 | } else { 20 | console.log("(errorCallback) " + url); 21 | console.error(err); 22 | } 23 | } 24 | }); 25 | 26 | var server = http.createServer(fileShare.app); 27 | 28 | function onError(error) { 29 | if (error.syscall !== 'listen') { 30 | throw error; 31 | } 32 | 33 | var bind = typeof port === 'string' 34 | ? 'Pipe ' + fileShare.port 35 | : 'Port ' + fileShare.port; 36 | 37 | // handle specific listen errors with friendly messages 38 | switch (error.code) { 39 | case 'EACCES': 40 | console.error(bind + ' requires elevated privileges'); 41 | process.exit(1); 42 | break; 43 | case 'EADDRINUSE': 44 | console.error(bind + ' is already in use'); 45 | process.exit(1); 46 | break; 47 | default: 48 | throw error; 49 | } 50 | } 51 | 52 | function onListening() { 53 | var addr = server.address(); 54 | 55 | if (typeof addr === 'string') { 56 | console.log('Listening on pipe ' + addr); 57 | } else { 58 | fileShare.addresses.forEach(function (address) { 59 | console.log('Listening on ' + address + ':' + addr.port); 60 | }); 61 | } 62 | } 63 | 64 | server.listen(fileShare.port); 65 | server.on('error', onError); 66 | server.on('listening', onListening); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Node.js LAN File Sharing 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |

LAN File Sharing

25 | 26 |

27 |
28 |
29 |
30 |
31 | 32 |
33 |

Drag file in page or click below:

34 |
35 |
36 | Choose File 37 | 38 |
39 |
40 |
41 | 42 |
43 | 44 | 49 | 50 | 55 | 56 |
57 | 58 |
59 | 65 |
66 | 67 |
68 |
69 |
70 | 71 | 72 |
73 | ... 74 |
75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | body { 12 | font-family: -apple-system, Roboto, Helvetica, Arial, sans-serif; 13 | margin: 0; 14 | padding: 0; 15 | height: 100vh; 16 | width: 100vw; 17 | } 18 | @media (prefers-color-scheme: dark) { 19 | html, body { 20 | background: #111; 21 | color: white; 22 | } 23 | } 24 | 25 | #successoverlay { 26 | display: none; 27 | white-space: normal; 28 | vertical-align: middle; 29 | z-index: 1000; 30 | } 31 | 32 | body.success>#successoverlay { 33 | display: block; 34 | position: fixed; 35 | top: 0; 36 | bottom: 0; 37 | left: 0; 38 | right: 0; 39 | background: rgba(0, 0, 0, 0.65); 40 | z-index: 1000; 41 | text-align: center; 42 | padding: 10vw; 43 | } 44 | @media (prefers-color-scheme: dark) { 45 | body.success>#successoverlay { 46 | background: rgba(0, 0, 0, 0.65); 47 | } 48 | } 49 | 50 | body.success>#successoverlay>.title { 51 | color: white; 52 | font-family: monospace; 53 | font-size: 64px; 54 | text-shadow: 0 0 20px black; 55 | } 56 | @media (prefers-color-scheme: dark) { 57 | body.success>#successoverlay>.title { 58 | text-shadow: 0 0 20px #54a0ff; 59 | } 60 | } 61 | 62 | .noselect { 63 | -webkit-touch-callout: none; 64 | -webkit-user-select: none; 65 | -khtml-user-select: none; 66 | -moz-user-select: none; 67 | -ms-user-select: none; 68 | user-select: none; 69 | } 70 | 71 | .hidden { 72 | display: none; 73 | } 74 | 75 | .text-center { 76 | text-align: center; 77 | } 78 | 79 | .row { 80 | width: 100%; 81 | text-align: center; 82 | } 83 | 84 | .column { 85 | margin: auto; 86 | width: 100%; 87 | max-width: 500px; 88 | } 89 | 90 | 91 | @media (prefers-color-scheme: dark) { 92 | #theQRCode { 93 | filter: invert(1); 94 | } 95 | } 96 | 97 | .btn { 98 | display: inline-block; 99 | margin-bottom: 0; 100 | font-weight: normal; 101 | text-align: center; 102 | vertical-align: middle; 103 | -ms-touch-action: manipulation; 104 | touch-action: manipulation; 105 | cursor: pointer; 106 | background-image: none; 107 | border: 1px solid transparent; 108 | white-space: nowrap; 109 | padding: 6px 12px; 110 | font-size: 14px; 111 | line-height: 1.42857143; 112 | border-radius: 4px; 113 | -webkit-user-select: none; 114 | -moz-user-select: none; 115 | -ms-user-select: none; 116 | user-select: none; 117 | white-space: normal; 118 | vertical-align: middle; 119 | } 120 | 121 | .btn-primary { 122 | color: #fff; 123 | background-color: #337ab7; 124 | border-color: #2e6da4; 125 | } 126 | @media (prefers-color-scheme: dark) { 127 | .btn-primary { 128 | color: #fff; 129 | background-color: #2c9dff; 130 | border-color: #6dbbff; 131 | } 132 | } 133 | 134 | h1 { 135 | font-size: 36px; 136 | font-family: inherit; 137 | font-weight: 500; 138 | line-height: 1.1; 139 | color: #333; 140 | margin: 20px 0.67em 10px; 141 | } 142 | @media (prefers-color-scheme: dark) { 143 | h1 { 144 | color: white; 145 | } 146 | } 147 | 148 | h4 { 149 | font-size: 18px; 150 | margin-top: 10px; 151 | margin-bottom: 10px; 152 | font-weight: 500; 153 | } 154 | 155 | p { 156 | margin: 0 0 10px; 157 | font-size: 14px; 158 | } 159 | 160 | a { 161 | color: #337ab7; 162 | text-decoration: none; 163 | background-color: transparent; 164 | } 165 | @media (prefers-color-scheme: dark) { 166 | a { 167 | color: #33a9ff; 168 | } 169 | } 170 | 171 | a:hover, a:focus { 172 | color: #23527c; 173 | text-decoration: underline 174 | } 175 | @media (prefers-color-scheme: dark) { 176 | a:hover, a:focus { 177 | color: #66b1ff; 178 | } 179 | } 180 | 181 | a:focus { 182 | outline: 5px auto -webkit-focus-ring-color; 183 | outline-offset: -2px 184 | } 185 | 186 | #IPandPort { 187 | color: #0af; 188 | font-family: monospace; 189 | line-height: 40px; 190 | } 191 | @media (prefers-color-scheme: dark) { 192 | #IPandPort { 193 | color: #2cb0ff; 194 | } 195 | } 196 | 197 | #IPandPort.error { 198 | color: #ff1a00; 199 | } 200 | 201 | #IPandPort.warning { 202 | color: #ffb700; 203 | } 204 | 205 | #fileToUploadButtonBox { 206 | height: 65px; 207 | padding-top: 20px; 208 | } 209 | 210 | #fileToUploadButton { 211 | padding: 0; 212 | width: 78px; 213 | height: 22px; 214 | margin: 0; 215 | display: inline-block; 216 | transform: scale(2); 217 | cursor: pointer; 218 | z-index: 100; 219 | } 220 | 221 | #fileToUploadButton .button-text { 222 | font-size: 12px; 223 | line-height: 22px; 224 | } 225 | 226 | #fileToUpload { 227 | position: relative; 228 | top: -22px; 229 | margin: 0; 230 | width: 78px; 231 | height: 22px; 232 | opacity: 0; 233 | } 234 | 235 | .folder-item { 236 | border-radius: 12px; 237 | border: 1px dashed #666; 238 | margin-bottom: 8px; 239 | padding: 1px; 240 | } 241 | @media (prefers-color-scheme: dark) { 242 | .folder-item { 243 | border: 2px dashed #aaa; 244 | background: rgba(64, 64, 64, 0.2); 245 | } 246 | } 247 | 248 | .folder-item.closed .folder-item { 249 | display: none; 250 | } 251 | 252 | .folder-name { 253 | padding: 8px; 254 | word-break: break-word; 255 | } 256 | 257 | .folder-item.closed>.folder-name { 258 | opacity: 0.6; 259 | } 260 | 261 | .file-item { 262 | display: flex; 263 | align-items: flex-end; 264 | padding: .75rem 1.25rem; 265 | margin-bottom: 8px; 266 | border-radius: 12px; 267 | border: 1px solid #666; 268 | word-break: break-word; 269 | } 270 | @media (prefers-color-scheme: dark) { 271 | .file-item { 272 | border: 2px solid #aaa; 273 | background: rgba(64, 64, 64, 0.2); 274 | } 275 | } 276 | 277 | .folder-item.closed .file-item { 278 | display: none; 279 | } 280 | 281 | .file-name { 282 | word-break: break-word; 283 | flex: 1; 284 | } 285 | 286 | .file-delete-button { 287 | cursor: pointer; 288 | margin-left: -1.5rem; 289 | z-index: 2; 290 | } -------------------------------------------------------------------------------- /public/text_only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Node.js LAN File Sharing [Text Only] 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 92 | 93 | 94 | 95 | 96 |
97 |
98 |
99 |

LAN File Sharing [Text Only]

100 |

101 |
102 |
103 |
104 |
105 | 106 |
107 |

Drag file in page or click below:

108 |
109 |
110 | Choose File 111 | 112 |
113 |
114 |
115 | 116 |
117 | 118 | 123 | 124 | 129 | 130 |
131 | 132 |
133 |
134 | 135 |
136 |
137 |
138 | 139 | 140 |
141 |    Uploading... 142 |
143 | 144 | 296 | 297 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // @ts-check 3 | 4 | /** @typedef {import('../../fileshare').Content} Content */ 5 | /** @typedef {import('../../fileshare').ServerInfoResult} ServerInfoResult */ 6 | 7 | /* Getting server info */ 8 | 9 | const IPandPortElement = document.getElementById('IPandPort'); 10 | const theQRCodeElement = document.getElementById('theQRCode'); 11 | 12 | let previousIpPortString = null; 13 | /** 14 | * @param {Array} addresses 15 | * @param {number} port 16 | */ 17 | function updateAddressInfo(addresses, port) { 18 | const newIpPortString = addresses.map(function (address) { 19 | return [address, port].join(":"); 20 | }).join(", "); 21 | if (previousIpPortString != newIpPortString) { 22 | IPandPortElement.innerText = newIpPortString; 23 | if (addresses.length > 0) { 24 | theQRCodeElement.setAttribute("src", `/qr_codes/${addresses[0]}_${port}.png`) 25 | } else { 26 | theQRCodeElement.setAttribute("src", ""); 27 | } 28 | previousIpPortString = newIpPortString; 29 | } 30 | } 31 | 32 | /** @typedef {"success"|"warning"|"error"} AddressInfoStatusEnum */ 33 | 34 | /** 35 | * @param {AddressInfoStatusEnum} statusEnum 36 | */ 37 | function updateAddressInfoStatus(statusEnum) { 38 | if (statusEnum == "success") { 39 | IPandPortElement.classList.remove("error", "warning"); 40 | } else if (statusEnum == "warning") { 41 | IPandPortElement.classList.remove("error"); 42 | IPandPortElement.classList.add("warning"); 43 | } else if (statusEnum == "error") { 44 | IPandPortElement.classList.remove("warning"); 45 | IPandPortElement.classList.add("error"); 46 | } else { 47 | console.error("updateAddressWError unknown statusEnum", statusEnum); 48 | } 49 | } 50 | 51 | /** @type {Object} */ 52 | let _path2isCollapsed = null; 53 | let _isCollapsedCacheTimeout = null; 54 | const isCollapsedCache = { 55 | get(path) { 56 | if(_path2isCollapsed == null) { 57 | let cacheStr = null; 58 | try { 59 | cacheStr = localStorage.getItem("path2isCollapsed"); 60 | } catch (error) { 61 | console.error("localStorage.getItem error", error); 62 | } 63 | if(cacheStr != null) { 64 | _path2isCollapsed = JSON.parse(cacheStr); 65 | } else { 66 | _path2isCollapsed = {}; 67 | } 68 | } 69 | return _path2isCollapsed[path] == true; 70 | }, 71 | set(path, isCollapsed) { 72 | if(isCollapsed) { 73 | _path2isCollapsed[path] = true; 74 | } else { 75 | // NOTE(baris): No need to store since default state is not collapsed 76 | delete _path2isCollapsed[path]; 77 | } 78 | 79 | if(_isCollapsedCacheTimeout != null) clearTimeout(_isCollapsedCacheTimeout); 80 | 81 | _isCollapsedCacheTimeout = setTimeout(() => { 82 | let cacheStr = JSON.stringify(_path2isCollapsed) 83 | try { 84 | localStorage.setItem("path2isCollapsed", cacheStr); 85 | } catch (error) { 86 | console.error("localStorage.getItem error", error); 87 | } 88 | }, 100); 89 | } 90 | } 91 | 92 | Vue.component('folderItem', { 93 | created() { 94 | // console.log("folderItem created"); 95 | this.isCollapsed = isCollapsedCache.get(this.contentObject.path); 96 | }, 97 | data () { 98 | return { 99 | "isCollapsed": false, 100 | } 101 | }, 102 | watch: { 103 | "isCollapsed": function () { 104 | isCollapsedCache.set(this.contentObject.path, this.isCollapsed); 105 | } 106 | }, 107 | template: `
108 |
109 | /{{contentObject.path}} 110 |
111 | 117 |
`, 118 | props: ["contentObject", "allowDeletion"] 119 | }) 120 | 121 | Vue.component('fileItem', { 122 | // created() { console.log("fileItem created");}, 123 | template: ``, 140 | props: ["contentObject", "allowDeletion"] 141 | }) 142 | 143 | Vue.component('listItem', { 144 | // created() { console.log("listItem created");}, 145 | template: ` 146 | 151 | 156 | `, 157 | props: ["content", "deletion"] 158 | }) 159 | 160 | 161 | // TODO(baris): comment 162 | Vue.config.devtools = true 163 | 164 | const fileListVueApp = new Vue({ 165 | el: '#fileList', 166 | data: { 167 | rootContentObject: {"contents": []}, 168 | allowDeletion: false 169 | }, 170 | created() { 171 | // ... 172 | }, 173 | methods: { 174 | /** 175 | * @param {Content} rootContentObject 176 | * @param {boolean} allowDeletion 177 | */ 178 | updateFiles(rootContentObject, allowDeletion) { 179 | this.rootContentObject = rootContentObject; 180 | this.allowDeletion = allowDeletion; 181 | }, 182 | } 183 | }) 184 | Vue.prototype.$deleteFile = deleteFile; 185 | 186 | /** 187 | * @param {Content} rootContentObject 188 | * @param {boolean} allowDeletion 189 | */ 190 | function updateFiles(rootContentObject, allowDeletion) { 191 | console.log("rootContentObject", rootContentObject, allowDeletion); 192 | if (!(rootContentObject instanceof Object)) { 193 | fileListVueApp.updateFiles({"contents":[]}, allowDeletion); 194 | return; 195 | } 196 | fileListVueApp.updateFiles(rootContentObject, allowDeletion); 197 | } 198 | 199 | /** 200 | * @param {Content} contentObject 201 | * @param {boolean} allowDeletion 202 | * //TODO(baris) do this with document.createElement ? Use Virtual DOM instead ??? 203 | */ 204 | function generateHTMLFromContentRecursive(contentObject, allowDeletion) { 205 | let strArray = []; 206 | 207 | if (contentObject.folder) { 208 | if (contentObject.path != "") { 209 | strArray.push(`
`); 210 | strArray.push(`
/${contentObject.path}
`); 211 | } 212 | 213 | strArray.push(...contentObject.contents.map((childContentObject) => { 214 | return generateHTMLFromContentRecursive(childContentObject, allowDeletion); 215 | })); 216 | 217 | if (contentObject.path != "") { 218 | strArray.push(`
`); 219 | } 220 | } else { 221 | strArray.push(`
`); 222 | strArray.push(`${contentObject.name}`); 223 | if (allowDeletion) { 224 | strArray.push(`×`); 225 | } 226 | strArray.push(`
`); 227 | } 228 | return strArray.join(''); 229 | } 230 | 231 | function deleteFile(path) { 232 | if (!confirm("Are you sure?")) return; 233 | const request = new XMLHttpRequest() 234 | request.open('GET', `./delete/` + encodeURIComponent(path), true) 235 | request.send(); 236 | } 237 | 238 | class ServerError extends Error { 239 | /** 240 | * @param {XMLHttpRequest} request 241 | */ 242 | constructor(request) { 243 | super(); 244 | this.request = request; 245 | this.name = "ServerError"; 246 | } 247 | } 248 | class ConnectionError extends Error { 249 | /** 250 | * @param {XMLHttpRequest} request 251 | */ 252 | constructor(request) { 253 | super(); 254 | this.request = request; 255 | this.name = "ConnectionError"; 256 | } 257 | } 258 | 259 | /** 260 | * @param {string} [oldMD5=null] 261 | * @returns {Promise} 262 | */ 263 | function getServerInfo(oldMD5 = null) { 264 | return new Promise((resolve, reject) => { 265 | const request = new XMLHttpRequest(); 266 | const URL = (oldMD5 != null ? `/info?md5=${encodeURIComponent(oldMD5)}` : '/info'); 267 | request.open('GET', URL, true); 268 | 269 | request.onload = function () { 270 | if (request.status >= 200 && request.status < 400) { 271 | const data = JSON.parse(request.responseText); 272 | resolve(data); 273 | } else { 274 | console.error("getServerInfo server error", request); 275 | reject(new ServerError(request)); 276 | } 277 | }; 278 | request.onerror = function () { 279 | console.error("getServerInfo connection error", request); 280 | reject(new ConnectionError(request)); 281 | }; 282 | 283 | request.send(); 284 | }) 285 | } 286 | 287 | let multiUploadFileInput = false; 288 | let folderUploadFileInput = false; 289 | let previousRootContentMD5 = null; 290 | function refreshInfoOnPage() { 291 | return getServerInfo(previousRootContentMD5).then((infoResult) => { 292 | const { addresses, port, rootContent, rootContentMD5, allowDeletion, multiUpload, folderUpload } = infoResult; 293 | 294 | if ( 295 | (addresses != null && (addresses instanceof Array)) && 296 | (port != null && !isNaN(port)) 297 | ) { 298 | updateAddressInfo(addresses, port); 299 | updateAddressInfoStatus("success"); 300 | } 301 | 302 | if ( 303 | (previousRootContentMD5 == null) || 304 | (previousRootContentMD5 != rootContentMD5) 305 | ) { 306 | updateFiles(rootContent, allowDeletion); 307 | previousRootContentMD5 = rootContentMD5; 308 | } 309 | 310 | if(multiUpload && !multiUploadFileInput) { 311 | multiUploadFileInput = true; 312 | fileToUploadInputElement.addAttribute("multiple"); 313 | } else if(!multiUpload && multiUploadFileInput) { 314 | multiUploadFileInput = false; 315 | fileToUploadInputElement.removeAttribute("multiple"); 316 | } 317 | 318 | if(folderUpload && !folderUploadFileInput) { 319 | folderUploadFileInput = true; 320 | fileToUploadInputElement.addAttribute("webkitdirectory"); 321 | } else if(!folderUpload && folderUploadFileInput) { 322 | folderUploadFileInput = false; 323 | fileToUploadInputElement.removeAttribute("webkitdirectory"); 324 | } 325 | 326 | 327 | }, (error) => { 328 | if (error instanceof ServerError) { 329 | updateAddressInfoStatus("warning"); 330 | } else if (error instanceof ServerError) { 331 | updateAddressInfoStatus("error"); 332 | } else { 333 | console.error("refreshInfoOnPage error", error); 334 | } 335 | }).then(() => { 336 | setTimeout(() => { 337 | refreshInfoOnPage(); 338 | }, 1000); 339 | }) 340 | } 341 | refreshInfoOnPage(); 342 | 343 | /* Query based upload success error stuff */ 344 | 345 | /** @type {URLSearchParams|Map} */ 346 | let TheURLSearchParams = null; 347 | try { 348 | let theURL = new URL(location.href); 349 | TheURLSearchParams = theURL.searchParams; 350 | } catch (error) { 351 | console.error("URL parse error", error); 352 | } 353 | if (TheURLSearchParams == null) { // Dirty hack polyfill. 354 | try { 355 | /** @type {Array<[string, string]>} */ 356 | let parts; 357 | if (location.search != null) { 358 | parts = (location.search.substring(1)).split("&").map((pairStr) => { 359 | let [key, rawValue] = pairStr.split("="); 360 | return [ 361 | key, 362 | ( 363 | rawValue != null ? 364 | decodeURIComponent(rawValue) : 365 | null 366 | ) 367 | ]; 368 | }); 369 | } else { 370 | parts = []; 371 | } 372 | TheURLSearchParams = new Map(parts); 373 | } catch (error) { 374 | console.error("Manuel TheURLSearchParams parse error", error); 375 | } 376 | } 377 | 378 | function clearSearchQuery() { 379 | window.history.replaceState('obj', document.title, "http://" + location.host + location.pathname); 380 | } 381 | 382 | // if (TheURLSearchParams.has("success")) { 383 | // document.getElementById('successPanel').classList.remove("hidden"); 384 | // document.getElementById('fileSuccessName').innerText = decodeURIComponent(TheURLSearchParams.get("success")); 385 | // clearSearchQuery(); 386 | // } else if (TheURLSearchParams.has("error")) { 387 | // document.getElementById('errorPanel').classList.remove("hidden"); 388 | // clearSearchQuery(); 389 | // } 390 | 391 | /* Drag n Drop Upload */ 392 | 393 | const dragDropHolder = document.body; 394 | const fileToUploadButton = document.getElementById('fileToUploadButton'); 395 | 396 | dragDropHolder.ondragover = function (e) { 397 | e.preventDefault(); 398 | e.stopPropagation(); 399 | 400 | fileToUploadButton.style.position = "fixed"; 401 | fileToUploadButton.style.top = (e.pageY - 11) + "px"; 402 | fileToUploadButton.style.left = (e.pageX - 40) + "px"; 403 | 404 | }; 405 | 406 | const fixButtonBack = function () { fileToUploadButton.style.position = "static"; } 407 | dragDropHolder.ondragend = fixButtonBack; 408 | dragDropHolder.ondragexit = fixButtonBack; 409 | dragDropHolder.ondragleave = function (e) { 410 | //console.log("holder.ondragleave",e); 411 | if (e.target == fileToUploadInputElement 412 | || e.pageX <= 10 || e.pageX >= window.innerWidth - 10 413 | || e.pageY <= 10 || e.pageY >= window.innerHeight - 10) { 414 | fixButtonBack(); 415 | } 416 | } 417 | dragDropHolder.ondrop = function (e) { 418 | if (e.target != fileToUploadInputElement) { 419 | e.preventDefault(); 420 | } 421 | fixButtonBack(); 422 | } 423 | 424 | const errorPanelDiv = document.getElementById('errorPanel') 425 | 426 | function handleFileUploadRequestEvents(eventName, event) { 427 | console.log("handleFileUploadRequestEvents", eventName); 428 | if( 429 | eventName == "load" || 430 | eventName == "abort" || 431 | eventName == "error" || 432 | eventName == "timeout" 433 | ) { 434 | dragDropHolder.classList.remove("success"); 435 | fileToUploadInputElement.value = ""; 436 | } 437 | if( 438 | eventName == "abort" || 439 | eventName == "error" || 440 | eventName == "timeout" 441 | ) { 442 | errorPanelDiv.classList.remove("hidden"); 443 | } 444 | } 445 | 446 | /** @type {HTMLInputElement} */ 447 | // @ts-ignore 448 | const progressMessageDiv = document.getElementById('progressMessage'); 449 | 450 | /** @type {HTMLInputElement} */ 451 | // @ts-ignore 452 | const fileToUploadInputElement = document.getElementById('fileToUpload'); 453 | fileToUploadInputElement.onchange = function (e) { 454 | const files = Array.from(fileToUploadInputElement.files); 455 | console.log("fileToUploadInputElement.onchange files", files); 456 | 457 | dragDropHolder.classList.add("success"); 458 | errorPanelDiv.classList.add("hidden"); 459 | 460 | const uploadRequest = new XMLHttpRequest(); 461 | uploadRequest.open('POST', "/", true); 462 | 463 | uploadRequest.addEventListener('readystatechange', (event) => {handleFileUploadRequestEvents('readystatechange', event)}); 464 | uploadRequest.addEventListener('load', (event) => {handleFileUploadRequestEvents('load', event)}); 465 | uploadRequest.addEventListener('abort', (event) => {handleFileUploadRequestEvents('abort', event)}); 466 | uploadRequest.addEventListener('error', (event) => {handleFileUploadRequestEvents('error', event)}); 467 | uploadRequest.addEventListener('timeout', (event) => {handleFileUploadRequestEvents('timeout', event)}); 468 | 469 | uploadRequest.upload.addEventListener('progress', (event) => { 470 | if(event.lengthComputable) { 471 | const percent = Math.floor((event.loaded / event.total) * 100); 472 | console.log('progress', percent); 473 | progressMessageDiv.innerText = percent + "%"; 474 | } else { 475 | console.log('progress', uploadRequest); 476 | progressMessageDiv.innerHTML = "   Uploading..."; 477 | } 478 | }); 479 | 480 | const formData = new FormData(); 481 | for (let index = 0; index < files.length; index++) { 482 | const file = files[index]; 483 | const filePath = ( 484 | (file.webkitRelativePath != null && file.webkitRelativePath != "") ? 485 | file.webkitRelativePath : 486 | file.name 487 | ) 488 | formData.append(filePath, file); 489 | } 490 | 491 | uploadRequest.send(formData); 492 | } 493 | -------------------------------------------------------------------------------- /public/bootstrap_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "vars": { 3 | "@gray-base": "#000", 4 | "@gray-darker": "lighten(@gray-base, 13.5%)", 5 | "@gray-dark": "lighten(@gray-base, 20%)", 6 | "@gray": "lighten(@gray-base, 33.5%)", 7 | "@gray-light": "lighten(@gray-base, 46.7%)", 8 | "@gray-lighter": "lighten(@gray-base, 93.5%)", 9 | "@brand-primary": "darken(#428bca, 6.5%)", 10 | "@brand-success": "#5cb85c", 11 | "@brand-info": "#5bc0de", 12 | "@brand-warning": "#f0ad4e", 13 | "@brand-danger": "#d9534f", 14 | "@body-bg": "#fff", 15 | "@text-color": "@gray-dark", 16 | "@link-color": "@brand-primary", 17 | "@link-hover-color": "darken(@link-color, 15%)", 18 | "@link-hover-decoration": "underline", 19 | "@font-family-sans-serif": "\"Helvetica Neue\", Helvetica, Arial, sans-serif", 20 | "@font-family-serif": "Georgia, \"Times New Roman\", Times, serif", 21 | "@font-family-monospace": "Menlo, Monaco, Consolas, \"Courier New\", monospace", 22 | "@font-family-base": "@font-family-sans-serif", 23 | "@font-size-base": "14px", 24 | "@font-size-large": "ceil((@font-size-base * 1.25))", 25 | "@font-size-small": "ceil((@font-size-base * 0.85))", 26 | "@font-size-h1": "floor((@font-size-base * 2.6))", 27 | "@font-size-h2": "floor((@font-size-base * 2.15))", 28 | "@font-size-h3": "ceil((@font-size-base * 1.7))", 29 | "@font-size-h4": "ceil((@font-size-base * 1.25))", 30 | "@font-size-h5": "@font-size-base", 31 | "@font-size-h6": "ceil((@font-size-base * 0.85))", 32 | "@line-height-base": "1.428571429", 33 | "@line-height-computed": "floor((@font-size-base * @line-height-base))", 34 | "@headings-font-family": "inherit", 35 | "@headings-font-weight": "500", 36 | "@headings-line-height": "1.1", 37 | "@headings-color": "inherit", 38 | "@icon-font-path": "\"../fonts/\"", 39 | "@icon-font-name": "\"glyphicons-halflings-regular\"", 40 | "@icon-font-svg-id": "\"glyphicons_halflingsregular\"", 41 | "@padding-base-vertical": "6px", 42 | "@padding-base-horizontal": "12px", 43 | "@padding-large-vertical": "10px", 44 | "@padding-large-horizontal": "16px", 45 | "@padding-small-vertical": "5px", 46 | "@padding-small-horizontal": "10px", 47 | "@padding-xs-vertical": "1px", 48 | "@padding-xs-horizontal": "5px", 49 | "@line-height-large": "1.3333333", 50 | "@line-height-small": "1.5", 51 | "@border-radius-base": "4px", 52 | "@border-radius-large": "6px", 53 | "@border-radius-small": "3px", 54 | "@component-active-color": "#fff", 55 | "@component-active-bg": "@brand-primary", 56 | "@caret-width-base": "4px", 57 | "@caret-width-large": "5px", 58 | "@table-cell-padding": "8px", 59 | "@table-condensed-cell-padding": "5px", 60 | "@table-bg": "transparent", 61 | "@table-bg-accent": "#f9f9f9", 62 | "@table-bg-hover": "#f5f5f5", 63 | "@table-bg-active": "@table-bg-hover", 64 | "@table-border-color": "#ddd", 65 | "@btn-font-weight": "normal", 66 | "@btn-default-color": "#333", 67 | "@btn-default-bg": "#fff", 68 | "@btn-default-border": "#ccc", 69 | "@btn-primary-color": "#fff", 70 | "@btn-primary-bg": "@brand-primary", 71 | "@btn-primary-border": "darken(@btn-primary-bg, 5%)", 72 | "@btn-success-color": "#fff", 73 | "@btn-success-bg": "@brand-success", 74 | "@btn-success-border": "darken(@btn-success-bg, 5%)", 75 | "@btn-info-color": "#fff", 76 | "@btn-info-bg": "@brand-info", 77 | "@btn-info-border": "darken(@btn-info-bg, 5%)", 78 | "@btn-warning-color": "#fff", 79 | "@btn-warning-bg": "@brand-warning", 80 | "@btn-warning-border": "darken(@btn-warning-bg, 5%)", 81 | "@btn-danger-color": "#fff", 82 | "@btn-danger-bg": "@brand-danger", 83 | "@btn-danger-border": "darken(@btn-danger-bg, 5%)", 84 | "@btn-link-disabled-color": "@gray-light", 85 | "@btn-border-radius-base": "@border-radius-base", 86 | "@btn-border-radius-large": "@border-radius-large", 87 | "@btn-border-radius-small": "@border-radius-small", 88 | "@input-bg": "#fff", 89 | "@input-bg-disabled": "@gray-lighter", 90 | "@input-color": "@gray", 91 | "@input-border": "#ccc", 92 | "@input-border-radius": "@border-radius-base", 93 | "@input-border-radius-large": "@border-radius-large", 94 | "@input-border-radius-small": "@border-radius-small", 95 | "@input-border-focus": "#66afe9", 96 | "@input-color-placeholder": "#999", 97 | "@input-height-base": "(@line-height-computed + (@padding-base-vertical * 2) + 2)", 98 | "@input-height-large": "(ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2)", 99 | "@input-height-small": "(floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2)", 100 | "@form-group-margin-bottom": "15px", 101 | "@legend-color": "@gray-dark", 102 | "@legend-border-color": "#e5e5e5", 103 | "@input-group-addon-bg": "@gray-lighter", 104 | "@input-group-addon-border-color": "@input-border", 105 | "@cursor-disabled": "not-allowed", 106 | "@dropdown-bg": "#fff", 107 | "@dropdown-border": "rgba(0,0,0,.15)", 108 | "@dropdown-fallback-border": "#ccc", 109 | "@dropdown-divider-bg": "#e5e5e5", 110 | "@dropdown-link-color": "@gray-dark", 111 | "@dropdown-link-hover-color": "darken(@gray-dark, 5%)", 112 | "@dropdown-link-hover-bg": "#f5f5f5", 113 | "@dropdown-link-active-color": "@component-active-color", 114 | "@dropdown-link-active-bg": "@component-active-bg", 115 | "@dropdown-link-disabled-color": "@gray-light", 116 | "@dropdown-header-color": "@gray-light", 117 | "@dropdown-caret-color": "#000", 118 | "@screen-xs": "480px", 119 | "@screen-xs-min": "@screen-xs", 120 | "@screen-phone": "@screen-xs-min", 121 | "@screen-sm": "768px", 122 | "@screen-sm-min": "@screen-sm", 123 | "@screen-tablet": "@screen-sm-min", 124 | "@screen-md": "992px", 125 | "@screen-md-min": "@screen-md", 126 | "@screen-desktop": "@screen-md-min", 127 | "@screen-lg": "1200px", 128 | "@screen-lg-min": "@screen-lg", 129 | "@screen-lg-desktop": "@screen-lg-min", 130 | "@screen-xs-max": "(@screen-sm-min - 1)", 131 | "@screen-sm-max": "(@screen-md-min - 1)", 132 | "@screen-md-max": "(@screen-lg-min - 1)", 133 | "@grid-columns": "12", 134 | "@grid-gutter-width": "30px", 135 | "@grid-float-breakpoint": "@screen-sm-min", 136 | "@grid-float-breakpoint-max": "(@grid-float-breakpoint - 1)", 137 | "@container-tablet": "(720px + @grid-gutter-width)", 138 | "@container-sm": "@container-tablet", 139 | "@container-desktop": "(940px + @grid-gutter-width)", 140 | "@container-md": "@container-desktop", 141 | "@container-large-desktop": "(1140px + @grid-gutter-width)", 142 | "@container-lg": "@container-large-desktop", 143 | "@navbar-height": "50px", 144 | "@navbar-margin-bottom": "@line-height-computed", 145 | "@navbar-border-radius": "@border-radius-base", 146 | "@navbar-padding-horizontal": "floor((@grid-gutter-width / 2))", 147 | "@navbar-padding-vertical": "((@navbar-height - @line-height-computed) / 2)", 148 | "@navbar-collapse-max-height": "340px", 149 | "@navbar-default-color": "#777", 150 | "@navbar-default-bg": "#f8f8f8", 151 | "@navbar-default-border": "darken(@navbar-default-bg, 6.5%)", 152 | "@navbar-default-link-color": "#777", 153 | "@navbar-default-link-hover-color": "#333", 154 | "@navbar-default-link-hover-bg": "transparent", 155 | "@navbar-default-link-active-color": "#555", 156 | "@navbar-default-link-active-bg": "darken(@navbar-default-bg, 6.5%)", 157 | "@navbar-default-link-disabled-color": "#ccc", 158 | "@navbar-default-link-disabled-bg": "transparent", 159 | "@navbar-default-brand-color": "@navbar-default-link-color", 160 | "@navbar-default-brand-hover-color": "darken(@navbar-default-brand-color, 10%)", 161 | "@navbar-default-brand-hover-bg": "transparent", 162 | "@navbar-default-toggle-hover-bg": "#ddd", 163 | "@navbar-default-toggle-icon-bar-bg": "#888", 164 | "@navbar-default-toggle-border-color": "#ddd", 165 | "@navbar-inverse-color": "lighten(@gray-light, 15%)", 166 | "@navbar-inverse-bg": "#222", 167 | "@navbar-inverse-border": "darken(@navbar-inverse-bg, 10%)", 168 | "@navbar-inverse-link-color": "lighten(@gray-light, 15%)", 169 | "@navbar-inverse-link-hover-color": "#fff", 170 | "@navbar-inverse-link-hover-bg": "transparent", 171 | "@navbar-inverse-link-active-color": "@navbar-inverse-link-hover-color", 172 | "@navbar-inverse-link-active-bg": "darken(@navbar-inverse-bg, 10%)", 173 | "@navbar-inverse-link-disabled-color": "#444", 174 | "@navbar-inverse-link-disabled-bg": "transparent", 175 | "@navbar-inverse-brand-color": "@navbar-inverse-link-color", 176 | "@navbar-inverse-brand-hover-color": "#fff", 177 | "@navbar-inverse-brand-hover-bg": "transparent", 178 | "@navbar-inverse-toggle-hover-bg": "#333", 179 | "@navbar-inverse-toggle-icon-bar-bg": "#fff", 180 | "@navbar-inverse-toggle-border-color": "#333", 181 | "@nav-link-padding": "10px 15px", 182 | "@nav-link-hover-bg": "@gray-lighter", 183 | "@nav-disabled-link-color": "@gray-light", 184 | "@nav-disabled-link-hover-color": "@gray-light", 185 | "@nav-tabs-border-color": "#ddd", 186 | "@nav-tabs-link-hover-border-color": "@gray-lighter", 187 | "@nav-tabs-active-link-hover-bg": "@body-bg", 188 | "@nav-tabs-active-link-hover-color": "@gray", 189 | "@nav-tabs-active-link-hover-border-color": "#ddd", 190 | "@nav-tabs-justified-link-border-color": "#ddd", 191 | "@nav-tabs-justified-active-link-border-color": "@body-bg", 192 | "@nav-pills-border-radius": "@border-radius-base", 193 | "@nav-pills-active-link-hover-bg": "@component-active-bg", 194 | "@nav-pills-active-link-hover-color": "@component-active-color", 195 | "@pagination-color": "@link-color", 196 | "@pagination-bg": "#fff", 197 | "@pagination-border": "#ddd", 198 | "@pagination-hover-color": "@link-hover-color", 199 | "@pagination-hover-bg": "@gray-lighter", 200 | "@pagination-hover-border": "#ddd", 201 | "@pagination-active-color": "#fff", 202 | "@pagination-active-bg": "@brand-primary", 203 | "@pagination-active-border": "@brand-primary", 204 | "@pagination-disabled-color": "@gray-light", 205 | "@pagination-disabled-bg": "#fff", 206 | "@pagination-disabled-border": "#ddd", 207 | "@pager-bg": "@pagination-bg", 208 | "@pager-border": "@pagination-border", 209 | "@pager-border-radius": "15px", 210 | "@pager-hover-bg": "@pagination-hover-bg", 211 | "@pager-active-bg": "@pagination-active-bg", 212 | "@pager-active-color": "@pagination-active-color", 213 | "@pager-disabled-color": "@pagination-disabled-color", 214 | "@jumbotron-padding": "30px", 215 | "@jumbotron-color": "inherit", 216 | "@jumbotron-bg": "@gray-lighter", 217 | "@jumbotron-heading-color": "inherit", 218 | "@jumbotron-font-size": "ceil((@font-size-base * 1.5))", 219 | "@jumbotron-heading-font-size": "ceil((@font-size-base * 4.5))", 220 | "@state-success-text": "#3c763d", 221 | "@state-success-bg": "#dff0d8", 222 | "@state-success-border": "darken(spin(@state-success-bg, -10), 5%)", 223 | "@state-info-text": "#31708f", 224 | "@state-info-bg": "#d9edf7", 225 | "@state-info-border": "darken(spin(@state-info-bg, -10), 7%)", 226 | "@state-warning-text": "#8a6d3b", 227 | "@state-warning-bg": "#fcf8e3", 228 | "@state-warning-border": "darken(spin(@state-warning-bg, -10), 5%)", 229 | "@state-danger-text": "#a94442", 230 | "@state-danger-bg": "#f2dede", 231 | "@state-danger-border": "darken(spin(@state-danger-bg, -10), 5%)", 232 | "@tooltip-max-width": "200px", 233 | "@tooltip-color": "#fff", 234 | "@tooltip-bg": "#000", 235 | "@tooltip-opacity": ".9", 236 | "@tooltip-arrow-width": "5px", 237 | "@tooltip-arrow-color": "@tooltip-bg", 238 | "@popover-bg": "#fff", 239 | "@popover-max-width": "276px", 240 | "@popover-border-color": "rgba(0,0,0,.2)", 241 | "@popover-fallback-border-color": "#ccc", 242 | "@popover-title-bg": "darken(@popover-bg, 3%)", 243 | "@popover-arrow-width": "10px", 244 | "@popover-arrow-color": "@popover-bg", 245 | "@popover-arrow-outer-width": "(@popover-arrow-width + 1)", 246 | "@popover-arrow-outer-color": "fadein(@popover-border-color, 5%)", 247 | "@popover-arrow-outer-fallback-color": "darken(@popover-fallback-border-color, 20%)", 248 | "@label-default-bg": "@gray-light", 249 | "@label-primary-bg": "@brand-primary", 250 | "@label-success-bg": "@brand-success", 251 | "@label-info-bg": "@brand-info", 252 | "@label-warning-bg": "@brand-warning", 253 | "@label-danger-bg": "@brand-danger", 254 | "@label-color": "#fff", 255 | "@label-link-hover-color": "#fff", 256 | "@modal-inner-padding": "15px", 257 | "@modal-title-padding": "15px", 258 | "@modal-title-line-height": "@line-height-base", 259 | "@modal-content-bg": "#fff", 260 | "@modal-content-border-color": "rgba(0,0,0,.2)", 261 | "@modal-content-fallback-border-color": "#999", 262 | "@modal-backdrop-bg": "#000", 263 | "@modal-backdrop-opacity": ".5", 264 | "@modal-header-border-color": "#e5e5e5", 265 | "@modal-footer-border-color": "@modal-header-border-color", 266 | "@modal-lg": "900px", 267 | "@modal-md": "600px", 268 | "@modal-sm": "300px", 269 | "@alert-padding": "15px", 270 | "@alert-border-radius": "@border-radius-base", 271 | "@alert-link-font-weight": "bold", 272 | "@alert-success-bg": "@state-success-bg", 273 | "@alert-success-text": "@state-success-text", 274 | "@alert-success-border": "@state-success-border", 275 | "@alert-info-bg": "@state-info-bg", 276 | "@alert-info-text": "@state-info-text", 277 | "@alert-info-border": "@state-info-border", 278 | "@alert-warning-bg": "@state-warning-bg", 279 | "@alert-warning-text": "@state-warning-text", 280 | "@alert-warning-border": "@state-warning-border", 281 | "@alert-danger-bg": "@state-danger-bg", 282 | "@alert-danger-text": "@state-danger-text", 283 | "@alert-danger-border": "@state-danger-border", 284 | "@progress-bg": "#f5f5f5", 285 | "@progress-bar-color": "#fff", 286 | "@progress-border-radius": "@border-radius-base", 287 | "@progress-bar-bg": "@brand-primary", 288 | "@progress-bar-success-bg": "@brand-success", 289 | "@progress-bar-warning-bg": "@brand-warning", 290 | "@progress-bar-danger-bg": "@brand-danger", 291 | "@progress-bar-info-bg": "@brand-info", 292 | "@list-group-bg": "#fff", 293 | "@list-group-border": "#ddd", 294 | "@list-group-border-radius": "@border-radius-base", 295 | "@list-group-hover-bg": "#f5f5f5", 296 | "@list-group-active-color": "@component-active-color", 297 | "@list-group-active-bg": "@component-active-bg", 298 | "@list-group-active-border": "@list-group-active-bg", 299 | "@list-group-active-text-color": "lighten(@list-group-active-bg, 40%)", 300 | "@list-group-disabled-color": "@gray-light", 301 | "@list-group-disabled-bg": "@gray-lighter", 302 | "@list-group-disabled-text-color": "@list-group-disabled-color", 303 | "@list-group-link-color": "#555", 304 | "@list-group-link-hover-color": "@list-group-link-color", 305 | "@list-group-link-heading-color": "#333", 306 | "@panel-bg": "#fff", 307 | "@panel-body-padding": "15px", 308 | "@panel-heading-padding": "10px 15px", 309 | "@panel-footer-padding": "@panel-heading-padding", 310 | "@panel-border-radius": "@border-radius-base", 311 | "@panel-inner-border": "#ddd", 312 | "@panel-footer-bg": "#f5f5f5", 313 | "@panel-default-text": "@gray-dark", 314 | "@panel-default-border": "#ddd", 315 | "@panel-default-heading-bg": "#f5f5f5", 316 | "@panel-primary-text": "#fff", 317 | "@panel-primary-border": "@brand-primary", 318 | "@panel-primary-heading-bg": "@brand-primary", 319 | "@panel-success-text": "@state-success-text", 320 | "@panel-success-border": "@state-success-border", 321 | "@panel-success-heading-bg": "@state-success-bg", 322 | "@panel-info-text": "@state-info-text", 323 | "@panel-info-border": "@state-info-border", 324 | "@panel-info-heading-bg": "@state-info-bg", 325 | "@panel-warning-text": "@state-warning-text", 326 | "@panel-warning-border": "@state-warning-border", 327 | "@panel-warning-heading-bg": "@state-warning-bg", 328 | "@panel-danger-text": "@state-danger-text", 329 | "@panel-danger-border": "@state-danger-border", 330 | "@panel-danger-heading-bg": "@state-danger-bg", 331 | "@thumbnail-padding": "4px", 332 | "@thumbnail-bg": "@body-bg", 333 | "@thumbnail-border": "#ddd", 334 | "@thumbnail-border-radius": "@border-radius-base", 335 | "@thumbnail-caption-color": "@text-color", 336 | "@thumbnail-caption-padding": "9px", 337 | "@well-bg": "#f5f5f5", 338 | "@well-border": "darken(@well-bg, 7%)", 339 | "@badge-color": "#fff", 340 | "@badge-link-hover-color": "#fff", 341 | "@badge-bg": "@gray-light", 342 | "@badge-active-color": "@link-color", 343 | "@badge-active-bg": "#fff", 344 | "@badge-font-weight": "bold", 345 | "@badge-line-height": "1", 346 | "@badge-border-radius": "10px", 347 | "@breadcrumb-padding-vertical": "8px", 348 | "@breadcrumb-padding-horizontal": "15px", 349 | "@breadcrumb-bg": "#f5f5f5", 350 | "@breadcrumb-color": "#ccc", 351 | "@breadcrumb-active-color": "@gray-light", 352 | "@breadcrumb-separator": "\"/\"", 353 | "@carousel-text-shadow": "0 1px 2px rgba(0,0,0,.6)", 354 | "@carousel-control-color": "#fff", 355 | "@carousel-control-width": "15%", 356 | "@carousel-control-opacity": ".5", 357 | "@carousel-control-font-size": "20px", 358 | "@carousel-indicator-active-bg": "#fff", 359 | "@carousel-indicator-border-color": "#fff", 360 | "@carousel-caption-color": "#fff", 361 | "@close-font-weight": "bold", 362 | "@close-color": "#000", 363 | "@close-text-shadow": "0 1px 0 #fff", 364 | "@code-color": "#c7254e", 365 | "@code-bg": "#f9f2f4", 366 | "@kbd-color": "#fff", 367 | "@kbd-bg": "#333", 368 | "@pre-bg": "#f5f5f5", 369 | "@pre-color": "@gray-dark", 370 | "@pre-border-color": "#ccc", 371 | "@pre-scrollable-max-height": "340px", 372 | "@component-offset-horizontal": "180px", 373 | "@text-muted": "@gray-light", 374 | "@abbr-border-color": "@gray-light", 375 | "@headings-small-color": "@gray-light", 376 | "@blockquote-small-color": "@gray-light", 377 | "@blockquote-font-size": "(@font-size-base * 1.25)", 378 | "@blockquote-border-color": "@gray-lighter", 379 | "@page-header-border-color": "@gray-lighter", 380 | "@dl-horizontal-offset": "@component-offset-horizontal", 381 | "@dl-horizontal-breakpoint": "@grid-float-breakpoint", 382 | "@hr-border": "@gray-lighter" 383 | }, 384 | "css": [ 385 | "type.less", 386 | "code.less", 387 | "grid.less", 388 | "forms.less", 389 | "buttons.less", 390 | "responsive-utilities.less", 391 | "button-groups.less", 392 | "input-groups.less", 393 | "breadcrumbs.less", 394 | "pagination.less", 395 | "pager.less", 396 | "labels.less", 397 | "badges.less", 398 | "thumbnails.less", 399 | "alerts.less", 400 | "progress-bars.less", 401 | "list-group.less", 402 | "panels.less", 403 | "responsive-embed.less", 404 | "wells.less" 405 | ], 406 | "js": [], 407 | "customizerUrl": "http://getbootstrap.com/customize/?id=13c0fafb728135df2b2d85d4fad67afb" 408 | } -------------------------------------------------------------------------------- /fileshare.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | "use_strict"; 3 | 4 | const express = require('express'); 5 | const formidable = require('formidable'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | const os = require('os'); 9 | const qr_image = require("qr-image"); 10 | const crypto = require('crypto'); 11 | const util = require("util"); 12 | const chokidar = require('chokidar'); 13 | 14 | /** 15 | * @callback FileShareProgressCallback 16 | * @param {number} progress 17 | * @param {string} fileName 18 | */ 19 | 20 | /** 21 | * @typedef FileShareConfig 22 | * @property {string} [filesFolderPath] 23 | * @property {string} [publicPath] 24 | * @property {number} [port] 25 | * @property {boolean} [allowDeletion] 26 | * @property {boolean} [multiUpload] 27 | * @property {boolean} [folderUpload] 28 | * @property {FileShareProgressCallback} [progressCallback] 29 | * @property {function} [errorCallback] 30 | * @property {number} [progressThreshold] 31 | * @property {boolean} [orderByTime] 32 | * @property {number} [maxFileSize] 33 | * @property {{"info": boolean, "fileDownload": boolean}} [disable] 34 | */ 35 | 36 | /** @typedef {import('fs').Stats} FileStats */ 37 | 38 | /** 39 | * @typedef FolderContent 40 | * @property {true} folder 41 | * @property {string} name 42 | * @property {string} path 43 | * @property {Array} contents 44 | * @property {number} timestamp 45 | * // TODO(baris): size 46 | */ 47 | /** 48 | * @typedef FileContent 49 | * @property {false} folder 50 | * @property {string} name 51 | * @property {string} path 52 | * @property {number} timestamp 53 | * // TODO(baris): size 54 | */ 55 | /** 56 | * @typedef {FolderContent|FileContent} Content 57 | */ 58 | 59 | /** 60 | * @typedef ServerInfoResult 61 | * @property {Array} addresses 62 | * @property {number} port 63 | * @property {FolderContent} rootContent 64 | * @property {string} rootContentMD5 65 | * @property {boolean} allowDeletion 66 | * @property {boolean} multiUpload 67 | * @property {boolean} folderUpload 68 | */ 69 | 70 | /** 71 | * @param {string} targetPath 72 | * @param {string} basePath 73 | * @param {string} [name=null] 74 | * @returns {Promise} 75 | */ 76 | function recursiveReaddir(targetPath, basePath, name = null, orderByTime = false) { 77 | return readdirPromise(targetPath).then((contentNames) => { 78 | return Promise.all(contentNames.map((fileOrDirName) => { 79 | let fileOrDirPath = path.join(targetPath, fileOrDirName); 80 | return lstatPromise(fileOrDirPath).then( 81 | /** @returns {Content|Promise} */ 82 | (stats) => { 83 | if (stats.isDirectory()) { 84 | return recursiveReaddir(fileOrDirPath, basePath, fileOrDirName); 85 | } else if (stats.isFile()) { 86 | /** @type {FileContent} */ 87 | const fileContent = { 88 | "folder": false, 89 | "name": fileOrDirName, 90 | "path": path.relative(basePath, fileOrDirPath), 91 | "timestamp": Math.max(...[stats.ctimeMs, stats.mtimeMs].filter(x => (x != null))) 92 | }; 93 | return fileContent; 94 | } else { 95 | // NOTE(baris): Only handling file and folders 96 | return null; 97 | } 98 | }); 99 | })); 100 | }).then((contentObjects) => { 101 | if (contentObjects.length == 0 && targetPath != basePath) { // Ignoring empty (sub)folders 102 | return null; 103 | } else { 104 | let timestamp = null; 105 | if (contentObjects.length > 0) { 106 | contentObjects = contentObjects.filter(obj => (obj != null)); 107 | if (orderByTime) { 108 | contentObjects = contentObjects.sort((a, b) => { 109 | if (a.timestamp == null) return 1; 110 | if (b.timestamp == null) return -1; 111 | return b.timestamp - a.timestamp; 112 | }); 113 | timestamp = contentObjects[0].timestamp; 114 | } else { 115 | timestamp = contentObjects.map((x) => x.timestamp).filter(x => (x != null)).sort((a, b) => b - a)[0]; 116 | } 117 | } 118 | return { 119 | "folder": true, 120 | "name": name, 121 | "path": path.relative(basePath, targetPath), 122 | "contents": contentObjects, 123 | "timestamp": timestamp 124 | } 125 | } 126 | }) 127 | } 128 | /** 129 | * @param {string} targetPath 130 | * @returns {Promise>} 131 | */ 132 | function readdirPromise(targetPath) { 133 | return new Promise((resolve) => { 134 | fs.readdir(targetPath, (err, contentNames) => { 135 | if (contentNames == null) { 136 | resolve([]); 137 | } else { 138 | // NOTE(baris): Ignoring hidden files all together. 139 | resolve(contentNames.filter((contentName) => contentName[0] != '.')); 140 | } 141 | }) 142 | }); 143 | } 144 | /** 145 | * @param {string} targetPath 146 | * @returns {Promise} 147 | */ 148 | function lstatPromise(targetPath) { 149 | return new Promise((resolve) => { 150 | fs.lstat(targetPath, (err, stats) => { 151 | resolve(stats); 152 | }); 153 | }); 154 | } 155 | 156 | // 157 | 158 | /** 159 | * @param {string} msg 160 | * @param {...any} args 161 | */ 162 | const logDebug = ( 163 | (process.env.NODE_ENV === "debug") ? 164 | (msg, ...args) => {console.log(msg, ...args.map(arg => util.inspect(arg, false, 10, true)))} : 165 | (msg, ...args) => {} 166 | ) 167 | 168 | 169 | /** 170 | * @typedef LiveCacheFolderContent 171 | * @property {true} folder 172 | * @property {string} path 173 | * @property {Object} contents 174 | * @property {number} timestamp 175 | * // TODO(baris): Sum size of files inside for size? 176 | */ 177 | /** 178 | * @typedef LiveCacheFileContent 179 | * @property {false} folder 180 | * @property {string} path 181 | * @property {number} timestamp 182 | * @property {number} size 183 | */ 184 | /** 185 | * @typedef {LiveCacheFolderContent|LiveCacheFileContent} LiveCacheContent 186 | */ 187 | 188 | class LiveCache { 189 | constructor(filesFolderPath, orderByTime=true) { 190 | 191 | this.orderByTime = orderByTime; 192 | 193 | /** @type {LiveCacheFolderContent} */ 194 | const rootContent = { 195 | "folder": true, 196 | "path": "", 197 | "contents": {}, 198 | "timestamp": null 199 | }; 200 | this.rootContent = rootContent; 201 | 202 | const watcher = chokidar.watch(filesFolderPath, { 203 | cwd: filesFolderPath, 204 | ignored: /(^|[\/\\])\../, // ignore dotfiles 205 | persistent: true, 206 | alwaysStat: true, 207 | }); 208 | this.watcher = watcher; 209 | 210 | watcher.on('error', error => { 211 | logDebug(`LiveCache: error`, error) 212 | // TODO(baris): Add error handling. 213 | }) 214 | 215 | this.contentPrepPromise = new Promise((resolve) => { 216 | watcher.on('ready', () => { 217 | logDebug('Initial scan complete. Ready for changes') 218 | resolve(); 219 | }); 220 | }) 221 | 222 | this._contentOutputJSON = null; 223 | this.contentOutputMD5 = null; 224 | 225 | /** 226 | * @param {string} pathStr 227 | * @returns {Array} 228 | */ 229 | function splitPath(pathStr) { 230 | if(pathStr == '') { 231 | return []; 232 | } else { 233 | return pathStr.split(path.sep); 234 | } 235 | } 236 | 237 | /** 238 | * @param {string} pathStr 239 | * @param {fs.Stats} stats 240 | */ 241 | function addFolderToCache(pathStr, stats) { 242 | if(pathStr == "") { 243 | rootContent["timestamp"] = Math.max(...[stats.ctimeMs, stats.mtimeMs].filter(x => (x != null))) 244 | } else { 245 | const pathParts = splitPath(pathStr); 246 | const partCount = pathParts.length; 247 | 248 | let currentDir = rootContent; 249 | for (let index = 0; index <= (partCount-2); index++) { 250 | // @ts-ignore 251 | currentDir = currentDir.contents[pathParts[index]]; 252 | } 253 | currentDir.contents[pathParts[partCount-1]] = { 254 | "folder": true, 255 | "path": pathStr, 256 | "contents": {}, 257 | // TODO(baris): Double check if valid for folders 258 | "timestamp": Math.max(...[stats.ctimeMs, stats.mtimeMs].filter(x => (x != null))) 259 | }; 260 | } 261 | } 262 | /** 263 | * @param {string} pathStr 264 | * @param {fs.Stats} stats 265 | * NOTE(baris): Overwrites if in same path. 266 | */ 267 | function addFileToCache(pathStr, stats) { 268 | if(pathStr == "") { 269 | console.error("addFileToCache root cannot be a file"); 270 | return; 271 | } 272 | const pathParts = splitPath(pathStr); 273 | const partCount = pathParts.length; 274 | 275 | let currentDir = rootContent; 276 | for (let index = 0; index <= (partCount-2); index++) { 277 | // @ts-ignore 278 | currentDir = currentDir.contents[pathParts[index]]; 279 | } 280 | currentDir.contents[pathParts[partCount-1]] = { 281 | "folder": false, 282 | "path": pathStr, 283 | "timestamp": Math.max(...[stats.ctimeMs, stats.mtimeMs].filter(x => (x != null))), 284 | "size": stats.size 285 | } 286 | 287 | } 288 | 289 | /** 290 | * @param {string} pathStr 291 | */ 292 | function removeFromCache(pathStr) { 293 | if(pathStr == "") { 294 | console.error("removeFromCache root cannot be removed"); 295 | return; 296 | } 297 | const pathParts = splitPath(pathStr); 298 | const partCount = pathParts.length; 299 | 300 | let currentDir = rootContent; 301 | for (let index = 0; index <= (partCount-2); index++) { 302 | // @ts-ignore 303 | currentDir = currentDir.contents[pathParts[index]]; 304 | } 305 | delete currentDir.contents[pathParts[partCount-1]]; 306 | } 307 | 308 | watcher 309 | .on('add', (path, stats) => { 310 | logDebug(`LiveCache: File has been added`, path, stats); 311 | this._invalidateOutputCache(); 312 | addFileToCache(path, stats); 313 | logDebug(`LiveCache: rootContent`, rootContent); 314 | }) 315 | .on('change', (path, stats) => { 316 | logDebug(`LiveCache: File has been changed`, path, stats); 317 | this._invalidateOutputCache(); 318 | addFileToCache(path, stats); 319 | logDebug(`LiveCache: rootContent`, rootContent); 320 | }) 321 | .on('unlink', path => { 322 | logDebug(`LiveCache: File has been removed`, path); 323 | this._invalidateOutputCache(); 324 | removeFromCache(path); 325 | logDebug(`LiveCache: rootContent`, rootContent); 326 | }) 327 | .on('addDir', (path, stats) => { 328 | logDebug(`LiveCache: Directory has been added`, path, stats); 329 | this._invalidateOutputCache(); 330 | addFolderToCache(path, stats); 331 | logDebug(`LiveCache: rootContent`, rootContent); 332 | }) 333 | .on('unlinkDir', path => { 334 | logDebug(`LiveCache: Directory has been removed`, path); 335 | this._invalidateOutputCache(); 336 | removeFromCache(path); 337 | logDebug(`LiveCache: rootContent`, rootContent); 338 | }) 339 | .on('error', error => { 340 | console.error("LiveCache error:", error); 341 | }) 342 | 343 | } 344 | 345 | /** 346 | * @param {string} baseFolderName 347 | * @param {LiveCacheFolderContent} baseFolderContent 348 | * @returns {FolderContent} 349 | */ 350 | _getContentRecursive(baseFolderName, baseFolderContent) { 351 | 352 | const contentNames = Object.keys(baseFolderContent.contents); 353 | 354 | if(contentNames.length == 0) return null; 355 | 356 | let contents = contentNames.map((contentName) => { 357 | const content = baseFolderContent.contents[contentName]; 358 | if (content.folder) { 359 | return this._getContentRecursive(contentName, content); 360 | } else { 361 | /** @type {FileContent} */ 362 | const fileContent = { 363 | "folder": false, 364 | "name": contentName, 365 | "path": content.path, 366 | "timestamp": content.timestamp 367 | } 368 | return fileContent; 369 | } 370 | }).filter(x => x != null); 371 | 372 | if (this.orderByTime) { 373 | contents = contents.sort((a, b) => { 374 | if (a.timestamp == null) return 1; 375 | if (b.timestamp == null) return -1; 376 | return b.timestamp - a.timestamp; 377 | }); 378 | } 379 | 380 | return { 381 | "folder": true, 382 | "name": baseFolderName, 383 | "path": baseFolderContent.path, 384 | "contents": contents, 385 | // TODO(baris): Calculate timestamp from contents? 386 | "timestamp": baseFolderContent.timestamp, 387 | } 388 | } 389 | 390 | _invalidateOutputCache() { 391 | this._contentOutputJSON = null; 392 | this.contentOutputMD5 = null; 393 | } 394 | 395 | /** 396 | * Returns output content. 397 | * @returns {Promise<[FolderContent, string]>} -- [root folder content, md5] 398 | */ 399 | prepContentOutput() { 400 | return this.contentPrepPromise.then(() => { 401 | // NOTE(baris): In class instance cache hit, independent of browser state. 402 | if(this.contentOutputMD5 == null) { 403 | this._contentOutputJSON = this._getContentRecursive(null, this.rootContent); 404 | this.contentOutputMD5 = crypto.createHash('md5').update(JSON.stringify(this._contentOutputJSON)).digest("hex") 405 | } 406 | return [this._contentOutputJSON, this.contentOutputMD5]; 407 | }); 408 | } 409 | 410 | // TODO(baris): attachUpdateListener 411 | // TODO(baris): detachUpdateListener 412 | 413 | destroy() { 414 | this.watcher.close().then(() => { 415 | logDebug("LiveCache destroy") 416 | }) 417 | } 418 | } 419 | 420 | // 421 | 422 | function normalizePort(val) { 423 | const port = parseInt(val, 10); 424 | 425 | if (isNaN(port)) { 426 | // named pipe 427 | return val; 428 | } 429 | 430 | if (port >= 0) { 431 | // port number 432 | return port; 433 | } 434 | 435 | return false; 436 | } 437 | 438 | function getAddresses() { 439 | let interfaces = os.networkInterfaces(); 440 | let addresses = []; 441 | for (const k in interfaces) { 442 | for (const k2 in interfaces[k]) { 443 | let address = interfaces[k][k2]; 444 | // NOTE: Only handling IPv4 at the moment. 445 | if (address.family === 'IPv4' && !address.internal) { 446 | addresses.push(address.address); 447 | } 448 | } 449 | } 450 | return addresses; 451 | } 452 | 453 | function generateQRCodeIfNotExists(imagePath, address, port) { 454 | return new Promise((resolve, reject) => { 455 | fs.exists(imagePath, function (exists) { 456 | if (exists) { 457 | resolve(); 458 | } else { 459 | let qr_svg = qr_image.image(`http://${address}:${port}/`, { type: 'png' }); 460 | qr_svg.pipe(fs.createWriteStream(imagePath)) 461 | .on("finish", () => { resolve(); }) 462 | .on("error", () => { reject(); }); 463 | } 464 | }) 465 | }) 466 | } 467 | 468 | function getAddressesWQRCodes(publicPath, port) { 469 | const addresses = getAddresses(); 470 | return Promise.all(addresses.map((address) => { 471 | const imagePath = path.join(publicPath, `./qr_codes/${address}_${port}.png`); 472 | return generateQRCodeIfNotExists(imagePath, address, port).catch(() => { 473 | // NOTE(baris): Ignoring errors here. 474 | }); 475 | })).then(() => addresses); 476 | } 477 | 478 | /** 479 | * @param {FileShareConfig} conf 480 | */ 481 | module.exports = function (conf) { 482 | 483 | //Getting config from conf. 484 | let filesFolderPath = conf.filesFolderPath || path.join(__dirname, 'files'), 485 | publicPath = conf.publicPath || path.join(__dirname, 'public'), 486 | port = normalizePort(conf.port || '8080'), 487 | allowDeletion = conf.allowDeletion === true, 488 | multiUpload = conf.multiUpload === true, 489 | folderUpload = conf.folderUpload === true, 490 | progressCallback = conf.progressCallback, 491 | errorCallback = conf.errorCallback, 492 | progressThreshold = conf.progressThreshold || 10, 493 | orderByTime = conf.orderByTime || true, 494 | maxFileSize = conf.maxFileSize || (100*1024*1024*1024), // 100GB 495 | disable = conf.disable || {"info": false, "fileDownload": false}; 496 | 497 | const vueDistPath = path.join(__dirname, "./node_modules/vue/dist"); 498 | 499 | let qrCodesPath = path.join(publicPath, "./qr_codes/"); 500 | if (!fs.existsSync(qrCodesPath)) { 501 | fs.mkdirSync(qrCodesPath); 502 | } 503 | 504 | const liveCache = new LiveCache(filesFolderPath, orderByTime); 505 | 506 | //New express app 507 | const app = express(); 508 | 509 | //For index. Basically app.get('/',...); 510 | app.use(express.static(publicPath)); 511 | 512 | //For vue.js 513 | app.use('/vue', express.static(vueDistPath)); 514 | 515 | //For downloading files 516 | if (!disable.fileDownload) app.use('/f', express.static(filesFolderPath)); 517 | 518 | app.get('/delete/:filename', function (req, res) { 519 | 520 | res.setHeader('Access-Control-Allow-Origin', '*'); 521 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // Just in case 522 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,contenttype'); // Just in case 523 | res.setHeader('Access-Control-Allow-Credentials', "true"); // Just in case 524 | 525 | if (allowDeletion) { 526 | const filename = decodeURIComponent(req.params.filename); 527 | const filesFolderFullPath = path.resolve(filesFolderPath); 528 | const fileFullPath = path.join(filesFolderFullPath, filename); 529 | logDebug("fileFullPath", fileFullPath); 530 | if( 531 | fileFullPath != filesFolderFullPath && 532 | fileFullPath.startsWith(filesFolderFullPath) 533 | ) { 534 | try { 535 | fs.unlinkSync(fileFullPath) 536 | res.sendStatus(200); 537 | } catch (error) { 538 | error.status = 404; 539 | console.error("fs.unlinkSync error", error); 540 | res.send(); 541 | } 542 | } else { 543 | res.statusCode = 500; 544 | res.send("Invalid filename"); 545 | } 546 | } else { 547 | res.sendStatus(500); 548 | } 549 | 550 | }); 551 | 552 | app.post('/', function (req, res) { 553 | 554 | // Bug fix for when the filesFolderPath folder does not exists 555 | if (!!conf.filesFolderPath) { 556 | // For a path that can have multiple non existent folders. 557 | // Borrowed from: https://stackoverflow.com/a/41970204 558 | filesFolderPath.split(path.sep).reduce((currentPath, folder) => { 559 | currentPath += folder + path.sep; 560 | if (!fs.existsSync(currentPath)) { 561 | fs.mkdirSync(currentPath); 562 | } 563 | return currentPath; 564 | }, ''); 565 | } else { 566 | // For the simple './files' path. 567 | if (!fs.existsSync(filesFolderPath)) { 568 | fs.mkdirSync(filesFolderPath); 569 | } 570 | } 571 | 572 | const form = new formidable.IncomingForm(); 573 | 574 | form.uploadDir = filesFolderPath; 575 | 576 | form.maxFields = 10000; 577 | form.multiples = true; 578 | form.maxFileSize = maxFileSize; 579 | 580 | let progress = 0; 581 | form.on('progress', function (bytesReceived, bytesExpected) { 582 | const temp = bytesReceived * 100 / bytesExpected; 583 | if (temp > progress + progressThreshold) { 584 | progress = Math.floor(temp); 585 | if (progressCallback) progressCallback(progress, null); 586 | } 587 | }); 588 | 589 | const foldersCreated = new Map(); // given folder => duplicate handled folder 590 | form.on('fileBegin', function (webkitRelativePath, file) { 591 | 592 | logDebug("fileBegin", webkitRelativePath, file); 593 | 594 | let {dir:parsedPathDir, name:parsedPathName, ext:parsedPathExt} = path.parse(webkitRelativePath); 595 | if(parsedPathDir != "") { 596 | // Borrowed from: https://stackoverflow.com/a/41970204 597 | parsedPathDir = parsedPathDir.split(path.sep).reduce((currentPath, folder, index) => { 598 | let combinedPath = [currentPath, folder, path.sep].join(''); 599 | if(index == 0) { 600 | if(foldersCreated.has(folder)) { 601 | combinedPath = [currentPath, foldersCreated.get(folder), path.sep].join(''); 602 | } else { 603 | let i = 0; 604 | let handledFolder = folder; 605 | while(fs.existsSync(path.join(filesFolderPath, combinedPath))) { 606 | handledFolder = [folder, " dup", (i++)].join(''); 607 | combinedPath = [currentPath, handledFolder, path.sep].join(''); 608 | } 609 | foldersCreated.set(folder, handledFolder); 610 | fs.mkdirSync(path.join(filesFolderPath, combinedPath)); 611 | } 612 | } else { 613 | if (!fs.existsSync(path.join(filesFolderPath, combinedPath))) { 614 | logDebug("combinedPath", combinedPath); 615 | fs.mkdirSync(path.join(filesFolderPath, combinedPath)); 616 | } 617 | } 618 | return combinedPath; 619 | }, ''); 620 | } 621 | 622 | let fileName = parsedPathName + parsedPathExt; 623 | let filePath = path.join(filesFolderPath, parsedPathDir, fileName); 624 | 625 | //For not overwriting files. 626 | let i = 0; 627 | while (fs.existsSync(filePath)) { 628 | fileName = [parsedPathName, " dup", (i++), ".", parsedPathExt].join(''); 629 | filePath = path.join(filesFolderPath, parsedPathDir, fileName); 630 | } 631 | 632 | file.path = filePath; 633 | 634 | }); 635 | 636 | form.on('file', function (name, file) { 637 | logDebug("file done", name, file); 638 | if (progressCallback) progressCallback(null, file.name); 639 | }); 640 | 641 | form.parse(req, (error, fields, files) => { 642 | if(error != null) { 643 | console.error("form error", error); 644 | res.sendStatus(400); 645 | } else { 646 | // logDebug("files", files); 647 | logDebug("file uploads done"); 648 | res.sendStatus(200); 649 | } 650 | }); 651 | 652 | }); 653 | 654 | app.get('/info', function (req, res) { 655 | 656 | if (disable.info) { 657 | res.sendStatus(404); 658 | return; 659 | } 660 | 661 | res.setHeader('Access-Control-Allow-Origin', '*'); 662 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // Just in case 663 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,contenttype'); // Just in case 664 | res.setHeader('Access-Control-Allow-Credentials', "true"); // Just in case 665 | 666 | const addressesPromise = getAddressesWQRCodes(publicPath, port); 667 | 668 | /** @type {Promise<[FolderContent, string]>} */ 669 | let rootContentPromise; 670 | if(disable.fileDownload) { 671 | rootContentPromise = Promise.resolve([null, null]); 672 | } else if(req.query.md5 != null && liveCache.contentOutputMD5 === req.query.md5) { 673 | // NOTE(baris): In browser cache hit case. Browser will not update. 674 | rootContentPromise = Promise.resolve([null, liveCache.contentOutputMD5]); 675 | } else { 676 | rootContentPromise = liveCache.prepContentOutput(); 677 | } 678 | 679 | Promise.all([ 680 | addressesPromise, 681 | rootContentPromise 682 | ]).then(([addresses, [rootContent, rootContentMD5]]) => { 683 | 684 | /** @type {ServerInfoResult} */ 685 | const info = { 686 | "addresses": addresses, 687 | "port": port, 688 | "allowDeletion": allowDeletion, 689 | "multiUpload": multiUpload, 690 | "folderUpload": folderUpload, 691 | "rootContent": rootContent, 692 | "rootContentMD5": rootContentMD5 693 | }; 694 | 695 | res.json(info); 696 | }) 697 | 698 | }); 699 | 700 | // catch 404 701 | app.use(function (req, res, next) { 702 | if (errorCallback) { // NOTE(baris): Preserved for backwards compatibility. 703 | var err = new Error('Not Found'); 704 | // @ts-ignore 705 | err.status = 404; 706 | // development error handler 707 | errorCallback(req.url, err); 708 | } 709 | res.sendStatus(404); 710 | }); 711 | 712 | app.use(function (err, req, res, next) { 713 | // development error handler 714 | if (errorCallback) errorCallback(req.url, err); 715 | res.sendStatus(500); 716 | }); 717 | 718 | app.set('port', port); 719 | 720 | return { 721 | "addresses": getAddresses(), 722 | "app": app, 723 | "disable": disable, // For manual debugging. 724 | "port": port 725 | }; 726 | 727 | }; 728 | -------------------------------------------------------------------------------- /public/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2017 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=13c0fafb728135df2b2d85d4fad67afb) 9 | * Config saved to config.json and https://gist.github.com/13c0fafb728135df2b2d85d4fad67afb 10 | *//*! 11 | * Bootstrap v3.3.7 (http://getbootstrap.com) 12 | * Copyright 2011-2016 Twitter, Inc. 13 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 14 | *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover,a.text-primary:focus{color:#286090}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#31708f}a.text-info:hover,a.text-info:focus{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover,a.text-warning:focus{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover,a.bg-primary:focus{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s, box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#337ab7;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#fff;background-color:#337ab7;border-color:#337ab7;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#fff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-left:15px;padding-right:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.pager:after,.panel-body:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}} --------------------------------------------------------------------------------