├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── INSTALL.md ├── LICENSE ├── README.md ├── config.default.hjson ├── index.js ├── micro ├── Router.js ├── SingleInstance.js ├── apps │ ├── getItem.js │ └── misc.js ├── helpers.js ├── routes │ ├── admin.js │ ├── deleteItem.js │ ├── getItem.js │ ├── getItemInfo.js │ ├── getThumb.js │ ├── getUploads.js │ ├── index.js │ ├── postUpload.js │ └── postUrl.js └── services │ ├── admin.js │ ├── getItem.js │ ├── shortenUrl.js │ └── uploadFile.js ├── models ├── Collection.js ├── Item.js ├── Short.js ├── Stat.js ├── Store.js └── User.js ├── modules ├── Archive.js ├── ClamAV.js ├── Collection.js ├── Item.js ├── Mail.js ├── Minio.js ├── MinioMulterStorage.js ├── Profiling.js ├── ShareX.js ├── Short.js ├── Stats.js ├── Store.js ├── User.js ├── Utils.js └── VirusTotal.js ├── package-lock.json ├── package.json ├── public ├── css │ └── main.css ├── favicons │ ├── android-chrome-192x192.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── images │ ├── logo.png │ ├── new_screenshot.png │ ├── screenshot.png │ └── unknown_file.png └── js │ ├── stats.bundle.js │ └── uploader.bundle.js ├── scripts ├── findLargeFiles.js ├── findUnusedFiles.js ├── fixCanonical.js ├── fixSizes.js ├── identifyFileFromBuffer.js ├── identifyFilesWithoutUser.js ├── identifyUsersFiles.js ├── migrateData.js ├── runAV.js ├── sendAnalytics.js └── tunnelDataMigration.js ├── src ├── css │ └── uploader.css ├── stats.js └── uploader.js ├── types ├── Base.js ├── Binary.js ├── Code.js ├── Image.js ├── Text.js ├── Url.js └── index.js ├── views ├── components │ ├── base.pug │ └── nav.pug ├── home.pug ├── info.pug ├── issues.pug ├── privacy.pug ├── stats.pug ├── terms.pug └── uploads.pug └── webpack.config.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ $default-branch ] 9 | pull_request: 10 | branches: [ $default-branch ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: docker run -p 27017:27017 --name mongo -d mongo 29 | - run: docker run -p 6379:6379 --name redis -d redis redis-server --appendonly yes 30 | - run: npm run build --if-present 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | 82 | # Ignore config file 83 | config.hjson 84 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Step by step installation instructions 2 | 3 | ## Requirements 4 | 5 | 1. Prerequisites 6 | 7 | 1. [MongoDB](https://www.mongodb.com/). 8 | 2. [Redis](https://redis.io/). 9 | 3. [MinIO](https://min.io/). 10 | 4. ClamAV (Optional) 11 | 5. [Node.js](https://nodejs.org/) v12 or later. 12 | 6. `carbon-now-cli` npm package 13 | 7. Package-Config, Pixman Dev Library, Cairo Dev Libirary, PangoCairo Dev Lbrary, Jpeg Dev Library 14 | 8. [Web Authentication Provider](https://github.com/femto-apps/web-authentication-provider) 15 | 9. [Web Authentication Token Service](https://github.com/femto-apps/web-authentication-token-service) 16 | 17 | ### Explanatory Notes. 18 | 19 | 1. Pulibc URL of Web-File-Uploader in this document is `www.example.com` 20 | 2. Public URL of Web Authentication Provider in this document is `auth.example.com` 21 | 3. Public URL of Web Authentication Token Service in this document is `token.example.com` 22 | 23 | ## Prepare MongoDB 24 | 25 | 1. If you want to run MongoDB locally, you can use [the official binary repository](https://docs.mongodb.com/manual/installation/) or docker. 26 | 27 | ``` 28 | docker volume create --name=mongodata 29 | docker run -p 27017:27017 --name mongo -v mongodata:/data/db -d mongo 30 | ``` 31 | 2. Or you can also use [MongoDB Atlas](https://www.mongodb.com/cloud/atlas?tck=docs_server) 32 | 33 | 34 | ## Prepare Redis 35 | 36 | You can use your distribution package or docker. 37 | 38 | ``` 39 | docker volume create --name=redisdata 40 | docker run -p 6379:6379 --name redis -v redisdata:/data -d redis redis-server --appendonly yes 41 | ``` 42 | 43 | 44 | ## Prepare MinIO 45 | 46 | You can use [the official binary](https://min.io/download) or docker. 47 | 48 | ``` 49 | docker volume create --name=miniodata 50 | docker run -p 9000:9000 -e MINIO_ACCESS_KEY=CHANGE_ME_ACCESS_KEY -e MINIO_SECRET_KEY=CHANGE_ME_SECRET_KEY --name minio -v miniodata:/data -d minio/minio server /data 51 | ``` 52 | 53 | ## Prepare Node.js and npm 54 | 55 | Refer to [the official page](https://nodejs.org/en/download/package-manager/). 56 | 57 | ## Prepare ClamAV (Optional) 58 | 59 | If you want to scan any uploaded files, you can use ClamAV of your distribution package or docker. 60 | 61 | ``` 62 | # the port is set to 9001 because 9000 is used by minio by default 63 | docker run -p 9001:9000 --name clamav -d niilo/clamav-rest 64 | # this takes a while to start up, monitor progress with `docker logs clamav` 65 | ``` 66 | 67 | ## Install Authentication Provider 68 | 69 | 1. Clone the repository of Authentication Provider. 70 | ``` 71 | git clone https://github.com/femto-apps/web-authentication-provider.git provider 72 | ``` 73 | 2. `cd provider` 74 | 3. Copy `config.default.js` to `config.js` 75 | 4. Edit `config.js`. Change the Mongo URI to point to your MongoDB server, and replace all secrets with random strings. 76 | 77 | - Example 78 | ```js 79 | module.exports = { 80 | port: 3001, 81 | mongo: { 82 | uri: 'mongodb://user:password@localhost:27017/', 83 | db: 'authenticationProvider' 84 | }, 85 | redis: { 86 | // url: 'redis://127.0.0.1:6379/0', 87 | host: '127.0.0.1', 88 | port: 6379, 89 | db: 0, 90 | session: 'sessions' 91 | }, 92 | cookie: { 93 | maxAge: 1000 * 60 * 60 * 24 * 7 * 4, // 28 days 94 | secret: 'REPLACE THIS WITH A RANDOM STRING', 95 | name: 'provider' 96 | }, 97 | session: { 98 | secret: 'REPLACE THIS WITH A RANDOM STRING' 99 | }, 100 | title: { 101 | suffix: 'Femto Authentication Provider' // You can replace this with your favorite title 102 | }, 103 | favicon: 'public/images/favicon/favicon.ico', 104 | } 105 | ``` 106 | 107 | 5. Run `npm install` 108 | 109 | 6. Run `node index.js` 110 | 111 | When your system uses systemd, refer the following sample. 112 | 113 | ```systemd 114 | [Unit] 115 | Description=Web Authentication Provider 116 | After=network.target 117 | Before=apache2.service 118 | 119 | [Service] 120 | User=user 121 | WorkingDirectory=/path/to/provider 122 | Environment=NODE_ENV=production 123 | ExecStart=/usr/bin/node index.js 124 | Restart=always 125 | 126 | [Install] 127 | WantedBy=multi-user.target 128 | ``` 129 | 130 | 7. Configure your web server. 131 | 132 | - Example for Apache 133 | ```apache 134 | 135 | 136 | 137 | ServerName auth.example.com 138 | ServerAdmin webmaster@example.com 139 | 140 | SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem 141 | SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem 142 | Include /etc/letsencrypt/options-ssl-apache.conf 143 | 144 | RequestHeader set X-Forwarded-Proto "https" 145 | ProxyPreserveHost On 146 | 147 | ProxyPass / http://127.0.0.1:3001/ 148 | ProxyPassReverse / http://127.0.0.1:3001/ 149 | 150 | ErrorLog ${APACHE_LOG_DIR}/error.log 151 | CustomLog ${APACHE_LOG_DIR}/access.log combined 152 | 153 | 154 | 155 | ``` 156 | 157 | - Example for Caddy 158 | ```caddy 159 | auth.example.com { 160 | proxy / localhost:3001 161 | } 162 | ``` 163 | 8. Access `https://auth.example.com/consumer/add` via your browser. 164 | 9. Enter `Name`, `Description` and `Redirects` but `Redirects` is as below: 165 | ``` 166 | https://www.example.com/login_callback 167 | ``` 168 | 10. If you got `...does not appear to be a URL` message, please refer https://github.com/femto-apps/web-authentication-provider/issues/8 169 | 11. When submitting above, you'll get authentication informations as JSON format. You **MUST** pick up `uuid` as `ConsumerId` 170 | 171 | ## Install Web Authentication Token Service 172 | 1. clone the repository of Web Authntication Token Service 173 | ``` 174 | git clone https://github.com/femto-apps/web-authentication-token-service.git token 175 | ``` 176 | 2. `cd token` 177 | 3. Copy `config.default.js` to `config.js` 178 | 4. Edit `config.js` if needed 179 | 180 | - Example 181 | ```js 182 | module.exports = { 183 | port: 4500, 184 | redis: { 185 | // url: 'redis://127.0.0.1:6379/0', 186 | host: '127.0.0.1', 187 | port: 6379, 188 | db: 0, 189 | session: 'sessions' 190 | } 191 | } 192 | ``` 193 | 194 | 5. Run `npm install` 195 | 6. Run `node index.js` 196 | When your system uses systemd, refer the following sample. 197 | 198 | ```systemd 199 | [Unit] 200 | Description=Authentication Token Service 201 | After=network.target 202 | Before=apache2.service 203 | 204 | [Service] 205 | User=user 206 | WorkingDirectory=/path/to/token 207 | Environment=NODE_ENV=production 208 | ExecStart=/usr/bin/node index.js 209 | Restart=always 210 | 211 | [Install] 212 | WantedBy=multi-user.target 213 | ``` 214 | 7. Configure your web server 215 | - Example for Apache 216 | ```apache 217 | 218 | 219 | 220 | ServerName token.example.com 221 | ServerAdmin webmaster@example.com 222 | 223 | SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem 224 | SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem 225 | Include /etc/letsencrypt/options-ssl-apache.conf 226 | 227 | RequestHeader set X-Forwarded-Proto "https" 228 | ProxyPreserveHost On 229 | 230 | ProxyPass / http://127.0.0.1:4500/ 231 | ProxyPassReverse / http://127.0.0.1:4500/ 232 | 233 | ErrorLog ${APACHE_LOG_DIR}/error.log 234 | CustomLog ${APACHE_LOG_DIR}/access.log combined 235 | 236 | 237 | 238 | ``` 239 | - Example for Caddy 240 | ```caddy 241 | token.example.com { 242 | proxy / localhost:4500 243 | } 244 | ``` 245 | 246 | ## Install Web-File-Uploader 247 | 1. Install `carbon-now-cli`. 248 | 249 | e.g. `yarn global add carbon-now-cli` or `npm install -g carbon-now-cli` 250 | 251 | 2. Install Package-Config, Pixman Dev Library, Cairo Dev Libirary, PangoCairo Dev Lbrary, Jpeg Dev Library. 252 | 253 | e.g. `apt install pkgconf libpixman-1-dev libpixman-1-dev libcairo2-dev librust-pangocairo-dev libjpeg-dev` 254 | 255 | 3. Clone the repository 256 | ``` 257 | git clone https://github.com/femto-apps/web-file-uploader.git uploader 258 | ``` 259 | 4. `cd uploader` 260 | 5. Copy `config.default.hjson` to `config.hjson` 261 | 6. Edit `config.hjson`. Specially you must change `minio.accessKey`, `minio.secretKey`, `session.secret` and `authenticationProvider.consumerId` and each `endpoint`. 262 | 263 | For example: 264 | ```hjson 265 | { 266 | port: 3005 267 | dev: false 268 | trustedProxy: ['127.0.0.1/8', '::1/128'] 269 | title: { 270 | name: Femto Uploader // You can replae this with your favorite name 271 | shortener: Femto Shortener // You can replae this with your favorite name 272 | suffix: Femto Uploader // You can replae this with your favorite name 273 | } 274 | url: { 275 | origin: https://www.example.com/ 276 | } 277 | carbon: { 278 | // you can find this installation directory with `which carbon-now` 279 | path: /path/to/.yarn/bin/carbon-now 280 | } 281 | mongo: { 282 | uri: mongodb://user:password@localhost:27017/ 283 | db: fileUploader 284 | } 285 | minio: { 286 | host: 127.0.0.1 287 | port: 9000 288 | itemBucket: items // the bucket you created on Minio 289 | accessKey: ACCESSKEY 290 | secretKey: SECRETKEY 291 | useSSL: false 292 | } 293 | clamav: { 294 | url: http://localhost:9001 295 | } 296 | session: { 297 | secret: REPLACE THIS WITH A RAMDOM STRING 298 | } 299 | redis: { 300 | host: 128.0.0.1 301 | port: 6379 302 | db: 0 303 | // url: redis://127.0.0.1:6379/0 304 | } 305 | cookie: { 306 | name: file-uploader 307 | maxAge: 15552000000 // 1000 * 60 * 60 * 24 * 180 (6 months) 308 | } 309 | email: { 310 | name: 'example.com' 311 | host: 'smtp.gmail.com' 312 | secure: false 313 | port: 587 314 | auth: { 315 | user: 'username' 316 | pass: 'password' 317 | } 318 | authMethod: 'PLAIN' 319 | ignoreTLS: false 320 | } 321 | 322 | tokenService: { 323 | endpoint: https://token.example.com 324 | } 325 | authenticationProvider: { 326 | endpoint: https://auth.example.com 327 | consumerId: REPLACE THIS WITH YOUR CONSUMER ID // You got this in the installation step 10 for Web Authentication Provider 328 | } 329 | authenticationConsumer: { 330 | endpoint: https://www.example.com 331 | } 332 | experimental: { 333 | profiling: false 334 | statsTimer: 86400000 // 1000 * 60 * 60 * 24 (1 day) 335 | } 336 | } 337 | ``` 338 | 7. Run `npm install` 339 | 8. Run `node index.js` 340 | When your system uses systemd, refer the following sample. 341 | 342 | ```systemd 343 | [Unit] 344 | Description=Web-File-Uploader 345 | After=network.target 346 | Before=apache2.service 347 | 348 | [Service] 349 | User=user 350 | WorkingDirectory=/path/to/uploader 351 | Environment=NODE_ENV=production 352 | ExecStart=/usr/bin/node index.js 353 | Restart=always 354 | 355 | [Install] 356 | WantedBy=multi-user.target 357 | ``` 358 | 9. Configure your web server 359 | - Example for Apache 360 | 361 | ```apache 362 | 363 | 364 | 365 | ServerName www.example.com 366 | ServerAdmin webmaster@example.com 367 | 368 | SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem 369 | SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem 370 | Include /etc/letsencrypt/options-ssl-apache.conf 371 | 372 | RequestHeader set X-Forwarded-Proto "https" 373 | ProxyPreserveHost On 374 | 375 | ProxyPass / http://127.0.0.1:3005/ 376 | ProxyPassReverse / http://127.0.0.1:3005/ 377 | 378 | ErrorLog ${APACHE_LOG_DIR}/error.log 379 | CustomLog ${APACHE_LOG_DIR}/access.log combined 380 | 381 | 382 | 383 | ``` 384 | - Example for Caddy 385 | ```caddy 386 | www.example.com { 387 | proxy / localhost:3005 388 | } 389 | ``` 390 | 10. Access `https://www.example.com` and you can register as many users as you wish! :) 391 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Femto Apps 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 9 | [![Contributors][contributors-shield]][contributors-url] 10 | [![Forks][forks-shield]][forks-url] 11 | [![Stargazers][stars-shield]][stars-url] 12 | [![Issues][issues-shield]][issues-url] 13 | [![Chat][chat-shield]][chat-url] 14 | [![MIT License][license-shield]][license-url] 15 | 16 | 17 |
18 |

19 | 20 | Logo 21 | 22 | 23 |

Web File Uploader

24 | 25 |

26 | Share files, images and other content. 27 |
28 | Explore the docs » 29 |
30 |
31 | View Demo 32 | · 33 | Report Bug 34 | · 35 | Request Feature 36 | · 37 | Changelog 38 |

39 |

40 | 41 | 42 | 43 | 44 | ## Table of Contents 45 | 46 | * [About the Project](#about-the-project) 47 | * [Built With](#built-with) 48 | * [Getting Started](#getting-started) 49 | * [Prerequisites](#prerequisites) 50 | * [Installation](#installation) 51 | * [Usage](#usage) 52 | * [Roadmap](#roadmap) 53 | * [Contributing](#contributing) 54 | * [License](#license) 55 | * [Contact](#contact) 56 | * [Acknowledgements](#acknowledgements) 57 | 58 | 59 | 60 | 61 | ## About The Project 62 | 63 | [![Web File Uploader Screenshot][product-screenshot]](https://v2.femto.pw) 64 | 65 | Sharing files, images and other forms of media between users should be easy and fast. Unfortunately many existing sharing solutions will only accept certain formats (e.g. sharing images) or have security issues surrounding execution of untrusted code. 66 | 67 | Thus, the aptly named 'web file uploader' has been written, it's main features include: 68 | 69 | - Fast and minimal, pages are <20kb in size. 70 | - Intelligent handling of files based on type, can generate icons and previews. 71 | - Handles images, text, URLs and other forms of media. 72 | 73 | ### Built With 74 | 75 | * [Node](https://nodejs.org) 76 | * [Redis](https://redis.io/) 77 | * [MongoDB](https://www.mongodb.com/) 78 | 79 | 80 | ## Getting Started 81 | 82 | To get a local copy up and running please see [INSTALL.md](INSTALL.md) 83 | 84 | 85 | ## Roadmap 86 | 87 | See the [open issues](https://github.com/femto-apps/web-file-uploader/issues) for a list of proposed features (and known issues). 88 | 89 | 90 | 91 | 92 | ## Contributing 93 | 94 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 95 | 96 | 1. Fork the Project 97 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 98 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 99 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 100 | 5. Open a Pull Request 101 | 102 | 103 | 104 | 105 | ## License 106 | 107 | Distributed under the MIT License. See `LICENSE` for more information. 108 | 109 | 110 | 111 | 112 | ## Contact 113 | 114 | Alexander Craggs - uploader@femto.host 115 | Dimi3 - https://github.com/Owkoot 116 | 117 | Project Link: [https://github.com/femto-apps/web-file-uploader](https://github.com/femto-apps/web-file-uploader) 118 | 119 | 120 | 121 | 122 | ## Acknowledgements 123 | * [Img Shields](https://shields.io) 124 | * [Choose an Open Source License](https://choosealicense.com) 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | [contributors-shield]: https://img.shields.io/github/contributors/femto-apps/web-file-uploader.svg?style=flat-square 133 | [contributors-url]: https://github.com/femto-apps/web-file-uploader/graphs/contributors 134 | [forks-shield]: https://img.shields.io/github/forks/femto-apps/web-file-uploader.svg?style=flat-square 135 | [forks-url]: https://github.com/femto-apps/web-file-uploader/network/members 136 | [chat-shield]: https://img.shields.io/discord/493418312714289158?style=flat-square 137 | [chat-url]: https://femto.pw/discord 138 | [stars-shield]: https://img.shields.io/github/stars/femto-apps/web-file-uploader.svg?style=flat-square 139 | [stars-url]: https://github.com/femto-apps/web-file-uploader/stargazers 140 | [issues-shield]: https://img.shields.io/github/issues/femto-apps/web-file-uploader.svg?style=flat-square 141 | [issues-url]: https://github.com/femto-apps/web-file-uploader/issues 142 | [license-shield]: https://img.shields.io/github/license/femto-apps/web-file-uploader.svg?style=flat-square 143 | [license-url]: https://github.com/femto-apps/web-file-uploader/blob/master/LICENSE.txt 144 | [product-screenshot]: public/images/new_screenshot.png 145 | -------------------------------------------------------------------------------- /config.default.hjson: -------------------------------------------------------------------------------- 1 | { 2 | port: 3005 3 | dev: false 4 | trustedProxy: ['127.0.0.1/8', '::1/128'] 5 | title: { 6 | name: Femto Uploader 7 | shortener: Femto Shortener 8 | suffix: Femto Uploader 9 | } 10 | url: { 11 | origin: https://v2.femto.pw/ 12 | } 13 | carbon: { 14 | path: /home/codefined/.yarn/bin/carbon-now 15 | } 16 | mongo: { 17 | uri: mongodb://localhost:27017/ 18 | db: fileUploader 19 | } 20 | minio: { 21 | host: 127.0.0.1 22 | port: 9000 23 | accessKey: 6ML8SMC66BO2IMCZH9IY 24 | secretKey: zO21IgZpawthEK6DibBOthwMWMFqM7iWr0riGCYR 25 | itemBucket: web-file-uploader 26 | useSSL: false 27 | } 28 | clamav: { 29 | enabled: true 30 | url: http://localhost:9002 31 | } 32 | session: { 33 | secret: 3a8glh3al85tgh3a538haga38g 34 | } 35 | redis: { 36 | // url: redis://127.0.0.1:6379/0 37 | host: localhost 38 | port: 6379 39 | db: 0 40 | } 41 | cookie: { 42 | name: file-uploader 43 | maxAge: 15552000000 // 1000 * 60 * 60 * 24 * 180 (6 months) 44 | } 45 | email: { 46 | name: 'localhost' 47 | host: 'smtp.example.com' 48 | secure: false 49 | port: 587 50 | auth: { 51 | user: 'username' 52 | pass: 'password' 53 | } 54 | authMethod: 'PLAIN' 55 | ignoreTLS: false 56 | } 57 | tokenService: { 58 | endpoint: http://localhost:4500 59 | } 60 | authenticationProvider: { 61 | endpoint: http://localhost:3001 62 | consumerId: 49396890-9740-4cde-b3f2-ad6916039f93 63 | } 64 | authenticationConsumer: { 65 | endpoint: http://localhost:3005 66 | } 67 | experimental: { 68 | profiling: false 69 | statsTimer: 86400000 // 1000 * 60 * 60 * 24 (1 day) 70 | } 71 | cluster: { 72 | miscPort: 5052 73 | getPort: 5053 74 | getClusterPort: 5055 75 | } 76 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const session = require('express-session') 2 | const cookieParser = require('cookie-parser') 3 | const RedisStore = require('connect-redis')(session) 4 | const bodyParser = require('body-parser') 5 | const Multer = require('multer') 6 | const mongoose = require('mongoose') 7 | const express = require('express') 8 | const morgan = require('morgan') 9 | const { v4: uuidv4 } = require('uuid') 10 | const toArray = require('stream-to-array') 11 | const config = require('@femto-apps/config') 12 | const compression = require('compression') 13 | const redis = require('redis') 14 | const prettyBytes = require('pretty-bytes') 15 | const dateFormat = require('dateformat') 16 | const cors = require('cors') 17 | const authenticationConsumer = require('@femto-apps/authentication-consumer') 18 | const errorHandler = require('errorhandler') 19 | 20 | const Types = require('./types') 21 | const Item = require('./modules/Item') 22 | const Short = require('./modules/Short') 23 | const User = require('./modules/User') 24 | const Store = require('./modules/Store') 25 | const Collection = require('./modules/Collection') 26 | const Utils = require('./modules/Utils') 27 | const minioStorage = require('./modules/MinioMulterStorage') 28 | const ShareX = require('./modules/ShareX') 29 | const Archive = require('./modules/Archive') 30 | const ClamAV = require('./modules/ClamAV') 31 | const Stats = require('./modules/Stats') 32 | const Mail = require('./modules/Mail') 33 | 34 | const { wrap } = require('./modules/Profiling') 35 | 36 | const analytics = require('express-google-analytics')('UA-121951630-1') 37 | 38 | function ignoreAuth(req, res) { 39 | return req.originalUrl.startsWith('/thumb/') || req.originalUrl.startsWith('/js/') || req.originalUrl.startsWith('/css/') 40 | } 41 | 42 | ; (async () => { 43 | const app = express() 44 | const port = config.get('port') 45 | 46 | const clam = new ClamAV() 47 | const stats = new Stats() 48 | const mail = new Mail(config.get('email')) 49 | 50 | const multer = Multer({ 51 | storage: minioStorage({ 52 | minio: Object.assign({}, config.get('minio'), { endPoint: config.get('minio.host') }), 53 | bucket: (req, file) => config.get('minio.itemBucket'), 54 | folder: async (req, file) => { 55 | return (await Collection.fromReq(req)).path 56 | }, 57 | filename: async (req, file) => { 58 | return uuidv4() 59 | } 60 | }), 61 | limits: { fileSize: 8589934592 } 62 | }).single('upload') 63 | 64 | const multer_text = Multer().none() 65 | 66 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 67 | useNewUrlParser: true, 68 | useUnifiedTopology: true, 69 | useFindAndModify: false, 70 | useCreateIndex: true 71 | }) 72 | 73 | const redisClient = config.get('redis.url') 74 | ? redis.createClient({ 75 | url: config.get('redis.url') 76 | }) 77 | : redis.createClient({ 78 | host: config.get('redis.host') || '127.0.0.1', 79 | port: config.get('redis.port') || 6379, 80 | db: config.get('redis.db') || '0' 81 | }) 82 | 83 | app.set('view engine', 'pug') 84 | app.set('trust proxy', ['loopback', ...config.get('trustedProxy')].join(',')) 85 | app.disable('x-powered-by') 86 | 87 | app.use(wrap(morgan('dev'))) 88 | app.use(wrap(compression())) 89 | app.use(wrap(bodyParser.json())) 90 | app.use(wrap(bodyParser.urlencoded({ extended: true }))) 91 | app.use(wrap(cookieParser(config.get('cookie.secret')))) 92 | app.use(wrap(function sess(req, res, next) { 93 | // if we don't need req.user, ignore it. 94 | if (ignoreAuth(req, res)) { 95 | return next() 96 | } 97 | 98 | session({ 99 | store: new RedisStore({ 100 | client: redisClient 101 | }), 102 | secret: config.get('session.secret'), 103 | resave: false, 104 | saveUninitialized: false, 105 | name: config.get('cookie.name'), 106 | cookie: { 107 | maxAge: config.get('cookie.maxAge') 108 | } 109 | })(req, res, next) 110 | })) 111 | 112 | app.use(wrap(function authConsumer(req, res, next) { 113 | // if we don't need req.user, ignore it. 114 | if (ignoreAuth(req, res)) { 115 | return next() 116 | } 117 | 118 | authenticationConsumer({ 119 | tokenService: { endpoint: config.get('tokenService.endpoint') }, 120 | authenticationProvider: { endpoint: config.get('authenticationProvider.endpoint'), consumerId: config.get('authenticationProvider.consumerId') }, 121 | authenticationConsumer: { endpoint: config.get('authenticationConsumer.endpoint') }, 122 | redirect: config.get('redirect') 123 | })(req, res, next) 124 | })) 125 | 126 | app.use(wrap(async function fromUser(req, res, next) { 127 | // if we don't need req.user, ignore it. 128 | if (ignoreAuth(req, res)) { 129 | return next() 130 | } 131 | 132 | if (req.user) { 133 | req.user = await User.fromUserCached(req.user) 134 | } 135 | 136 | if (req.body && req.body.apiKey && !req.user) { 137 | req.user = await User.fromApiKey(req.body.apiKey) 138 | } 139 | 140 | next() 141 | })) 142 | 143 | app.use(wrap(function setIp(req, res, next) { 144 | req.ip = (req.headers['x-forwarded-for'] || '').split(',').pop() || 145 | req.connection.remoteAddress || 146 | req.socket.remoteAddress || 147 | req.connection.socket.remoteAddress 148 | 149 | next() 150 | })) 151 | 152 | app.use(wrap(function provideLinks(req, res, next) { 153 | const links = [] 154 | 155 | res.locals.req = req 156 | 157 | if (res.locals.auth) { 158 | if (req.user) { 159 | links.push({ title: 'Logout', href: res.locals.auth.getLogout(`${config.get('authenticationConsumer.endpoint')}${req.originalUrl}`) }) 160 | 161 | res.locals.user = req.user 162 | } else { 163 | links.push({ title: 'Login', href: res.locals.auth.getLogin(`${config.get('authenticationConsumer.endpoint')}${req.originalUrl}`) }) 164 | } 165 | } 166 | 167 | res.locals.nav = { 168 | title: `${config.get('title.suffix')}`, 169 | links 170 | } 171 | 172 | next() 173 | })) 174 | 175 | // app.use(wrap(analytics)) 176 | 177 | app.get('/', async (req, res) => { 178 | res.render('home', { 179 | page: { title: `Home :: ${config.get('title.suffix')}` }, 180 | key: req.user ? req.user.getApiKey() : undefined, 181 | origin: config.get('url.origin') 182 | }) 183 | }) 184 | 185 | app.get('/robots.txt', async (req, res) => { 186 | res.set('Content-Type', 'text/plain') 187 | res.send(`User-agent: *\nAllow: /`) 188 | }) 189 | 190 | app.get('/terms', async (req, res) => { 191 | res.render('terms', { 192 | page: { title: `Terms of Service :: ${config.get('title.suffix')}` } 193 | }) 194 | }) 195 | 196 | app.get('/privacy', async (req, res) => { 197 | res.render('privacy', { 198 | page: { title: `Privacy :: ${config.get('title.suffix')}` } 199 | }) 200 | }) 201 | 202 | // app.get('/export', async (req, res) => { 203 | // if (!req.user) { 204 | // res.send('Please login to export your uploads') 205 | // } 206 | 207 | // const collection = await Collection.fromReq(req) 208 | // const items = (await collection.list()) 209 | // .filter(item => item.item.metadata.filetype !== 'thumb') 210 | // .filter(item => item.item.metadata.expired !== true) 211 | 212 | // res.set('Content-Disposition', `filename="femto_export.zip"`) 213 | // res.set('Content-Type', 'application/zip') 214 | 215 | // console.log('hey') 216 | 217 | // Archive.archive(res, items) 218 | // }) 219 | 220 | app.get('/stats', async (req, res) => { 221 | res.render('stats', { 222 | stats: await stats.getRecent(365), 223 | page: { title: `Stats :: ${config.get('title.suffix')}` } 224 | }) 225 | }) 226 | 227 | app.get('/uploads', async (req, res) => { 228 | if (!req.user) { 229 | res.send('Please login to see uploads') 230 | } 231 | 232 | const collection = await Collection.fromReq(req) 233 | const items = (await collection.list()) 234 | .filter(item => item.item.metadata.filetype !== 'thumb') 235 | .filter(item => item.item.metadata.expired !== true) 236 | .filter(item => item.item.deleted !== true) 237 | 238 | // console.log(items.forEach(item => { 239 | // console.log(item.item.references) 240 | // })) 241 | 242 | res.render('uploads', { 243 | page: { title: `Uploads :: ${config.get('title.suffix')}` }, 244 | items 245 | }) 246 | }) 247 | 248 | // /i/upload/url is deprecated, please use /upload/url 249 | app.options(['/i/upload/url', '/upload/url'], cors()) 250 | app.post(['/i/upload/url', '/upload/url'], cors(), multer_text, async (req, res) => { 251 | req.user = await User.fromReq(req) 252 | const expiresAt = Utils.parseExpiry(req.body.expiry) 253 | 254 | // console.log(req.body, req.headers) 255 | 256 | const item = await Item.create({ 257 | name: { 258 | // req.body.text is only used in uploader v2 259 | original: req.body.url || req.body.text, 260 | }, 261 | metadata: { 262 | filetype: 'url', 263 | expiresAt 264 | }, 265 | user: req.user && req.user.getIdentifier() ? { _id: req.user.getIdentifier() } : { ip: req.ip } 266 | }) 267 | 268 | const short = await Short.generate({ keyLength: 4 }) 269 | const shortItem = await Short.createReference(short, item.item) 270 | await item.setCanonical(shortItem) 271 | 272 | res.json({ data: { short } }) 273 | }) 274 | 275 | // /i/upload and /upload are deprecated, please use /upload/multipart 276 | app.options(['/i/upload', '/upload', '/upload/multipart'], cors()) 277 | app.post(['/i/upload', '/upload', '/upload/multipart'], cors(), multer, async (req, res) => { 278 | req.user = await User.fromReq(req) 279 | 280 | // console.log(req.file) 281 | 282 | const originalName = req.file.originalname 283 | const extension = originalName.slice((originalName.lastIndexOf(".") - 1 >>> 0) + 2) 284 | const expiresAt = Utils.parseExpiry(req.body.expiry) 285 | const store = await Store.create(req.file.storage) 286 | const bytes = (await toArray(await store.getStream({ end: 2048, start: 0 })))[0] 287 | const { data } = await Types.detect(store, bytes, req.file) 288 | 289 | const item = await Item.create({ 290 | name: { 291 | original: originalName, 292 | extension: extension, 293 | filename: originalName.replace(/\.[^/.]+$/, '') 294 | }, 295 | metadata: { 296 | expiresAt, 297 | createdAt: new Date(), 298 | updatedAt: new Date(), 299 | size: req.file.storage.size, 300 | ...data 301 | }, 302 | references: { 303 | storage: store.store 304 | }, 305 | user: req.user && req.user.getIdentifier() ? { _id: req.user.getIdentifier() } : { ip: req.ip } 306 | }) 307 | 308 | const short = await Short.generate({ keyLength: 4 }) 309 | const shortItem = await Short.createReference(short, item.item) 310 | await item.setCanonical(shortItem) 311 | 312 | res.json({ data: { short } }) 313 | 314 | // console.log('scanning file') 315 | clam.scan(originalName, await store.getStream()).then(async result => { 316 | if (result.disabled) { 317 | return console.log(`didn't run scan because it was disabled.`) 318 | } 319 | 320 | const virusResult = { 321 | run: true, 322 | description: result.Description 323 | } 324 | 325 | if (result.Status === 'FOUND') { 326 | virusResult.detected = true 327 | } else if (result.Status === 'OK') { 328 | virusResult.detected = false 329 | } 330 | 331 | await item.setVirus(virusResult) 332 | console.log(`updated file ${item.item._id} ${result.Status}: "${result.Description}"`) 333 | }) 334 | }) 335 | 336 | app.use(wrap(express.static('public'))) 337 | app.use(wrap(express.static('public/favicons'))) 338 | 339 | app.get('/sharex/uploader.sxcu', ShareX.downloadUploader) 340 | app.get('/sharex/shortener.sxcu', ShareX.downloadShortener) 341 | 342 | app.get('/issue', async (req, res) => { 343 | res.render('issues', { 344 | page: { title: `Issues :: ${config.get('title.suffix')}` } 345 | }) 346 | }) 347 | 348 | app.post('/issue', async (req, res) => { 349 | console.log('sending suggestion', req.body) 350 | const response = await mail.sendMail({ 351 | from: 'uploader@femto.host', 352 | to: 'uploader@femto.host', 353 | subject: 'Contact Form', 354 | text: `Contact: ${req.body.contact}\nMessage: ${req.body.issue}` 355 | }) 356 | 357 | res.send("thanks for your message, if you entered in a way to contact you we'll be in touch with our response :)") 358 | }) 359 | 360 | app.get(['/info/:item', '/info/:item/*'], Item.fromReq, async (req, res) => { 361 | res.render('info', { 362 | item: req.item.item.item, 363 | page: { title: `Info :: ${config.get('title.suffix')}` }, 364 | prettyBytes, 365 | dateFormat, 366 | isOwner: await req.item.ownedBy(req.user) 367 | }) 368 | }) 369 | 370 | app.get(['/thumb/:item', '/thumb/:item/*'], Item.fromReq, async (req, res) => { 371 | req.item.thumb(req, res) 372 | }) 373 | 374 | app.get(['/delete/:item', '/delete/item/*'], Item.fromReq, async (req, res) => { 375 | if (!(await req.item.ownedBy(req.user))) { 376 | return res.json({ removed: false, err: 'You do not own this item.' }) 377 | } 378 | 379 | await req.item.delete() 380 | 381 | return res.json({ removed: true }) 382 | }) 383 | 384 | app.get(['/:item', '/:item/*'], Item.fromReq, async (req, res) => { 385 | req.item.serve(req, res) 386 | }) 387 | 388 | process.on('uncaughtException', function (exception) { 389 | console.log(exception); // to see your exception details in the console 390 | // if you are on production, maybe you can send the exception details to your 391 | // email as well ? 392 | }) 393 | 394 | app.use(errorHandler({ dumpExceptions: true, showStack: true })) 395 | 396 | app.listen(port, () => console.log(`Example app listening on port ${port}`)) 397 | })() 398 | -------------------------------------------------------------------------------- /micro/Router.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const bodyParser = require('body-parser') 3 | const cookieParser = require('cookie-parser') 4 | const config = require('@femto-apps/config') 5 | const session = require('express-session') 6 | const RedisStore = require('connect-redis')(session) 7 | const redis = require('redis') 8 | const authenticationConsumer = require('@femto-apps/authentication-consumer') 9 | const cors = require('cors') 10 | 11 | const User = require('../modules/User') 12 | 13 | class Router extends express.Router { 14 | constructor() { 15 | super() 16 | 17 | this.parseURLEncoded = Router.parseURLEncoded 18 | this.parseCookies = Router.parseCookies 19 | this.setupSessions = Router.setupSessions 20 | this.setupAuthentication = Router.setupAuthentication 21 | this.enableRendering = Router.enableRendering 22 | this.addNav = Router.addNav 23 | this.cors = cors 24 | this.parseUser = Router.parseUser 25 | } 26 | 27 | static addNav(req, res, next) { 28 | const links = [] 29 | 30 | if (res.locals.auth) { 31 | if (req.user) { 32 | links.push({ title: 'Logout', href: res.locals.auth.getLogout(`${config.get('authenticationConsumer.endpoint')}${req.originalUrl}`) }) 33 | 34 | res.locals.user = req.user 35 | } else { 36 | links.push({ title: 'Login', href: res.locals.auth.getLogin(`${config.get('authenticationConsumer.endpoint')}${req.originalUrl}`) }) 37 | } 38 | } 39 | 40 | res.locals.nav = { 41 | title: `${config.get('title.suffix')}`, 42 | links 43 | } 44 | 45 | next() 46 | } 47 | 48 | static enableRendering() { 49 | this.use((req, res, next) => { 50 | function sendPage(view, object) { 51 | return res.render(view, Object.assign({ 52 | page: { title: `${object.title} :: ${config.get('title.suffix')}` }, 53 | user: req.user 54 | }, object)) 55 | } 56 | 57 | res.page = sendPage 58 | 59 | next() 60 | }) 61 | } 62 | 63 | static parseURLEncoded() { 64 | this.use(bodyParser.urlencoded({ extended: true })) 65 | } 66 | 67 | static parseCookies() { 68 | this.use(cookieParser(config.get('cookie.secret'))) 69 | } 70 | 71 | static async parseUser(req, res, next) { 72 | if (req.user) { 73 | req.user = await User.fromUserCached(req.user) 74 | } 75 | 76 | if (req.body && req.body.apiKey && !req.user) { 77 | req.user = await User.fromApiKey(req.body.apiKey) 78 | } 79 | 80 | next() 81 | } 82 | 83 | static setupSessions() { 84 | const redisClient = redis.createClient({ 85 | host: config.get('redis.host'), 86 | port: config.get('redis.port') 87 | }) 88 | 89 | this.session = session({ 90 | store: new RedisStore({ 91 | client: redisClient 92 | }), 93 | secret: config.get('session.secret'), 94 | resave: false, 95 | saveUninitialized: false, 96 | name: config.get('cookie.name'), 97 | cookie: { 98 | maxAge: config.get('cookie.maxAge') 99 | } 100 | }) 101 | } 102 | 103 | static setupAuthentication() { 104 | this.authenticate = authenticationConsumer({ 105 | tokenService: { endpoint: config.get('tokenService.endpoint') }, 106 | authenticationProvider: { endpoint: config.get('authenticationProvider.endpoint'), consumerId: config.get('authenticationProvider.consumerId') }, 107 | authenticationConsumer: { endpoint: config.get('authenticationConsumer.endpoint') }, 108 | redirect: config.get('redirect') 109 | }) 110 | } 111 | } 112 | 113 | module.exports = Router -------------------------------------------------------------------------------- /micro/SingleInstance.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | const morgan = require('morgan') 4 | 5 | const Router = require('./Router') 6 | const uploadFile = require('./services/uploadFile') 7 | const shortenUrl = require('./services/shortenUrl') 8 | const getItem = require('./services/getItem') 9 | const admin = require('./services/admin') 10 | 11 | const app = express() 12 | 13 | const router = new Router() 14 | 15 | app.set('view engine', 'pug') 16 | app.set('views', '../views') 17 | 18 | app.use(express.static(path.join(__dirname, '../public'))) 19 | app.use(morgan('dev')) 20 | 21 | app.use(uploadFile(router)) 22 | app.use(shortenUrl(router)) 23 | app.use(admin(router)) 24 | app.use(getItem(router)) 25 | 26 | app.listen(3050, () => console.log('listening on port 3050')) -------------------------------------------------------------------------------- /micro/apps/getItem.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const morgan = require('morgan') 3 | const config = require('@femto-apps/config') 4 | const { cachedGetItem } = require('../helpers') 5 | 6 | const Router = require('../Router') 7 | const getItem = require('../services/getItem') 8 | 9 | const app = express() 10 | 11 | const router = new Router() 12 | 13 | app.use(morgan('dev')) 14 | app.use((req, res, next) => { 15 | res.set('X-CLUSTER', 'get') 16 | next() 17 | }) 18 | app.use(getItem(router)) 19 | 20 | app.listen(config.get('cluster.getPort'), () => console.log(`listening on port ${config.get('cluster.getPort')}`)) 21 | 22 | const cluster = express() 23 | cluster.get('/buster', (req, res) => { 24 | console.log('deleted cache for', req.query.short) 25 | cachedGetItem.delete(req.query.short) 26 | res.json({ deleted: true }) 27 | }) 28 | cluster.listen(config.get('cluster.getClusterPort'), () => console.log(`cluster listening on port ${config.get('cluster.getClusterPort')}`)) -------------------------------------------------------------------------------- /micro/apps/misc.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | const morgan = require('morgan') 4 | const config = require('@femto-apps/config') 5 | 6 | const Router = require('../Router') 7 | const uploadFile = require('../services/uploadFile') 8 | const shortenUrl = require('../services/shortenUrl') 9 | const admin = require('../services/admin') 10 | 11 | const app = express() 12 | const router = new Router() 13 | 14 | app.set('view engine', 'pug') 15 | app.set('views', '../../views') 16 | 17 | app.use((req, res, next) => { 18 | res.set('X-CLUSTER', 'admin') 19 | next() 20 | }) 21 | app.use(express.static(path.join(__dirname, '../../public'))) 22 | app.use(express.static(path.join(__dirname, '../../public/favicons'))) 23 | app.use(morgan('dev')) 24 | 25 | app.use(uploadFile(router)) 26 | app.use(shortenUrl(router)) 27 | app.use(admin(router)) 28 | 29 | app.listen(config.get('cluster.miscPort'), () => console.log(`listening on port ${config.get('cluster.miscPort')}`)) -------------------------------------------------------------------------------- /micro/helpers.js: -------------------------------------------------------------------------------- 1 | const toArray = require('stream-to-array') 2 | const mongoose = require('mongoose') 3 | const config = require('@femto-apps/config') 4 | const memoize = require('memoizee') 5 | const fetch = require('node-fetch') 6 | 7 | const Types = require('../types') 8 | const Item = require('../modules/Item') 9 | const Short = require('../modules/Short') 10 | const Store = require('../modules/Store') 11 | 12 | let connectedToMongoose = false 13 | 14 | module.exports.connectToMongoose = () => { 15 | if (connectedToMongoose) return 16 | 17 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 18 | useNewUrlParser: true, 19 | useUnifiedTopology: true, 20 | useFindAndModify: false, 21 | useCreateIndex: true 22 | }) 23 | 24 | connectedToMongoose = true 25 | } 26 | 27 | module.exports.bustCache = async (short) => { 28 | return fetch(`http://localhost:${config.get('cluster.getClusterPort')}/buster?short=${short}`) 29 | .then(res => res.json()) 30 | } 31 | 32 | module.exports.getTypeFromStore = async (store, metadata) => { 33 | const bytes = (await toArray(await store.getStream({ end: 2048, start: 0 })))[0] 34 | const { data } = await Types.detect(store, bytes, metadata) 35 | 36 | return data 37 | } 38 | 39 | module.exports.createUrlItem = async (url, { expiry, user }) => { 40 | const item = await Item.create({ 41 | name: { 42 | // req.body.text is only used in uploader v2 43 | original: url, 44 | }, 45 | metadata: { 46 | filetype: 'url', 47 | expiresAt: expiry 48 | }, 49 | user 50 | }) 51 | 52 | const shortWord = await Short.generate({ keyLength: 4 }) 53 | const short = await Short.createReference(shortWord, item.item) 54 | await item.setCanonical(short) 55 | 56 | return { item, short } 57 | } 58 | 59 | module.exports.createItem = async (storage, { expiry, mimetype, encoding, filename, user }) => { 60 | const extension = filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2) 61 | 62 | const store = await Store.create(storage) 63 | const type = await module.exports.getTypeFromStore(store, { mimetype, encoding }) 64 | 65 | const item = await Item.create({ 66 | name: { 67 | original: filename, 68 | extension: extension, 69 | filename: filename.replace(/\.[^/.]+$/, '') 70 | }, 71 | metadata: { 72 | expiresAt: expiry, 73 | createdAt: new Date(), 74 | updatedAt: new Date(), 75 | size: storage.size, 76 | ...type 77 | }, 78 | references: { 79 | storage: store.store 80 | }, 81 | user 82 | }) 83 | 84 | const shortWord = await Short.generate({ keyLength: 4 }) 85 | const short = await Short.createReference(shortWord, item.item) 86 | await item.setCanonical(short) 87 | 88 | return { item, store, short } 89 | } 90 | 91 | module.exports.getItem = async (name) => { 92 | const short = await Short.get(name.split('.')[0]) 93 | 94 | if (short === null) { 95 | return undefined 96 | } 97 | 98 | const item = new Item(short.item) 99 | const type = Types.match(item) 100 | 101 | return type 102 | } 103 | 104 | module.exports.cachedGetItem = memoize(module.exports.getItem, { 105 | primitive: true, 106 | length: 1, 107 | resolvers: [String], 108 | promise: true, 109 | maxAge: 1000 * 60 * 60, 110 | preFetch: false 111 | }) -------------------------------------------------------------------------------- /micro/routes/admin.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | 3 | const Mail = require('../../modules/Mail') 4 | 5 | const mail = new Mail(config.get('email')) 6 | 7 | module.exports.getHome = (req, res) => { 8 | res.page('home', { 9 | title: 'Home', 10 | origin: config.get('url.origin') 11 | }) 12 | } 13 | 14 | module.exports.getIssue = (req, res) => { 15 | res.page('issues', { 16 | title: 'Issues' 17 | }) 18 | } 19 | 20 | module.exports.postIssue = async (req, res) => { 21 | console.log('sending suggestion', req.body) 22 | const response = await mail.sendMail({ 23 | from: 'uploader@femto.host', 24 | to: 'uploader@femto.host', 25 | subject: 'Contact Form', 26 | text: `Contact: ${req.body.contact}\nMessage: ${req.body.issue}` 27 | }) 28 | 29 | res.send("thanks for your message, if you entered in a way to contact you we'll be in touch with our response :)") 30 | } 31 | 32 | module.exports.getStats = (req, res) => { 33 | res.send('temp disabled') 34 | } 35 | 36 | module.exports.getUploader = (req, res) => { 37 | res.setHeader('Content-Disposition', `attachment; filename="${config.get('title.name')}.sxcu"`) 38 | res.setHeader('Content-Type', 'application/octet-binary; charset=utf-8') 39 | res.send(JSON.stringify({ 40 | Name: config.get('title.suffix'), 41 | DestinationType: 'ImageUploader, TextUploader, FileUploader', 42 | RequestURL: `${config.get('url.origin')}upload/multipart`, 43 | FileFormName: 'upload', 44 | Arguments: { 45 | apiKey: req.user ? req.user.getApiKey() : undefined 46 | }, 47 | URL: `${config.get('url.origin')}$json:data.short$`, 48 | ThumbnailURL: `${config.get('url.origin')}thumb/$json:data.short$`, 49 | DeletionURL: `${config.get('url.origin')}delete/$json:data.short$?apiKey=${req.user ? req.user.getApiKey() : undefined}` 50 | })) 51 | } 52 | 53 | module.exports.getShortener = (req, res) => { 54 | res.setHeader('Content-Disposition', `attachment; filename="${config.get('title.shortener')}.sxcu"`) 55 | res.setHeader('Content-Type', 'application/octet-binary; charset=utf-8') 56 | res.send(JSON.stringify({ 57 | Name: config.get('title.shortener'), 58 | DestinationType: 'URLShortener', 59 | RequestURL: `${config.get('url.origin')}upload/url`, 60 | Arguments: { 61 | apiKey: req.user ? req.user.getApiKey() : undefined, 62 | text: '$input$' 63 | }, 64 | URL: `${config.get('url.origin')}$json:data.short$`, 65 | ThumbnailURL: `${config.get('url.origin')}thumb/$json:data.short$`, 66 | DeletionURL: `${config.get('url.origin')}delete/$json:data.short$?apiKey=${req.user ? req.user.getApiKey() : undefined}` 67 | })) 68 | } 69 | 70 | module.exports.getTerms = (req, res) => { 71 | res.page('terms', { 72 | title: 'Terms of Service' 73 | }) 74 | } 75 | 76 | module.exports.getPrivacy = (req, res) => { 77 | res.page('privacy', { 78 | title: 'Privacy' 79 | }) 80 | } 81 | 82 | module.exports.getRobots = (req, res) => { 83 | res.set('Content-Type', 'text/plain') 84 | res.send(`User-agent: *\nAllow: /`) 85 | } -------------------------------------------------------------------------------- /micro/routes/deleteItem.js: -------------------------------------------------------------------------------- 1 | const Utils = require('../../modules/Utils') 2 | const { getItem } = require('../helpers') 3 | 4 | module.exports = async (req, res) => { 5 | const item = await getItem(req.params.item) 6 | 7 | if (!item.ownedBy(req.user)) { 8 | return res.json({ removed: false, err: 'You do not own this item.' }) 9 | } 10 | 11 | await item.delete() 12 | 13 | return res.json({ removed: true }) 14 | } -------------------------------------------------------------------------------- /micro/routes/getItem.js: -------------------------------------------------------------------------------- 1 | const { cachedGetItem } = require('../helpers') 2 | 3 | module.exports = async (req, res) => { 4 | const item = await cachedGetItem(req.params.item) 5 | 6 | if (!item) { 7 | return res.status(404).send("Sorry, can't find that!") 8 | } 9 | 10 | return item.serve(req, res) 11 | } -------------------------------------------------------------------------------- /micro/routes/getItemInfo.js: -------------------------------------------------------------------------------- 1 | const prettyBytes = require('pretty-bytes') 2 | const dateFormat = require('dateformat') 3 | 4 | const { cachedGetItem } = require('../helpers') 5 | 6 | module.exports = async (req, res) => { 7 | const item = await cachedGetItem(req.params.item) 8 | 9 | return res.page('info', { 10 | item: item.item.item, 11 | title: 'Info', 12 | prettyBytes, 13 | dateFormat, 14 | isOwner: await item.ownedBy(req.user) 15 | }) 16 | } -------------------------------------------------------------------------------- /micro/routes/getThumb.js: -------------------------------------------------------------------------------- 1 | const { getItem } = require('../helpers') 2 | 3 | module.exports = async (req, res) => { 4 | const item = await getItem(req.params.item) 5 | 6 | if (!item) { 7 | return res.status(404).send("Sorry, can't find that!") 8 | } 9 | 10 | return item.thumb(req, res) 11 | } -------------------------------------------------------------------------------- /micro/routes/getUploads.js: -------------------------------------------------------------------------------- 1 | const Collection = require('../../modules/Collection') 2 | 3 | module.exports = async (req, res) => { 4 | if (!req.user) { 5 | res.send('Please login to see uploads') 6 | } 7 | 8 | const collection = await Collection.fromReq(req) 9 | const items = (await collection.list()) 10 | .filter(item => item.item.metadata.filetype !== 'thumb') 11 | .filter(item => item.item.metadata.expired !== true) 12 | .filter(item => item.item.deleted !== true) 13 | 14 | res.page('uploads', { 15 | title: 'Uploads', 16 | items 17 | }) 18 | } -------------------------------------------------------------------------------- /micro/routes/index.js: -------------------------------------------------------------------------------- 1 | const postUpload = require('./postUpload') 2 | const postUrl = require('./postUrl') 3 | const admin = require('./admin') 4 | const getItemInfo = require('./getItemInfo') 5 | const deleteItem = require('./deleteItem') 6 | const getUploads = require('./getUploads') 7 | const getItem = require('./getItem') 8 | const getThumb = require('./getThumb') 9 | 10 | module.exports = { 11 | postUpload, 12 | postUrl, 13 | getItemInfo, 14 | deleteItem, 15 | getUploads, 16 | getItem, 17 | getThumb, 18 | ...admin 19 | } -------------------------------------------------------------------------------- /micro/routes/postUpload.js: -------------------------------------------------------------------------------- 1 | const Utils = require('../../modules/Utils') 2 | const { createItem, bustCache } = require('../helpers') 3 | 4 | module.exports = async (req, res) => { 5 | const filename = req.file.originalname 6 | const expiry = Utils.parseExpiry(req.body.expiry) 7 | const storage = req.file.storage 8 | 9 | const mimetype = req.file.mimetype 10 | const encoding = req.file.encoding 11 | 12 | const user = req.user && req.user.getIdentifier() ? { _id: req.user.getIdentifier() } : { ip: req.ip } 13 | 14 | const { item, store, short } = await createItem(storage, { 15 | expiry, mimetype, encoding, filename, user 16 | }) 17 | 18 | await bustCache(short.short) 19 | 20 | return res.json({ data: short.short }) 21 | } -------------------------------------------------------------------------------- /micro/routes/postUrl.js: -------------------------------------------------------------------------------- 1 | const Utils = require('../../modules/Utils') 2 | const { createUrlItem, bustCache } = require('../helpers') 3 | 4 | module.exports = async (req, res) => { 5 | const expiry = Utils.parseExpiry(req.body.expiry) 6 | const user = req.user && req.user.getIdentifier() ? { _id: req.user.getIdentifier() } : { ip: req.ip } 7 | const url = req.body.url || req.body.text 8 | 9 | const { item, short } = await createUrlItem(url, { 10 | expiry, user 11 | }) 12 | 13 | await bustCache(short.short) 14 | 15 | return res.json({ data: short.short }) 16 | } -------------------------------------------------------------------------------- /micro/services/admin.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | 3 | const helpers = require('../helpers') 4 | const routes = require('../routes') 5 | 6 | module.exports = router => { 7 | helpers.connectToMongoose() 8 | 9 | router.parseURLEncoded() 10 | router.parseCookies() 11 | router.setupSessions() 12 | router.enableRendering() 13 | 14 | router.post('/issue', routes.postIssue) 15 | router.get('/robots.txt', routes.getRobots) 16 | 17 | router.use(router.session) 18 | router.use(router.authenticate) 19 | router.use(router.parseUser) 20 | 21 | router.get('/', router.addNav, routes.getHome) 22 | 23 | router.get('/issue', router.addNav, routes.getIssue) 24 | 25 | router.get('/stats', router.addNav, routes.getStats) 26 | 27 | router.get('/sharex/uploader.sxcu', routes.getUploader) 28 | router.get('/sharex/shortener.sxcu', routes.getShortener) 29 | 30 | router.get('/terms', router.addNav, routes.getTerms) 31 | router.get('/privacy', router.addNav, routes.getPrivacy) 32 | 33 | 34 | router.get('/uploads', router.addNav, routes.getUploads) 35 | 36 | router.get(['/info/:item', '/info/:item/*'], 37 | router.addNav, 38 | routes.getItemInfo 39 | ) 40 | 41 | router.get(['/delete/:item', '/delete/:item/*'], 42 | router.addNav, 43 | routes.deleteItem 44 | ) 45 | 46 | return router 47 | } -------------------------------------------------------------------------------- /micro/services/getItem.js: -------------------------------------------------------------------------------- 1 | const helpers = require('../helpers') 2 | const { getThumb, getItem } = require('../routes') 3 | 4 | module.exports = router => { 5 | helpers.connectToMongoose() 6 | 7 | router.get(['/thumb/:item', '/thumb/:item/*'], getThumb) 8 | router.get(['/:item', '/:item/*'], getItem) 9 | 10 | return router 11 | } -------------------------------------------------------------------------------- /micro/services/shortenUrl.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | const Multer = require('multer') 3 | 4 | const routes = require('../routes') 5 | const helpers = require('../helpers') 6 | 7 | const multer = Multer().none() 8 | 9 | module.exports = router => { 10 | helpers.connectToMongoose() 11 | 12 | router.parseURLEncoded() 13 | router.parseCookies() 14 | router.setupSessions() 15 | router.setupAuthentication() 16 | 17 | router.use(router.session) 18 | router.use(router.authenticate) 19 | router.use(router.parseUser) 20 | 21 | router.options(['/i/upload/url', '/upload/url'], router.cors()) 22 | router.post(['/i/upload/url', '/upload/url'], 23 | router.cors(), 24 | multer, 25 | routes.postUrl 26 | ) 27 | 28 | return router 29 | } -------------------------------------------------------------------------------- /micro/services/uploadFile.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | const Multer = require('multer') 3 | const { v4: uuidv4 } = require('uuid') 4 | 5 | const routes = require('../routes') 6 | const minioStorage = require('../../modules/MinioMulterStorage') 7 | const Collection = require('../../modules/Collection') 8 | 9 | const helpers = require('../helpers') 10 | 11 | const multer = Multer({ 12 | storage: minioStorage({ 13 | minio: Object.assign({}, config.get('minio'), { endPoint: config.get('minio.host') }), 14 | bucket: (req, file) => config.get('minio.itemBucket'), 15 | folder: async (req, file) => { 16 | return (await Collection.fromReq(req)).path 17 | }, 18 | filename: async (req, file) => { 19 | return uuidv4() 20 | } 21 | }), 22 | limits: { fileSize: 8589934592 } 23 | }).single('upload') 24 | 25 | module.exports = router => { 26 | helpers.connectToMongoose() 27 | 28 | router.parseURLEncoded() 29 | router.parseCookies() 30 | router.setupSessions() 31 | router.setupAuthentication() 32 | 33 | router.use(router.session) 34 | router.use(router.authenticate) 35 | router.use(router.parseUser) 36 | 37 | router.options(['/i/upload', '/upload', '/upload/multipart'], router.cors()) 38 | router.post(['/i/upload', '/upload', '/upload/multipart'], 39 | router.cors(), 40 | multer, 41 | routes.postUpload 42 | ) 43 | 44 | return router 45 | } -------------------------------------------------------------------------------- /models/Collection.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const CollectionSchema = mongoose.Schema({ 4 | user: { type: mongoose.Schema.Types.ObjectId }, 5 | ip: { type: String }, 6 | path: { type: String, required: true }, 7 | }, { 8 | timestamps: true, 9 | strict: true 10 | }) 11 | 12 | module.exports = mongoose.model('Collection', CollectionSchema) 13 | -------------------------------------------------------------------------------- /models/Item.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const expiryTime = 10 * 1000 4 | 5 | const ItemSchema = mongoose.Schema({ 6 | name: { 7 | original: { type: String }, // 'Hello.test.exe' 8 | extension: { type: String }, // 'exe' 9 | filename: { type: String } // 'Hello.test' 10 | }, 11 | metadata: { 12 | views: { type: Number, default: 0 }, 13 | createdAt: { type: Date, default: Date.now }, 14 | updatedAt: { type: Date, default: Date.now }, 15 | mime: { type: String }, 16 | encoding: { type: String }, 17 | filetype: { type: String }, 18 | expiresAt: { type: Date }, 19 | expired: { type: Boolean }, 20 | size: { type: Number, default: 0 }, 21 | virus: { 22 | run: { type: Boolean, default: false }, 23 | detected: { type: Boolean }, 24 | description: { type: String } 25 | } 26 | }, 27 | references: { 28 | storage: { type: mongoose.Schema.Types.ObjectId, ref: 'Store', autopopulate: true }, 29 | thumb: { type: mongoose.Schema.Types.ObjectId, ref: 'Item', autopopulate: true }, 30 | canonical: { type: mongoose.Schema.Types.ObjectId, ref: 'Short', autopopulate: true }, 31 | aliases: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Short', autopopulate: true }] 32 | }, 33 | user: { 34 | _id: { type: mongoose.Schema.Types.ObjectId }, 35 | ip: { type: String } 36 | }, 37 | deleted: { type: Boolean, default: false }, 38 | version: { type: Number, default: 3 } 39 | }, { 40 | timestamps: true, 41 | strict: false 42 | }) 43 | 44 | ItemSchema.plugin(require('mongoose-autopopulate')) 45 | 46 | const ItemModel = mongoose.model('Item', ItemSchema) 47 | 48 | async function expireItems() { 49 | for (let item of await ItemModel.find({ 'metadata.expiresAt': { '$lte': new Date() }, 'metadata.expired': { '$exists': false } })) { 50 | item.metadata.expired = true 51 | await item.save() 52 | 53 | console.log(`Expired ${item.name.original}, ${item._id}`) 54 | } 55 | 56 | setTimeout(expireItems, expiryTime) 57 | } 58 | 59 | setTimeout(expireItems, expiryTime) 60 | 61 | module.exports = ItemModel 62 | module.exports.schema = ItemSchema -------------------------------------------------------------------------------- /models/Short.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const ShortSchema = mongoose.Schema({ 4 | short: { type: String, unique: true }, 5 | item: { type: mongoose.Schema.Types.ObjectId, ref: 'Item' } 6 | }, { 7 | timestamps: true, 8 | strict: true 9 | }) 10 | 11 | module.exports = mongoose.model('Short', ShortSchema) 12 | -------------------------------------------------------------------------------- /models/Stat.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const StatSchema = mongoose.Schema({ 4 | time: { type: Date }, 5 | field: { type: String }, 6 | value: { type: Number } 7 | 8 | }, { 9 | timestamps: true, 10 | strict: true 11 | }) 12 | 13 | module.exports = mongoose.model('Stat', StatSchema) 14 | -------------------------------------------------------------------------------- /models/Store.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const StoreSchema = mongoose.Schema({ 4 | store: { type: String }, 5 | bucket: { type: String }, 6 | folder: { type: String }, 7 | filename: { type: String }, 8 | filepath: { type: String }, 9 | size: { type: Number, default: 0 } 10 | }, { 11 | timestamps: true, 12 | strict: false 13 | }) 14 | 15 | module.exports = mongoose.model('Store', StoreSchema) 16 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const UserSchema = mongoose.Schema({ 4 | user: { type: mongoose.Schema.Types.ObjectId }, 5 | apiKey: { type: String }, 6 | }, { 7 | timestamps: true, 8 | strict: false 9 | }) 10 | 11 | module.exports = mongoose.model('User', UserSchema) 12 | module.exports.schema = UserSchema -------------------------------------------------------------------------------- /modules/Archive.js: -------------------------------------------------------------------------------- 1 | const archiver = require('archiver') 2 | const Minio = require('./Minio') 3 | 4 | class Archive { 5 | constructor() { 6 | } 7 | 8 | static async archive(pipe, items) { 9 | const minio = new Minio() 10 | 11 | const archive = archiver('zip', { 12 | zlib: { level: 9 } 13 | }) 14 | 15 | pipe.on('close', function () { 16 | console.log(archive.pointer() + ' total bytes') 17 | console.log('archiver has been finalized and the output file descriptor has closed.') 18 | }) 19 | 20 | pipe.on('end', function () { 21 | console.log('Data has been drained') 22 | }) 23 | 24 | archive.on('warning', function (err) { 25 | if (err.code === 'ENOENT') { 26 | // log warning 27 | console.log('archive warning', err) 28 | } else { 29 | // throw error 30 | throw err 31 | } 32 | }) 33 | 34 | archive.on('error', function (err) { 35 | throw err; 36 | }) 37 | 38 | archive.on('end', () => { 39 | console.log('archive end') 40 | pipe.end() 41 | }) 42 | 43 | archive.pipe(pipe) 44 | 45 | let i = 0 46 | 47 | for (let item of items) { 48 | console.log('getting an item') 49 | const itemStore = await item.getStore('references.storage') 50 | console.log('got store', itemStore.store.filepath, itemStore.store.bucket) 51 | const stream = await minio.download(itemStore.store.bucket, itemStore.store.filepath ) 52 | console.log('got download') 53 | archive.append(stream, { name: item.item.name.original }) 54 | console.log('appended stream') 55 | if (i > 100) { 56 | break 57 | } 58 | i++ 59 | } 60 | 61 | archive.finalize() 62 | } 63 | } 64 | 65 | module.exports = Archive -------------------------------------------------------------------------------- /modules/ClamAV.js: -------------------------------------------------------------------------------- 1 | const FormData = require('form-data') 2 | const config = require('@femto-apps/config') 3 | const fetch = require('node-fetch') 4 | const hjson = require('hjson') 5 | 6 | class ClamAV { 7 | constructor() { 8 | 9 | } 10 | 11 | async scan(filename, stream) { 12 | if (!config.get('clamav.enabled')) { 13 | return { 14 | disabled: true 15 | } 16 | } 17 | 18 | const form = new FormData() 19 | form.append('file', stream, { filename }) 20 | 21 | return fetch(`${config.get('clamav.url')}/scan`, { 22 | method: 'POST', 23 | body: form, 24 | headers: form.getHeaders() 25 | }) 26 | .then(res => res.text()) 27 | .then(res => { 28 | try { 29 | return hjson.parse(res) 30 | } catch (e) { 31 | console.log(res) 32 | console.error('invalid clamav response', res) 33 | } 34 | }) 35 | } 36 | } 37 | 38 | module.exports = ClamAV 39 | -------------------------------------------------------------------------------- /modules/Collection.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid') 2 | 3 | const CollectionModel = require('../models/Collection') 4 | const ItemModel = require('../models/Item') 5 | 6 | class Collection { 7 | constructor(collection) { 8 | this.collection = collection 9 | 10 | this.path = collection.path 11 | } 12 | 13 | static async fromReq(req) { 14 | if (req.user) { 15 | return Collection.fromUser(req.user) 16 | } 17 | 18 | return Collection.fromIp(req.ip) 19 | } 20 | 21 | static async fromItem(item) { 22 | if (item.user._id) { 23 | return Collection.fromUser({ 24 | getIdentifier: () => item.user._id 25 | }) 26 | } 27 | 28 | return Collection.fromIp(item.user.ip) 29 | } 30 | 31 | static async fromUser(user) { 32 | let collection = await CollectionModel.findOne({ 33 | user: user.getIdentifier() 34 | }) 35 | 36 | if (!collection) { 37 | collection = new CollectionModel({ 38 | user: user.getIdentifier(), 39 | path: uuidv4() + '/' 40 | }) 41 | 42 | collection = await collection.save() 43 | } 44 | 45 | return new Collection(collection) 46 | } 47 | 48 | static async fromIp(ip) { 49 | let collection = await CollectionModel.findOne({ 50 | ip 51 | }) 52 | 53 | if (!collection) { 54 | collection = new CollectionModel({ 55 | ip, 56 | path: uuidv4() + '/', 57 | }) 58 | 59 | await collection.save() 60 | } 61 | 62 | return new Collection(collection) 63 | } 64 | 65 | async list() { 66 | const Item = require('./Item') 67 | 68 | if (this.collection.user) { 69 | return (await ItemModel.find({ 'user._id': this.collection.user }).sort('-createdAt')).map(item => new Item(item)) 70 | } 71 | 72 | if (this.collection.ip) { 73 | return (await ItemModel.find({ 'user.ip': this.collection.ip }).sort('-createdAt')).map(item => new Item(item)) 74 | } 75 | } 76 | } 77 | 78 | module.exports = Collection -------------------------------------------------------------------------------- /modules/Item.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const { v4: uuidv4 } = require('uuid') 3 | const path = require('path') 4 | 5 | const ItemModel = require('../models/Item') 6 | const Short = require('./Short') 7 | const Store = require('./Store') 8 | const Types = require('../types') 9 | const Collection = require('./Collection') 10 | const config = require('@femto-apps/config') 11 | 12 | class Item { 13 | constructor(item) { 14 | this.item = item 15 | } 16 | 17 | static async create(itemInformation) { 18 | const item = new ItemModel(itemInformation) 19 | await item.save() 20 | 21 | return new Item(item) 22 | } 23 | 24 | static async fromReq(req, res, next) { 25 | const short = await Short.get(req.params.item.split('.')[0]) 26 | 27 | if (short === null) { 28 | return res.json({ error: 'Short not found' }) 29 | } 30 | 31 | const item = new Item(short.item) 32 | 33 | req.item = await Types.match(item) 34 | next() 35 | } 36 | 37 | async setVirus(status) { 38 | this.item.metadata.virus = status 39 | await this.item.save() 40 | } 41 | 42 | async setCanonical(shortItem) { 43 | this.item.references.canonical = shortItem.id() 44 | await this.item.save() 45 | } 46 | 47 | async getFiletype() { 48 | return this.item.metadata.filetype 49 | } 50 | 51 | async getExpired() { 52 | return this.item.metadata.expired 53 | } 54 | 55 | async getDeleted() { 56 | return this.item.deleted 57 | } 58 | 59 | async getVirus() { 60 | return this.item.metadata.virus 61 | } 62 | 63 | async getMime() { 64 | return this.item.metadata.mime 65 | } 66 | 67 | async getName() { 68 | return this.item.name.original 69 | } 70 | 71 | async getCanonical() { 72 | return this.item.references.canonical 73 | } 74 | 75 | async getStore(store) { 76 | return new Store(_.get(this.item, store)) 77 | } 78 | 79 | async getItem(item) { 80 | return new Item(_.get(this.item, item)) 81 | } 82 | 83 | async getItemStream(range) { 84 | const itemStore = await this.getStore('references.storage') 85 | 86 | return itemStore.getStream(range) 87 | } 88 | 89 | async getItemStat() { 90 | const itemStore = await this.getStore('references.storage') 91 | 92 | return itemStore.getStat() 93 | } 94 | 95 | async getThumb() { 96 | const thumbItem = await this.getItem('references.thumb') 97 | 98 | return thumbItem 99 | } 100 | 101 | async getThumbStream() { 102 | const thumbItem = await this.getItem('references.thumb') 103 | return thumbItem.getItemStream() 104 | } 105 | 106 | async hasThumb() { 107 | return typeof this.item.references.thumb !== 'undefined' 108 | } 109 | 110 | async markDeleted() { 111 | this.item.deleted = true 112 | await this.item.save() 113 | } 114 | 115 | async setThumb(stream) { 116 | const itemStore = await this.getStore('references.storage') 117 | const folder = itemStore.store ? await itemStore.getFolder() : (await Collection.fromItem(this.item)).path 118 | const filename = uuidv4() 119 | 120 | const thumbStorage = await Store.create({ 121 | store: 'minio', 122 | bucket: config.get('minio.itemBucket'), 123 | folder: folder, 124 | filename: filename, 125 | filepath: path.posix.join(folder, filename) 126 | }) 127 | 128 | await thumbStorage.setStream(stream) 129 | 130 | const thumb = await Item.create({ 131 | name: { 132 | original: `${this.item.name.filename}.png`, 133 | extension: 'png', 134 | filename: this.item.name.filename 135 | }, 136 | metadata: { 137 | mime: 'image/png', 138 | encoding: '7bit', 139 | filetype: 'thumb', 140 | expiresAt: this.item.metadata.expiresAt 141 | }, 142 | references: { 143 | storage: await thumbStorage.getModel() 144 | }, 145 | user: this.item.user 146 | }) 147 | 148 | this.item.references.thumb = await thumb.getModel() 149 | await this.item.save() 150 | } 151 | 152 | async ownedBy(user) { 153 | if (user && this.item.user._id.equals(user.user.generic._id)) { 154 | return true 155 | } 156 | 157 | return false 158 | } 159 | 160 | async getModel() { 161 | return this.item 162 | } 163 | 164 | async incrementViews() { 165 | return ItemModel.findOneAndUpdate({ _id: this.item._id }, { $inc: { 'metadata.views': 1 } }).exec() 166 | } 167 | 168 | async id() { 169 | return this.item._id 170 | } 171 | } 172 | 173 | module.exports = Item -------------------------------------------------------------------------------- /modules/Mail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer') 2 | 3 | class Mail { 4 | constructor(config) { 5 | this.transporter = nodemailer.createTransport(config) 6 | } 7 | 8 | async sendMail(data) { 9 | return this.transporter.sendMail(data) 10 | } 11 | } 12 | 13 | module.exports = Mail -------------------------------------------------------------------------------- /modules/Minio.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | const Minio = require('minio') 3 | 4 | class MinioModule { 5 | constructor() { 6 | this.client = new Minio.Client({ 7 | endPoint: config.get('minio.host'), 8 | port: config.get('minio.port'), 9 | useSSL: false, 10 | accessKey: config.get('minio.accessKey'), 11 | secretKey: config.get('minio.secretKey') 12 | }) 13 | } 14 | 15 | async download(bucket, file, range) { 16 | if (typeof range === 'undefined') return this.client.getObject(bucket, file) 17 | return this.client.getPartialObject(bucket, file, range.start, range.end - range.start) 18 | } 19 | 20 | async stat(bucket, file) { 21 | const stats = await this.client.statObject(bucket, file) 22 | 23 | return stats 24 | } 25 | 26 | async save({ bucket, filepath }, stream) { 27 | const res = await this.client.putObject(bucket, filepath, stream) 28 | 29 | return res 30 | } 31 | } 32 | 33 | module.exports = MinioModule -------------------------------------------------------------------------------- /modules/MinioMulterStorage.js: -------------------------------------------------------------------------------- 1 | const Minio = require('minio') 2 | const path = require('path') 3 | 4 | class MinioMulterStorage { 5 | constructor(opts) { 6 | for (let requirement of ['bucket', 'folder', 'filename', 'minio']) { 7 | if (typeof opts[requirement] === 'undefined') { throw new Error(`Expects ${requirement}`) } 8 | } 9 | 10 | this.bucket = opts.bucket 11 | this.folder = opts.folder 12 | this.filename = opts.filename 13 | this.middleware = opts.middleware 14 | 15 | this.minio = opts.minio 16 | this.client = new Minio.Client(this.minio) 17 | } 18 | 19 | async _handleFile(req, file, cb) { 20 | const bucket = await this.bucket(req, file) 21 | const folder = await this.folder(req, file) 22 | const filename = await this.filename(req, file) 23 | const filepath = path.posix.join(folder, filename) 24 | 25 | await this.client.putObject(bucket, filepath, file.stream, undefined) 26 | const stat = await this.client.statObject(bucket, filepath) 27 | 28 | cb(undefined, Object.assign({}, { 29 | storage: { 30 | store: 'minio', 31 | bucket, 32 | folder, 33 | filename, 34 | filepath, 35 | size: stat.size 36 | } 37 | })) 38 | } 39 | 40 | _removeFile(req, file, cb) { 41 | // remove file.path 42 | console.log('removal of files has to be implemented', file.path) 43 | 44 | // fs.unlink(file.path, cb) 45 | } 46 | } 47 | 48 | module.exports = opts => new MinioMulterStorage(opts) -------------------------------------------------------------------------------- /modules/Profiling.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events') 2 | const config = require('@femto-apps/config') 3 | 4 | const profiling = new EventEmitter() 5 | 6 | profiling.on('middleware', ({ req, name, time }) => { 7 | console.log(req.method, req.url, ':', name, `${time}ms`) 8 | }) 9 | 10 | function wrap(fn) { 11 | if (config.get('experimental.profiling') !== true) { 12 | return function (req, res, next) { 13 | fn(req, res, function () { 14 | next.apply(this, arguments) 15 | }) 16 | } 17 | } 18 | 19 | return function (req, res, next) { 20 | const start = Date.now() 21 | fn(req, res, function () { 22 | profiling.emit('middleware', { 23 | req, 24 | name: fn.name, 25 | time: Date.now() - start 26 | }) 27 | 28 | next.apply(this, arguments) 29 | }) 30 | } 31 | } 32 | 33 | module.exports = { wrap } -------------------------------------------------------------------------------- /modules/ShareX.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | 3 | class ShareX { 4 | constructor() {} 5 | 6 | static async downloadUploader(req, res) { 7 | res.setHeader('Content-Disposition', `attachment; filename="${config.get('title.name')}.sxcu"`) 8 | res.setHeader('Content-Type', 'application/octet-binary; charset=utf-8') 9 | res.send(JSON.stringify({ 10 | Name: config.get('title.suffix'), 11 | DestinationType: 'ImageUploader, TextUploader, FileUploader', 12 | RequestURL: `${config.get('url.origin')}upload/multipart`, 13 | FileFormName: 'upload', 14 | Arguments: { 15 | apiKey: req.user ? req.user.getApiKey() : undefined 16 | }, 17 | URL: `${config.get('url.origin')}$json:data.short$`, 18 | ThumbnailURL: `${config.get('url.origin')}thumb/$json:data.short$`, 19 | DeletionURL: `${config.get('url.origin')}delete/$json:data.short$?apiKey=${req.user ? req.user.getApiKey() : undefined}` 20 | })) 21 | } 22 | 23 | static async downloadShortener(req, res) { 24 | res.setHeader('Content-Disposition', `attachment; filename="${config.get('title.shortener')}.sxcu"`) 25 | res.setHeader('Content-Type', 'application/octet-binary; charset=utf-8') 26 | res.send(JSON.stringify({ 27 | Name: config.get('title.shortener'), 28 | DestinationType: 'URLShortener', 29 | RequestURL: `${config.get('url.origin')}upload/url`, 30 | Arguments: { 31 | apiKey: req.user ? req.user.getApiKey() : undefined, 32 | text: '$input$' 33 | }, 34 | URL: `${config.get('url.origin')}$json:data.short$`, 35 | ThumbnailURL: `${config.get('url.origin')}thumb/$json:data.short$`, 36 | DeletionURL: `${config.get('url.origin')}delete/$json:data.short$?apiKey=${req.user ? req.user.getApiKey() : undefined}` 37 | })) 38 | } 39 | } 40 | 41 | module.exports = ShareX 42 | -------------------------------------------------------------------------------- /modules/Short.js: -------------------------------------------------------------------------------- 1 | const ShortModel = require('../models/Short') 2 | 3 | const alphabet = '23456789abcdefghijkmnpqrstuvwxyz' 4 | 5 | // A short transforms a small string to an item reference 6 | class Short { 7 | constructor(short) { 8 | this.short = short 9 | } 10 | 11 | static async createReference(short, item) { 12 | console.log(`[+] Created short [${short}] for item [${item._id}, ${item.name.original}]`) 13 | 14 | const shortItem = new ShortModel({ 15 | short, item: item._id 16 | }) 17 | 18 | await shortItem.save() 19 | 20 | return new Short(shortItem) 21 | } 22 | 23 | static async generate(options) { 24 | if (typeof options === 'undefined') options = {} 25 | if (typeof options.keyLength === 'undefined') options.keyLength = 4 26 | 27 | let short = '' 28 | 29 | for (let i = 0; i < options.keyLength; i++) { 30 | short += alphabet[Math.floor(Math.random() * Math.floor(alphabet.length))] 31 | } 32 | 33 | if (await ShortModel.findOne({ short })) { 34 | return Short.generate(options) 35 | } 36 | 37 | return short 38 | } 39 | 40 | static async get(short) { 41 | const shortItem = await ShortModel.findOne({ short }) 42 | .populate('item') 43 | 44 | return shortItem 45 | } 46 | 47 | id() { 48 | return this.short._id 49 | } 50 | 51 | model() { 52 | return this.short 53 | } 54 | } 55 | 56 | module.exports = Short -------------------------------------------------------------------------------- /modules/Stats.js: -------------------------------------------------------------------------------- 1 | const UserModel = require('../models/User') 2 | const ItemModel = require('../models/Item') 3 | const StatModel = require('../models/Stat') 4 | 5 | const config = require('@femto-apps/config') 6 | 7 | class Stats { 8 | constructor() { 9 | setInterval(() => { 10 | this.update() 11 | }, config.get('experimental.statsTimer')) 12 | this.update() 13 | } 14 | 15 | async getRecent(time) { 16 | const [users, items, views, bandwidth] = await Promise.all([ 17 | this.getRecentByType('users', time), 18 | this.getRecentByType('items', time), 19 | this.getRecentByType('views', time), 20 | this.getRecentByType('bandwidth', time) 21 | ]) 22 | 23 | return { users, items, views, bandwidth } 24 | } 25 | 26 | // time is scale in days 27 | async getRecentByType(type, time) { 28 | const now = new Date() 29 | now.setDate(now.getDate() - time) 30 | 31 | const stats = await StatModel.find({ 32 | time: { $gte: now }, 33 | field: type, 34 | }) 35 | 36 | return stats.map(stat => ({ 37 | time: stat.time, 38 | value: stat.value 39 | })) 40 | } 41 | 42 | async update() { 43 | console.log('updating statistics') 44 | 45 | const userCount = await UserModel.countDocuments() 46 | const itemCount = await ItemModel.countDocuments() 47 | 48 | let viewCountResponse = await ItemModel.aggregate([{ $match: {} }, { 49 | $group: { 50 | _id: null, 51 | total: { 52 | $sum: "$metadata.views" 53 | } 54 | } 55 | }]) 56 | 57 | if (viewCountResponse.length === 0) { 58 | viewCountResponse = [{ total: 0 }] 59 | } 60 | 61 | const viewCount = viewCountResponse[0].total 62 | 63 | let bandwidthUsedResponse = await ItemModel.aggregate([{ $match: {} }, { 64 | $group: { 65 | _id: null, 66 | total: { 67 | $sum: { $multiply: ["$metadata.views", "$metadata.size"] } 68 | } 69 | } 70 | }]) 71 | 72 | if (bandwidthUsedResponse.length === 0) { 73 | bandwidthUsedResponse = [{ total: 0 }] 74 | } 75 | 76 | const bandwidthUsed = bandwidthUsedResponse[0].total 77 | 78 | const user = new StatModel({ 79 | time: new Date(), 80 | field: 'users', 81 | value: userCount 82 | }) 83 | 84 | const item = new StatModel({ 85 | time: new Date(), 86 | field: 'items', 87 | value: itemCount 88 | }) 89 | 90 | const views = new StatModel({ 91 | time: new Date(), 92 | field: 'views', 93 | value: viewCount 94 | }) 95 | 96 | const bandwidth = new StatModel({ 97 | time: new Date(), 98 | field: 'bandwidth', 99 | value: bandwidthUsed 100 | }) 101 | 102 | await Promise.all([ 103 | user.save(), 104 | item.save(), 105 | views.save(), 106 | bandwidth.save() 107 | ]) 108 | } 109 | } 110 | 111 | module.exports = Stats -------------------------------------------------------------------------------- /modules/Store.js: -------------------------------------------------------------------------------- 1 | const StoreModel = require('../models/Store') 2 | const Minio = require('./Minio') 3 | 4 | class Store { 5 | constructor(store) { 6 | this.store = store 7 | } 8 | 9 | static async create(storeParams) { 10 | const store = new StoreModel(storeParams) 11 | 12 | await store.save() 13 | 14 | return new Store(store) 15 | } 16 | 17 | id() { 18 | return this.store._id 19 | } 20 | 21 | async getStream(range) { 22 | if (this.store.store === 'minio') { 23 | const minio = new Minio() 24 | const stream = await minio.download(this.store.bucket, this.store.filepath, range) 25 | 26 | return stream 27 | } 28 | } 29 | 30 | async getStat() { 31 | if (this.store.store === 'minio') { 32 | const minio = new Minio() 33 | const stats = await minio.stat(this.store.bucket, this.store.filepath) 34 | 35 | return stats 36 | } 37 | } 38 | 39 | async getFolder() { 40 | return this.store.folder 41 | } 42 | 43 | async getModel() { 44 | return this.store 45 | } 46 | 47 | async setStream(stream) { 48 | if (this.store.store === 'minio') { 49 | const minio = new Minio() 50 | await minio.save({ 51 | bucket: this.store.bucket, 52 | filepath: this.store.filepath 53 | }, stream) 54 | } 55 | } 56 | } 57 | 58 | module.exports = Store -------------------------------------------------------------------------------- /modules/User.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid') 2 | const memoize = require('memoizee') 3 | 4 | const UserModel = require('../models/User') 5 | 6 | class User { 7 | constructor(user) { 8 | this.user = user 9 | } 10 | 11 | static async fromApiKey(key) { 12 | const uploaderUser = await UserModel.findOne({ apiKey: key }) 13 | 14 | if (!uploaderUser) { 15 | return undefined 16 | } 17 | 18 | let user = { 19 | uploader: uploaderUser 20 | } 21 | 22 | return new User(user) 23 | } 24 | 25 | static async fromUser(genericUser) { 26 | let user = { 27 | generic: genericUser, 28 | uploader: await UserModel.findOne({ user: genericUser._id }) 29 | } 30 | 31 | if (!user.uploader) { 32 | console.log('Updating user model') 33 | user.uploader = new UserModel({ 34 | user: genericUser._id, 35 | apiKey: uuidv4() 36 | }) 37 | 38 | await user.uploader.save() 39 | } 40 | 41 | return new User(user) 42 | } 43 | 44 | static fromReq(req) { 45 | if (req.body && req.body.apiKey && !req.user) { 46 | return User.fromApiKey(req.body.apiKey) 47 | } 48 | 49 | return req.user 50 | } 51 | 52 | getIdentifier() { 53 | // console.log(this.user) 54 | 55 | if (this.user) { 56 | return this.user._id || this.user.uploader.user 57 | } 58 | } 59 | 60 | getApiKey() { 61 | return this.user.uploader.apiKey 62 | } 63 | } 64 | 65 | User.fromUserCached = memoize(User.fromUser, { 66 | promise: true, 67 | normalizer: args => args[0]._id, 68 | maxAge: 1000 * 60 * 60, 69 | preFetch: true, 70 | primitive: true 71 | }) 72 | 73 | module.exports = User -------------------------------------------------------------------------------- /modules/Utils.js: -------------------------------------------------------------------------------- 1 | const User = require('../modules/User') 2 | const fs = require('fs') 3 | const qs = require('qs') 4 | 5 | class Utils { 6 | constructor() { } 7 | 8 | static parseExpiry(relativeExpiry) { 9 | if (relativeExpiry === undefined) { 10 | return undefined 11 | } 12 | 13 | const expiry = Number(relativeExpiry) 14 | 15 | if (expiry < 0 || expiry > 60 * 60 * 24 * 365 * 1000) { 16 | expiresAt = undefined 17 | } 18 | 19 | const expiresAt = new Date() 20 | expiresAt.setSeconds(expiresAt.getSeconds() + expiry) 21 | 22 | return expiresAt 23 | } 24 | 25 | static getFirstLines(stream, lineCount) { 26 | return new Promise((resolve, reject) => { 27 | let data = '' 28 | let lines = [] 29 | 30 | stream.on('data', chunk => { 31 | data += chunk.toString('utf-8') 32 | lines = data.split('\n') 33 | 34 | if (lines.length > lineCount + 1) { 35 | stream.destroy() 36 | lines = lines.slice(0, lineCount) 37 | resolve(lines.join('\n')) 38 | } 39 | }) 40 | 41 | stream.on('error', err => reject(err)) 42 | stream.on('end', () => resolve(lines.join('\n'))) 43 | }) 44 | } 45 | 46 | static move(oldPath, newPath) { 47 | return new Promise((res, rej) => { 48 | fs.rename(oldPath, newPath, function (err) { 49 | if (err) { 50 | if (err.code === 'EXDEV') { 51 | copy() 52 | } else { 53 | rej(err) 54 | } 55 | return 56 | } 57 | res() 58 | }) 59 | 60 | function copy() { 61 | var readStream = fs.createReadStream(oldPath) 62 | var writeStream = fs.createWriteStream(newPath) 63 | 64 | readStream.on('error', rej) 65 | writeStream.on('error', rej) 66 | 67 | readStream.on('close', function () { 68 | fs.unlink(oldPath, err => { 69 | if (err) return rej(err) 70 | return res() 71 | }) 72 | }) 73 | 74 | readStream.pipe(writeStream) 75 | } 76 | }) 77 | } 78 | 79 | static addQuery(url, query, value) { 80 | let parts = url.split('?') 81 | 82 | const extractedQuery = qs.parse(parts[1]) 83 | extractedQuery[query] = value 84 | 85 | return parts[0] + '?' + qs.stringify(extractedQuery) 86 | } 87 | 88 | static timeout(promise, time) { 89 | let timeout = new Promise((resolve, reject) => { 90 | let id = setTimeout(() => { 91 | clearTimeout(id); 92 | reject('Timed out in ' + time + 'ms.') 93 | }, time) 94 | }) 95 | 96 | return Promise.race([ 97 | promise, 98 | timeout 99 | ]) 100 | } 101 | 102 | static pause(time) { 103 | return new Promise(resolve => setTimeout(resolve, time)) 104 | } 105 | } 106 | 107 | module.exports = Utils -------------------------------------------------------------------------------- /modules/VirusTotal.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | const nvt = require('node-virustotal') 3 | 4 | class VirusTotal { 5 | constructor() { 6 | this.instance = nvt.makeAPI() 7 | this.instance.setDelay(15000) 8 | } 9 | 10 | async scan(item) { 11 | const stream = await store.getItemStream() 12 | if (!config.get('clamav.enabled')) { 13 | return { 14 | disabled: true 15 | } 16 | } 17 | 18 | const form = new FormData() 19 | form.append('file', stream, { filename }) 20 | 21 | return fetch(`${config.get('clamav.url')}/scan`, { 22 | method: 'POST', 23 | body: form, 24 | headers: form.getHeaders() 25 | }) 26 | .then(res => res.text()) 27 | .then(res => { 28 | try { 29 | return hjson.parse(res) 30 | } catch (e) { 31 | console.log(res) 32 | console.error('invalid clamav response', res) 33 | } 34 | }) 35 | } 36 | } 37 | 38 | module.exports = VirusTotal 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@femto-dev/web-file-uploader", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@femto-apps/authentication-consumer": "^1.0.2", 14 | "@femto-apps/config": "^3.0.1", 15 | "@uppy/core": "^1.10.5", 16 | "@uppy/dashboard": "^1.9.0", 17 | "@uppy/xhr-upload": "^1.5.11", 18 | "archiver": "^4.0.1", 19 | "body-parser": "^1.19.0", 20 | "buffer-to-stream": "^1.0.0", 21 | "canvas": "^2.6.1", 22 | "chart.js": "^2.9.3", 23 | "clamscan": "^1.3.1", 24 | "combined-stream": "^1.0.8", 25 | "compression": "^1.7.4", 26 | "connect-redis": "^4.0.4", 27 | "cookie-parser": "^1.4.5", 28 | "cors": "^2.8.5", 29 | "dateformat": "^3.0.3", 30 | "errorhandler": "^1.5.1", 31 | "execa": "^4.0.2", 32 | "express": "^4.17.1", 33 | "express-google-analytics": "^1.0.2", 34 | "express-session": "^1.17.1", 35 | "fetch-timeout": "0.0.2", 36 | "file-type": "^14.6.1", 37 | "form-data": "^3.0.0", 38 | "isbinaryfile": "^4.0.6", 39 | "language-detect": "^1.1.0", 40 | "lodash": "^4.17.19", 41 | "memoizee": "^0.4.14", 42 | "minio": "^7.0.16", 43 | "mongoose": "^5.10.14", 44 | "mongoose-autopopulate": "^0.12.2", 45 | "morgan": "^1.10.0", 46 | "multer": "^1.4.2", 47 | "multistream": "^4.0.0", 48 | "node-fetch": "^2.6.1", 49 | "node-virustotal": "^3.32.0", 50 | "nodemailer": "^6.4.15", 51 | "number-abbreviate": "^2.0.0", 52 | "p-limit": "^3.0.2", 53 | "p-memoize": "^4.0.0", 54 | "pretty-bytes": "^5.3.0", 55 | "pug": "^3.0.0", 56 | "puppeteer": "^3.3.0", 57 | "qs": "^6.9.4", 58 | "redis": "^3.0.2", 59 | "resolve-path": "^1.4.0", 60 | "send-ranges": "^3.0.0", 61 | "sharp": "^0.25.3", 62 | "smartcrop-sharp": "^2.0.3", 63 | "stream-to-array": "^2.3.0", 64 | "tmp": "^0.2.1", 65 | "tunnel-ssh": "^4.1.4", 66 | "universal-analytics": "^0.4.23", 67 | "uuid": "^8.1.0", 68 | "valid-url": "^1.0.9" 69 | }, 70 | "devDependencies": { 71 | "css-loader": "^3.5.3", 72 | "style-loader": "^1.2.1", 73 | "webpack": "^4.43.0", 74 | "webpack-cli": "^3.3.11" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | .thumb-link { 2 | line-height: 0; 3 | } 4 | 5 | grid.uploads { 6 | display: grid; 7 | grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); 8 | grid-gap: 5px; 9 | } 10 | 11 | grid.uploads img.thumb { 12 | max-width: 100%; 13 | border-radius: 5px; 14 | } 15 | 16 | buffer { 17 | display: block; 18 | height: 25px; 19 | } 20 | 21 | .spoiler:not(:hover) { 22 | color: #2b2b2b; 23 | background-color: #2b2b2b; 24 | } 25 | 26 | .spoiler:not(:hover) > * { 27 | opacity: 0; 28 | } -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #9f00a7 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /public/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Femto", 3 | "short_name": "Femto", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "#ffffff", 12 | "background_color": "#ffffff", 13 | "display": "standalone" 14 | } 15 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/images/logo.png -------------------------------------------------------------------------------- /public/images/new_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/images/new_screenshot.png -------------------------------------------------------------------------------- /public/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/images/screenshot.png -------------------------------------------------------------------------------- /public/images/unknown_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femto-apps/web-file-uploader/c22d989c5bf1ecf9a12f5ae679efadfefb00055e/public/images/unknown_file.png -------------------------------------------------------------------------------- /scripts/findLargeFiles.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const config = require('@femto-apps/config') 3 | const prettyBytes = require('pretty-bytes') 4 | 5 | const Item = require('../modules/Item.js') 6 | const ItemModel = require('../models/Item.js') 7 | const ShortModel = require('../models/Short.js') 8 | const StoreModel = require('../models/Store.js') 9 | 10 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | useFindAndModify: false, 14 | useCreateIndex: true 15 | }) 16 | 17 | async function init() { 18 | const items = ItemModel.find({}).sort({ 'metadata.size': -1 }).limit(50).cursor() 19 | 20 | for (let dbItem = await items.next(); dbItem != null; dbItem = await items.next()) { 21 | const item = new Item(dbItem) 22 | console.log(`[+] [size: ${prettyBytes(dbItem.metadata.size)}] [type: ${dbItem.metadata.filetype}] [views: ${dbItem.metadata.views}] [link: https://femto.pw/${(await item.getCanonical()).short} ]`) 23 | } 24 | } 25 | 26 | init() 27 | -------------------------------------------------------------------------------- /scripts/findUnusedFiles.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const config = require('@femto-apps/config') 3 | const prettyBytes = require('pretty-bytes') 4 | 5 | const Item = require('../modules/Item.js') 6 | const ItemModel = require('../models/Item.js') 7 | const ShortModel = require('../models/Short.js') 8 | const StoreModel = require('../models/Store.js') 9 | 10 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | useFindAndModify: false, 14 | useCreateIndex: true 15 | }) 16 | 17 | async function init() { 18 | const Minio = require('minio') 19 | 20 | const client = new Minio.Client(Object.assign({}, config.get('minio'), { endPoint: config.get('minio.host') })) 21 | const stream = client.listObjectsV2('items', '', true) 22 | 23 | stream.on('data', async obj => { 24 | const store = await StoreModel.findOne({ 'filepath': obj.name }) 25 | if (!store) { 26 | console.log(`[+] [size: ${prettyBytes(obj.size)}] [name: ${obj.name}]`) 27 | } 28 | }) 29 | stream.on('error', err => { 30 | console.log(err) 31 | }) 32 | } 33 | 34 | init() 35 | -------------------------------------------------------------------------------- /scripts/fixCanonical.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const config = require('@femto-apps/config') 3 | const pLimit = require('p-limit') 4 | 5 | const Utils = require('../modules/Utils') 6 | 7 | const Item = require('../modules/Item.js') 8 | const ItemModel = require('../models/Item.js') 9 | const ShortModel = require('../models/Short.js') 10 | 11 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 12 | useNewUrlParser: true, 13 | useUnifiedTopology: true, 14 | useFindAndModify: false, 15 | useCreateIndex: true 16 | }) 17 | 18 | async function init() { 19 | // const numItems = await ItemModel.count({ 'metadata.virus.run': false, "metadata.filetype": { "$ne": "url" } }) 20 | 21 | // const random = Math.floor(Math.random() * (numItems - 20)) 22 | // const items = await ItemModel.find({ 'metadata.virus.run': false }).skip(random) 23 | // const items = ItemModel.find({ 'metadata.filetype': { '$ne': 'thumb' } }).cursor() 24 | const items = ItemModel.find({ _id: '5f5cb8a023ac8762ec024eba' }).cursor() 25 | 26 | let i = 0 27 | for (let dbItem = await items.next(); dbItem != null; dbItem = await items.next()) { 28 | i += 1 29 | const item = new Item(dbItem) 30 | 31 | console.log(`= [${i} / ${items.length}] [name: ${dbItem.name.original}] [id: ${dbItem._id}] [size: ${dbItem.metadata.size}]`) 32 | 33 | const canonical = await item.getCanonical() 34 | 35 | console.log('canonical', canonical) 36 | 37 | if (!canonical) { 38 | console.log(item) 39 | console.log('deleting') 40 | await ItemModel.deleteOne({ _id: item._id }) 41 | } 42 | } 43 | 44 | mongoose.disconnect() 45 | } 46 | 47 | init() 48 | -------------------------------------------------------------------------------- /scripts/fixSizes.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const config = require('@femto-apps/config') 3 | const pLimit = require('p-limit') 4 | 5 | const Utils = require('../modules/Utils') 6 | 7 | const Item = require('../modules/Item.js') 8 | const ItemModel = require('../models/Item.js') 9 | 10 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 11 | useNewUrlParser: true, 12 | useUnifiedTopology: true, 13 | useFindAndModify: false, 14 | useCreateIndex: true 15 | }) 16 | 17 | async function init() { 18 | // const numItems = await ItemModel.count({ 'metadata.virus.run': false, "metadata.filetype": { "$ne": "url" } }) 19 | 20 | // const random = Math.floor(Math.random() * (numItems - 20)) 21 | // const items = await ItemModel.find({ 'metadata.virus.run': false }).skip(random) 22 | const items = await ItemModel.find({ 'metadata.filetype': { '$ne': 'url' } }).skip(31000) 23 | 24 | let i = 0 25 | 26 | for (let dbItem of items) { 27 | 28 | const item = new Item(dbItem) 29 | const stat = await item.getItemStat() 30 | 31 | console.log(`= [${i} / ${items.length}] [name: ${dbItem.name.original}] [id: ${dbItem._id}] [size: ${dbItem.metadata.size}] [realSize: ${stat.size}]`) 32 | 33 | item.item.metadata.size = stat.size 34 | await item.item.save() 35 | 36 | i += 1 37 | } 38 | 39 | mongoose.disconnect() 40 | } 41 | 42 | init() 43 | -------------------------------------------------------------------------------- /scripts/identifyFileFromBuffer.js: -------------------------------------------------------------------------------- 1 | const fileType = require('file-type') 2 | 3 | const buffer = Buffer.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,6,106,0,0,4,71,8,2,0,0,0,124,139,171,120,0,0,0,1,115,82,71,66,0,174,206,28,233,0,0,0,4,103,65,77,65,0,0,177,143,11,252,97,5,0,0,0,9,112,72,89,115,170,0,22,37,0,0,22,37,1,73,82,36,240,170,170,255,165,171,68,69,84,120,94,236,189,63,142,100,205,113,189,45,139,32,32,128,144,65,131,2,8,208,249,237,64,160,243,110,64,123,144,35,143,30,215,32,139,62,183,193,13,112,3,116,181,3,174,65,107,248,190,168,251,220,62,125,42,34,51,111,222,91,85,221,61,61,249,32,145,136,56,113,34,50,235,79,87,207,20,230,37,255,229,255,91,44,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,29,214,215,103,139,197,98,177,88,44,22,139,197,98,177,88,44,22,139,69,151,245,245,217,98,177,88,44,22,139,197,98,177,88,44,22,139,197,98,209,101,125,125,182,88,44,22,139,197,98,177,88,44,22,139,197,98,177,88,116,89,95,159,45,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,93,214,215,103,139,197,98,177,88,44,22,139,197,98,177,88,44,22,139,69,151,245,245,217,98,177,88,44,22,139,197,98,177,88,44,22,139,197,98,209,101,125,125,182,88,44,22,139,197,98,177,88,44,22,139,197,98,177,88,116,89,95,159,45,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,93,214,215,103,139,197,98,177,88,44,22,139,197,98,177,88,44,22,139,69,151,245,245,217,98,177,88,44,22,139,197,98,177,88,44,22,139,197,98,209,101,125,125,182,88,44,22,139,197,98,177,88,44,22,139,197,98,177,88,116,89,95,159,45,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,93,214,215,103,139,197,98,177,88,44,22,139,197,98,177,88,44,22,139,69,151,245,245,217,98,177,88,44,22,139,197,98,177,88,44,22,139,197,98,209,101,125,125,182,88,44,22,139,197,98,177,88,44,22,139,197,98,177,88,116,89,95,159,45,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,93,214,215,103,139,197,98,177,88,44,22,139,197,98,177,88,44,22,139,69,151,245,245,217,98,177,88,44,22,139,197,98,177,88,44,22,139,197,98,209,101,125,125,182,88,44,22,139,197,98,177,88,44,22,139,197,98,177,88,116,89,95,159,45,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,93,214,215,103,139,197,98,177,88,44,22,139,197,98,177,88,44,22,139,69,151,245,245,217,98,177,88,44,22,139,197,98,177,88,44,22,139,197,98,209,101,125,125,182,88,44,22,139,197,98,177,88,44,22,139,197,98,177,88,116,89,95,159,45,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,93,214,215,103,139,197,98,177,88,44,22,139,197,98,177,88,44,22,139,69,151,245,245,217,98,177,88,44,22,139,197,98,177,88,44,22,139,197,98,209,101,125,125,182,88,44,22,139,197,98,177,88,44,22,139,197,98,177,88,116,89,95,159,45,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,93,214,215,103,139,197,98,177,88,44,22,139,197,98,177,88,44,22,139,69,151,245,245,217,98,177,88,44,22,139,197,98,177,88,44,22,139,197,98,209,101,125,125,182,88,44,22,139,197,98,177,88,44,22,139,197,98,177,88,116,89,95,159,45,22,139,197,98,177,88,44,22,139,197,98,177,88,44,22,93,214,215,103,139,111,203,255,253,254,255,105,247,96,177,88,44,22,139,197,98,177,88,44,22,139,197,98,158,245,245,217,226,59,243,127,191,255,127,254,245,153,22,202,98,177,88,44,22,139,197,98,177,88,44,22,139,197,33,235,235,179,197,247,193,191,26,211,55,101,90,73,196,182,88,44,22,139,197,98,177,88,44,22,139,197,98,49,102,125,125,182,248,129,209,183,96,254,117,24,177,246,222,218,188,139,197,98,177,88,44,22,95,11,254,148,226,251,12,79,252,179,205,96,84,42,205,56,221,67,60,63,100,177,88,44,22,139,175,195,250,250,108,241,99,16,127,180,106,254,97,75,123,77,199,43,108,139,197,98,177,88,44,22,31,198,204,31,63,220,115,234,143,43,61,51,186,239,207,37,102,54,199,30,158,229,141,135,230,197,98,177,88,44,62,157,245,245,217,226,147,225,15,76,105,215,159,162,106,16,184,1,80,16,181,143,215,214,183,88,44,22,139,197,226,107,241,145,191,163,31,249,35,193,217,198,11,254,113,203,161,65,204,216,220,51,240,167,18,105,236,181,165,231,36,5,137,44,68,144,146,244,197,98,177,88,44,62,139,245,245,217,226,227,24,252,1,136,63,36,185,65,177,74,85,33,13,82,213,119,95,73,143,96,177,88,44,22,139,197,23,225,242,175,102,53,190,244,151,251,35,195,163,247,233,119,99,224,179,38,55,135,104,248,228,17,213,54,104,111,150,34,213,34,213,190,88,44,22,139,197,231,178,190,62,91,140,24,252,121,69,165,201,63,211,84,91,157,16,129,22,74,69,85,57,149,250,78,32,5,92,95,44,22,139,197,98,241,149,185,240,43,251,218,111,249,83,93,152,99,191,118,214,33,103,47,115,232,199,131,109,108,86,117,220,226,138,108,215,80,111,26,66,234,213,164,44,22,139,197,98,241,241,172,175,207,22,199,212,63,172,184,162,63,211,36,146,152,90,216,89,82,28,247,160,56,169,74,64,236,186,211,20,23,139,197,98,177,88,124,41,198,191,175,155,191,223,247,232,204,239,250,212,165,125,64,211,48,238,170,167,204,51,227,79,243,207,30,113,129,11,71,164,150,230,4,46,95,75,82,122,134,197,98,177,88,44,62,140,245,245,217,162,203,229,63,166,208,229,189,138,153,217,92,94,77,177,210,20,108,149,59,84,93,44,22,139,197,226,75,241,89,191,161,78,29,138,121,254,170,242,167,61,49,40,137,168,206,216,6,168,241,69,19,174,205,199,28,251,133,174,67,124,236,169,249,79,225,240,196,122,55,15,136,165,4,169,234,75,34,134,197,98,177,88,44,62,158,245,245,217,247,103,240,71,141,94,169,234,174,52,171,201,224,74,179,228,235,176,4,85,175,193,98,177,88,44,22,147,124,228,239,14,255,229,245,82,56,197,247,25,228,140,192,99,130,38,84,147,167,182,200,86,75,142,108,164,149,90,74,74,164,117,136,68,161,180,150,130,166,24,244,244,47,194,115,239,246,117,30,41,55,225,201,87,188,85,22,139,197,98,177,248,4,214,215,103,223,153,193,31,50,38,255,252,225,182,212,18,41,74,218,125,245,170,17,131,235,82,36,106,119,122,250,98,177,88,44,126,30,226,183,192,119,250,69,48,243,112,102,30,175,60,213,60,110,167,170,157,181,85,26,12,74,77,14,7,2,134,158,237,172,238,184,199,79,137,93,165,170,8,137,205,234,226,185,232,25,214,115,78,26,240,252,187,178,88,44,22,139,197,71,178,190,62,251,206,164,63,115,164,160,71,114,214,29,34,150,152,226,173,254,174,55,21,95,131,146,22,19,22,139,197,98,177,224,151,130,126,53,60,242,59,226,179,122,47,112,237,56,186,102,122,229,57,117,208,204,124,121,122,102,41,181,52,32,153,35,61,213,222,35,205,33,245,181,23,22,47,131,39,89,187,175,173,190,88,44,22,139,197,231,176,190,62,251,81,225,207,16,250,147,68,253,35,5,127,206,112,91,221,193,99,33,209,131,26,251,46,168,250,170,98,211,214,92,219,200,197,98,177,88,124,91,206,126,212,39,255,133,95,22,248,213,53,223,238,45,243,93,224,254,26,15,6,202,64,218,99,208,126,216,43,48,79,250,171,173,215,136,62,24,59,115,162,134,84,51,98,213,123,184,179,25,51,141,180,198,155,101,241,42,244,12,243,108,175,231,124,177,88,44,22,95,132,245,245,217,51,121,245,111,247,11,243,189,37,253,249,131,84,11,69,37,15,48,84,49,237,205,84,49,200,160,149,210,180,182,166,197,98,177,88,124,16,159,245,193,123,225,92,181,16,156,157,112,185,61,57,35,157,236,149,109,208,82,75,164,190,207,32,103,4,44,210,30,201,48,110,241,18,177,239,77,6,165,196,35,67,78,245,186,66,92,149,20,164,133,190,120,5,60,189,122,158,215,179,189,88,44,22,139,175,192,250,250,236,153,232,215,124,143,90,29,252,177,160,103,6,53,178,16,155,36,167,210,173,184,235,41,72,59,36,131,112,81,30,80,90,69,41,158,122,176,21,23,139,197,98,241,113,60,242,217,171,222,201,33,110,35,190,124,250,217,198,167,28,116,109,200,184,235,145,106,143,11,93,209,242,220,155,92,187,249,101,210,113,164,190,39,146,193,3,98,79,165,44,158,136,63,207,4,129,139,235,105,95,44,22,139,197,87,96,125,125,246,28,122,191,215,253,119,63,65,48,243,135,128,240,96,243,221,145,1,82,26,40,85,169,238,142,148,90,10,146,24,41,138,235,82,36,166,106,77,165,16,72,100,81,90,44,22,139,197,5,248,20,253,248,207,210,107,39,94,232,242,150,83,237,207,125,78,98,26,107,207,159,65,154,118,121,248,115,111,245,1,204,92,120,236,121,240,33,211,174,33,74,125,81,90,60,157,244,12,43,150,178,88,44,22,139,197,231,178,190,62,27,209,251,133,173,223,232,226,212,47,120,111,247,24,122,67,92,79,237,44,233,236,32,69,11,93,184,66,44,155,7,183,242,6,162,43,149,84,37,149,232,85,47,85,125,177,88,44,22,103,73,159,159,51,31,167,143,127,228,198,4,31,50,63,80,206,11,119,72,45,103,39,92,110,239,57,165,159,189,137,184,220,248,157,24,60,189,79,124,126,210,168,58,153,227,124,185,136,103,241,8,245,105,212,211,75,73,6,5,139,197,98,177,88,124,46,235,235,179,17,233,247,119,226,154,174,106,4,238,116,157,0,198,41,132,200,82,74,0,164,242,120,186,213,111,120,12,85,17,189,18,51,169,42,0,137,236,117,109,174,197,226,59,51,255,62,63,251,19,241,21,126,130,126,242,159,226,151,62,252,15,120,110,227,8,173,93,122,61,58,139,224,131,79,119,252,220,167,220,225,179,30,200,167,195,3,31,63,252,102,117,220,168,170,22,250,60,189,46,137,4,216,82,156,150,204,139,107,248,179,167,184,247,148,134,190,158,237,197,98,177,88,124,29,214,215,103,35,6,191,179,199,191,206,189,170,223,253,190,59,201,188,71,27,106,97,33,6,210,73,3,143,29,111,84,76,224,186,2,215,69,85,28,175,18,51,36,233,82,20,251,194,182,88,124,87,244,62,127,250,187,253,233,99,7,163,198,103,141,245,193,88,144,141,229,226,55,195,31,224,60,175,123,42,210,228,7,15,162,253,117,183,29,115,225,220,104,121,238,109,63,235,177,59,95,225,14,135,212,75,186,66,28,123,90,42,93,70,237,26,232,164,106,53,44,94,65,122,170,215,51,191,88,44,22,139,175,201,250,250,236,10,250,189,158,126,217,167,192,161,197,61,158,138,158,152,130,32,57,83,10,136,190,246,194,27,85,73,200,224,78,141,170,59,72,241,32,197,73,137,96,177,248,222,204,191,213,47,255,68,140,27,207,142,157,241,207,156,168,93,230,212,229,41,182,164,236,209,215,67,119,59,188,36,134,83,143,229,193,7,254,145,103,5,143,79,8,98,200,181,57,79,57,253,167,98,254,25,27,56,15,95,175,11,189,18,101,208,174,181,213,143,169,206,102,47,98,175,212,212,23,215,208,243,233,193,86,217,105,138,139,197,98,177,88,124,29,190,231,215,103,189,95,189,210,35,240,88,129,235,65,138,149,18,147,106,39,80,236,74,144,210,32,165,149,106,64,145,238,134,113,201,211,32,85,83,144,104,234,105,2,203,99,173,166,88,215,54,105,177,248,81,153,127,15,15,222,240,148,6,134,202,140,19,207,161,211,207,29,183,156,210,67,148,78,32,197,75,224,10,177,43,95,19,191,225,43,174,122,97,38,87,122,197,101,190,62,215,158,174,61,250,70,240,160,6,15,237,208,16,212,106,211,127,118,8,132,174,229,169,170,73,84,172,181,25,15,144,205,253,205,246,166,51,168,206,197,101,244,204,55,159,228,245,84,47,22,139,197,226,235,243,13,191,62,171,191,158,149,186,78,234,34,1,164,52,168,134,61,218,32,77,98,147,52,199,113,93,113,18,93,247,146,131,238,78,79,161,215,155,168,182,58,214,21,165,205,184,174,173,239,110,200,98,81,249,159,255,249,159,61,106,225,213,136,199,230,215,209,124,15,159,122,111,187,147,120,190,183,73,106,111,206,236,29,33,115,93,24,2,143,131,241,51,223,108,39,245,96,176,63,133,195,81,243,103,185,51,98,22,49,98,15,57,231,153,156,124,150,107,3,117,153,103,221,71,115,46,12,212,101,72,131,103,221,234,43,51,120,140,42,29,122,124,239,17,85,173,93,186,39,233,164,61,115,143,230,124,141,82,53,197,55,211,27,41,117,84,234,181,204,143,90,204,51,120,26,163,68,213,131,173,178,88,44,22,139,197,87,231,251,124,125,54,254,101,236,186,7,196,66,162,150,244,26,16,75,129,102,234,98,196,172,61,127,67,74,45,37,48,48,68,241,86,233,210,52,84,81,3,129,24,177,198,224,138,116,137,3,157,229,34,158,197,119,165,247,197,150,139,77,67,208,107,116,157,216,149,121,30,121,251,29,246,98,168,251,19,137,129,90,187,180,49,72,137,83,75,138,211,218,11,27,158,70,60,126,218,49,215,9,44,98,223,107,16,251,35,248,132,193,52,157,248,8,147,19,206,30,116,237,98,135,93,151,31,47,141,151,219,157,167,12,9,158,53,231,195,240,11,79,94,30,91,236,3,255,228,168,121,234,161,245,136,166,146,68,111,239,249,125,245,116,22,213,38,169,234,105,47,94,188,148,230,43,178,158,255,197,98,177,88,252,136,124,171,127,125,22,191,140,181,72,165,179,123,64,220,76,9,2,215,19,148,82,181,103,14,6,165,30,117,126,32,165,89,5,116,119,18,52,73,182,216,89,73,169,6,150,244,90,106,174,166,141,246,197,23,225,218,55,80,193,160,81,165,228,153,57,107,242,62,97,3,165,4,147,188,244,125,56,249,62,127,228,14,244,246,38,52,47,224,45,24,234,242,146,204,21,158,121,32,69,15,188,145,57,117,109,198,27,73,215,218,203,143,193,156,222,52,233,143,156,56,104,244,249,4,167,184,124,165,30,143,63,70,130,199,47,246,244,135,246,185,248,195,121,209,67,155,25,251,244,163,7,3,207,158,85,253,82,34,96,13,68,197,32,157,20,148,38,125,241,10,234,147,220,124,218,67,92,47,199,98,177,88,44,126,116,190,195,215,103,250,125,172,223,205,4,90,40,201,16,59,32,106,237,234,70,83,73,65,144,60,206,164,45,81,157,161,52,71,245,102,38,127,69,134,230,174,21,169,168,85,22,213,128,88,138,7,44,143,149,110,150,197,1,254,125,196,143,133,110,126,251,102,101,248,40,168,186,167,250,111,35,238,13,205,84,251,225,27,44,25,78,189,33,47,188,123,39,91,230,39,227,60,244,135,129,181,231,247,120,201,157,138,155,41,79,50,220,158,247,141,40,17,236,133,13,252,160,9,136,158,178,16,85,186,245,188,128,193,228,11,231,210,114,161,241,139,112,234,218,50,215,224,187,50,255,0,113,78,250,101,102,33,142,145,173,231,119,125,48,51,149,6,78,8,131,214,46,25,227,246,90,69,241,29,20,71,160,170,98,167,41,10,74,3,195,226,165,140,95,29,88,175,206,98,177,88,44,126,80,190,213,127,188,201,239,99,2,173,90,77,169,199,177,11,233,196,4,144,210,160,42,23,96,72,236,90,232,206,160,218,244,7,174,55,61,18,35,208,66,9]) 4 | 5 | ;(async () => { 6 | const type = await fileType.fromBuffer(buffer) 7 | 8 | console.log(type) 9 | })() -------------------------------------------------------------------------------- /scripts/identifyFilesWithoutUser.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | const mongoose = require('mongoose') 3 | 4 | const UserModel = require('../models/User.js') 5 | const ItemModel = require('../models/Item.js') 6 | const StoreModel = require('../models/Store.js') 7 | const ShortModel = require('../models/Short.js') 8 | 9 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true, 12 | useFindAndModify: false, 13 | useCreateIndex: true 14 | }) 15 | 16 | async function init() { 17 | const items = await ItemModel.find({ 'user.ip': { $exists: true } }) 18 | 19 | for (let item of items) { 20 | console.log(`${item._id} - [name: ${item.name.original}] [filetype: ${item.metadata.filetype}]`) 21 | } 22 | 23 | mongoose.connection.close() 24 | } 25 | 26 | init() -------------------------------------------------------------------------------- /scripts/identifyUsersFiles.js: -------------------------------------------------------------------------------- 1 | const config = require('@femto-apps/config') 2 | const mongoose = require('mongoose') 3 | 4 | const UserModel = require('../models/User.js') 5 | const ItemModel = require('../models/Item.js') 6 | const StoreModel = require('../models/Store.js') 7 | const ShortModel = require('../models/Short.js') 8 | 9 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true, 12 | useFindAndModify: false, 13 | useCreateIndex: true 14 | }) 15 | 16 | async function init() { 17 | const users = await UserModel.find() 18 | 19 | for (let user of users) { 20 | console.log(`=== ${user._id} (base: ${user.user}) ===`) 21 | for (let item of await ItemModel.find({ 'user._id': user.user })) { 22 | console.log(`${item._id} - [name: ${item.name.original}] [filetype: ${item.metadata.filetype}]`) 23 | } 24 | } 25 | 26 | mongoose.connection.close() 27 | } 28 | 29 | init() -------------------------------------------------------------------------------- /scripts/migrateData.js: -------------------------------------------------------------------------------- 1 | const Minio = require('minio') 2 | const path = require('path') 3 | const { v4: uuidv4 } = require('uuid') 4 | const fetch = require('node-fetch') 5 | const config = require('@femto-apps/config') 6 | const mongoose = require('mongoose') 7 | const toArray = require('stream-to-array') 8 | const pLimit = require('p-limit') 9 | 10 | const newUser = require('../modules/User.js') 11 | const newCollection = require('../modules/Collection.js') 12 | const newItemModel = require('../modules/Item.js') 13 | const newStore = require('../modules/Store.js') 14 | const newShort = require('../modules/Short.js') 15 | const newShortModel = require('../models/Short.js') 16 | const Types = require('../types') 17 | 18 | const minioOptions = { 19 | minio: { 20 | endPoint: config.get('minio.host'), 21 | port: config.get('minio.port'), 22 | useSSL: false, 23 | accessKey: config.get('minio.accessKey'), 24 | secretKey: config.get('minio.secretKey') 25 | } 26 | } 27 | 28 | const minimalItemConnection = mongoose.createConnection(config.get('mongo.uri') + 'minimal_design', { 29 | useNewUrlParser: true, 30 | useUnifiedTopology: true, 31 | useFindAndModify: false, 32 | useCreateIndex: true 33 | }) 34 | 35 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 36 | useNewUrlParser: true, 37 | useUnifiedTopology: true, 38 | useFindAndModify: false, 39 | useCreateIndex: true 40 | }) 41 | 42 | const MinimalItem = minimalItemConnection.model('Item', new mongoose.Schema({ transfer: String }, { strict: false })) 43 | 44 | const client = new Minio.Client(minioOptions.minio) 45 | 46 | const pause = t => new Promise(resolve => setTimeout(resolve, t)) 47 | 48 | async function convertUrl(original) { 49 | const trueOriginal = original 50 | original = JSON.parse(JSON.stringify(original)) 51 | 52 | if (await newShort.get(original.name.short)) { 53 | // console.log('already handled', original._id, original.name.short) 54 | return 55 | } 56 | 57 | if (!original.user) { 58 | original.user = {} 59 | } 60 | 61 | console.log('start folder') 62 | const folder = (await newCollection.fromReq({ 63 | user: original.user._id ? new newUser({ _id: original.user._id }) : undefined, 64 | ip: original.user.ip 65 | })).path 66 | console.log('end folder') 67 | 68 | console.log('detected type') 69 | 70 | const item = await newItemModel.create({ 71 | name: { 72 | original: original.name.original, 73 | }, 74 | metadata: { 75 | filetype: 'url' 76 | }, 77 | user: { _id: original.user._id, ip: original.user.ip } 78 | }) 79 | 80 | console.log('created item') 81 | 82 | const shortItem = await newShort.createReference(original.name.short, item.item) 83 | await item.setCanonical(shortItem) 84 | 85 | console.log(item) 86 | console.log(shortItem) 87 | 88 | console.log('finished') 89 | 90 | trueOriginal.transfer = 'success' 91 | trueOriginal.markModified('transfer') 92 | await trueOriginal.save() 93 | } 94 | 95 | async function convertItem(original, force = false) { 96 | const trueOriginal = original 97 | original = JSON.parse(JSON.stringify(original)) 98 | 99 | if (await newShort.get(original.name.short)) { 100 | if (!force) { 101 | console.log('already handled', original._id, original.name.short) 102 | return 103 | } 104 | 105 | const rem = await newShortModel.remove({ short: original.name.short }) 106 | console.log(rem) 107 | } 108 | 109 | console.log('original', original) 110 | 111 | const originalName = original.name.original 112 | const extension = originalName.slice((originalName.lastIndexOf(".") - 1 >>> 0) + 2) 113 | 114 | console.log('grabbing data stream') 115 | // download file and reupload it to minio 116 | 117 | const fetchResponse = await fetch(`http://localhost:7983/${original.name.short}?download`) 118 | .catch(e => { 119 | return 'failed' 120 | }) 121 | 122 | if (fetchResponse === 'failed' || !fetchResponse.ok) { 123 | trueOriginal.transfer = 'failed' 124 | trueOriginal.markModified('transfer') 125 | await trueOriginal.save() 126 | console.log('pausing for 10s') 127 | await pause(10000) 128 | 129 | return 'failed' 130 | } 131 | const dataStream = await fetchResponse.body 132 | // console.log(dataStream) 133 | 134 | console.log('finished fetch') 135 | 136 | const bucket = 'items' 137 | 138 | console.log('start folder') 139 | const folder = (await newCollection.fromReq({ 140 | user: original.user._id ? new newUser({ _id: original.user._id }) : undefined, 141 | ip: original.user.ip 142 | })).path 143 | console.log('end folder') 144 | 145 | const filename = uuidv4() 146 | const filepath = path.posix.join(folder, filename) 147 | 148 | console.log('got body') 149 | 150 | await client.putObject(bucket, filepath, dataStream, undefined) 151 | 152 | console.log('put object') 153 | 154 | const store = await newStore.create({ 155 | bucket, folder, filename, filepath, store: 'minio' 156 | }) 157 | 158 | let grab = { start: 0, end: 4100 } 159 | if (original.file.length < 4100) { 160 | grab = { start: 0, end: 0 } 161 | } 162 | 163 | let bytes = (await toArray(await store.getStream(grab)))[0] 164 | 165 | console.log(bytes) 166 | 167 | if (bytes === undefined) { 168 | bytes = Buffer.from([]) 169 | } 170 | 171 | const { data } = await Types.detect(store, bytes, { 172 | mimetype: original.file.mime, 173 | encoding: original.file.encoding 174 | }) 175 | 176 | console.log('detected type', data) 177 | 178 | const item = await newItemModel.create({ 179 | name: { 180 | original: originalName, 181 | extension: extension, 182 | filename: originalName.replace(/\.[^/.]+$/, '') 183 | }, 184 | metadata: { 185 | createdAt: original.createdAt, 186 | updatedAt: original.updatedAt, 187 | mime: original.file.mime || 'application/octet-stream', 188 | encoding: original.file.encoding, 189 | filetype: data.filetype, 190 | views: original.views 191 | }, 192 | references: { 193 | storage: store.store 194 | }, 195 | user: { _id: original.user._id, ip: original.user.ip } 196 | }) 197 | 198 | console.log('created item') 199 | 200 | const shortItem = await newShort.createReference(original.name.short, item.item) 201 | await item.setCanonical(shortItem) 202 | 203 | console.log(item) 204 | console.log(shortItem) 205 | 206 | console.log('finished') 207 | 208 | trueOriginal.transfer = 'success' 209 | trueOriginal.markModified('transfer') 210 | await trueOriginal.save() 211 | } 212 | 213 | async function countLeft() { 214 | const itemsLeft = await MinimalItem.countDocuments({ transfer: { '$ne': 'success' } }) 215 | const itemsTotal = await MinimalItem.countDocuments() 216 | 217 | console.log(`Processed ${itemsTotal - itemsLeft} / ${itemsTotal} items (${((itemsTotal - itemsLeft) / itemsTotal * 100).toFixed(1)}%) `) 218 | } 219 | 220 | async function doesExist(url, name, size) { 221 | const fetchResponse = await fetch(url) 222 | .then(res => { 223 | if (Number(res.headers.get('content-length')) !== size) { 224 | return { failed: true, reason: 'size', expected: size, got: Number(res.headers.get('content-length')) } 225 | } 226 | 227 | if (!(res.headers.get('content-disposition').includes(name))) { 228 | return { failed: true, reason: 'name' } 229 | } 230 | 231 | return { success: true } 232 | }) 233 | .catch(e => { 234 | console.log(e) 235 | return { failed: true } 236 | }) 237 | 238 | return fetchResponse 239 | } 240 | 241 | async function migrateItem(item) { 242 | const simple = JSON.parse(JSON.stringify(item)) 243 | 244 | if (!simple.type) { 245 | console.log(simple.name.extension) 246 | if (simple.name.extension === 'png') simple.type = { long: 'image' } 247 | else if (simple.file && simple.file.filetype) simple.type = { long: simple.file.filetype } 248 | else if (['celtx', 'mp4', 'sql', 'jpg', 'gif', 'png', 'mov', 'jpeg', 'jpg-345'].includes(simple.name.extension)) { 249 | const valid = await doesExist(`http://localhost:3005/${simple.name.short}?overrideVirusCheck=true`, simple.name.original, simple.file.length) 250 | if (valid.success) { 251 | console.log('looks valid') 252 | 253 | item.transfer = 'success' 254 | item.markModified('transfer') 255 | await item.save() 256 | return 257 | } 258 | 259 | return convertItem(item, true) 260 | } 261 | else { 262 | console.log(item) 263 | console.log('type not found') 264 | process.exit(0) 265 | } 266 | } 267 | 268 | if (simple.type.long === 'text') { 269 | return convertItem(item, true) 270 | } 271 | 272 | if (simple.type.long === 'image' || simple.type.long === 'video' || simple.type.long === 'audio' || simple.type.long === 'binary') { 273 | // does it already appear to exist? 274 | const valid = await doesExist(`http://localhost:3005/${simple.name.short}?overrideVirusCheck=true`, simple.name.original, simple.file.length) 275 | if (valid.success) { 276 | console.log('looks valid') 277 | 278 | item.transfer = 'success' 279 | item.markModified('transfer') 280 | await item.save() 281 | return 282 | } 283 | 284 | console.log(simple.name.short) 285 | console.log('does not look valid', valid) 286 | 287 | if (valid.reason === 'size') { 288 | return convertItem(item, true) 289 | } 290 | 291 | process.exit(0) 292 | } 293 | 294 | if (simple.type.long === 'url') { 295 | console.log(item) 296 | await convertUrl(item) 297 | return 298 | } 299 | 300 | console.log(item) 301 | 302 | process.exit(0) 303 | } 304 | 305 | async function init() { 306 | await countLeft() 307 | 308 | const remaining = await MinimalItem.find({ transfer: { '$ne': 'success' } }).cursor() 309 | 310 | for (let item = await remaining.next(); item != null; item = await remaining.next()) { 311 | await migrateItem(item) 312 | } 313 | 314 | mongoose.disconnect() 315 | } 316 | 317 | init() 318 | -------------------------------------------------------------------------------- /scripts/runAV.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const config = require('@femto-apps/config') 3 | const pLimit = require('p-limit') 4 | 5 | const Utils = require('../modules/Utils') 6 | 7 | const Item = require('../modules/Item.js') 8 | const ItemModel = require('../models/Item.js') 9 | const ClamAV = require('../modules/ClamAV') 10 | 11 | let running = 0 12 | 13 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 14 | useNewUrlParser: true, 15 | useUnifiedTopology: true, 16 | useFindAndModify: false, 17 | useCreateIndex: true 18 | }) 19 | 20 | async function scanItem(clam, item, i, total) { 21 | // const originalName = item.name.original 22 | const originalName = String(item._id) 23 | const itemClass = new Item(item) 24 | 25 | return clam.scan(originalName, await itemClass.getItemStream()).then(async result => { 26 | const virusResult = { 27 | run: true, 28 | description: result.Description 29 | } 30 | 31 | if (result.Status === 'FOUND') { 32 | virusResult.detected = true 33 | } else if (result.Status === 'OK') { 34 | virusResult.detected = false 35 | } 36 | 37 | await itemClass.setVirus(virusResult) 38 | running -= 1 39 | console.log(`+ [${i} / ${total}] scan complete [id: ${itemClass.item._id}] [status: ${result.Status}] [description: ${result.Description}]`) 40 | }) 41 | } 42 | 43 | async function init() { 44 | const clam = new ClamAV() 45 | 46 | const numItems = await ItemModel.count({ 'metadata.virus.run': false, "metadata.filetype": { "$ne": "url" } }) 47 | 48 | const random = Math.floor(Math.random() * (numItems - 20)) 49 | // const items = await ItemModel.find({ 'metadata.virus.run': false }).skip(random) 50 | const items = await ItemModel.find({ '_id': '5f5aabaeeddf282a7304242a' }) 51 | 52 | let i = 0 53 | 54 | for (let item of items) { 55 | console.log(`= [${i} / ${items.length}] [name: ${item.name.original}] [id: ${item._id}] [size: ${item.metadata.size}]`) 56 | 57 | try { 58 | running += 1 59 | 60 | while (running > 16) { 61 | console.log('too many running', running) 62 | await Utils.pause(15000) 63 | } 64 | 65 | await Utils.timeout(scanItem(clam, item, i, items.length), 50) 66 | } catch (e) { 67 | err = JSON.stringify(e) 68 | if (!err.startsWith('"Timed out')) { 69 | console.log(e) 70 | } 71 | } 72 | i += 1 73 | } 74 | 75 | // moongose.disconnect() 76 | } 77 | 78 | init() 79 | -------------------------------------------------------------------------------- /scripts/sendAnalytics.js: -------------------------------------------------------------------------------- 1 | const redis = require("redis"); 2 | const { promisify } = require("util"); 3 | const ua = require('universal-analytics'); 4 | 5 | const client = redis.createClient(); 6 | 7 | const blpopAsync = promisify(client.blpop).bind(client); 8 | 9 | client.on("error", function (error) { 10 | console.error(error); 11 | }); 12 | 13 | async function getItem() { 14 | const item = await blpopAsync('hits', 10) 15 | if (item !== null) { 16 | const visitor = ua('UA-121951630-1').pageview(item).send() 17 | } 18 | getItem() 19 | 20 | } 21 | 22 | getItem() -------------------------------------------------------------------------------- /scripts/tunnelDataMigration.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const tunnel = require('tunnel-ssh') 3 | const Minio = require('minio') 4 | const path = require('path') 5 | const { v4: uuidv4 } = require('uuid') 6 | // const fetch = require('fetch-timeout') 7 | const fetch = require('node-fetch') 8 | const config = require('@femto-apps/config') 9 | const toArray = require('stream-to-array') 10 | const fs = require('fs') 11 | const prettyBytes = require('pretty-bytes') 12 | const { bustCache } = require('../micro/helpers') 13 | 14 | const newUser = require('../modules/User.js') 15 | const newCollection = require('../modules/Collection.js') 16 | const newItemModel = require('../modules/Item.js') 17 | const newStore = require('../modules/Store.js') 18 | const newShort = require('../modules/Short.js') 19 | const newShortModel = require('../models/Short.js') 20 | const Types = require('../types') 21 | 22 | const minioOptions = { 23 | minio: { 24 | endPoint: config.get('minio.host'), 25 | port: config.get('minio.port'), 26 | useSSL: false, 27 | accessKey: config.get('minio.accessKey'), 28 | secretKey: config.get('minio.secretKey') 29 | } 30 | } 31 | 32 | const historicalMinioOptions = { 33 | minio: { 34 | endPoint: 'femto.pw', 35 | port: config.get('minio.port'), 36 | useSSL: false, 37 | accessKey: config.get('minio.accessKey'), 38 | secretKey: config.get('minio.secretKey') 39 | } 40 | } 41 | 42 | const translations = {} 43 | 44 | mongoose.connect(config.get('mongo.uri') + config.get('mongo.db'), { 45 | useNewUrlParser: true, 46 | useUnifiedTopology: true, 47 | useFindAndModify: false, 48 | useCreateIndex: true 49 | }) 50 | 51 | const client = new Minio.Client(minioOptions.minio) 52 | 53 | const historicalClient = new Minio.Client(historicalMinioOptions.minio) 54 | 55 | const ssh_config = { 56 | username: 'codefined', 57 | privateKey: fs.readFileSync('/home/codefined/.ssh/soyoufemto'), 58 | host: 'femto.pw', 59 | port: '22', 60 | dstHost: 'localhost', 61 | dstPort: '27017', 62 | localHost: '127.0.0.1', 63 | localPort: '27000' 64 | } 65 | 66 | const pause = time => new Promise(resolve => setTimeout(resolve, time)) 67 | 68 | function simplify(item) { 69 | return JSON.parse(JSON.stringify(item)) 70 | } 71 | 72 | async function isFileAlreadyMigrated(short, expectedSize) { 73 | // console.log(short, expectedSize) 74 | 75 | console.log(short) 76 | 77 | const url = `http://localhost:5053/${short}?cache=${new Date()}` 78 | 79 | console.log('fetching url') 80 | return fetch(url, {}, 5000, 'timeout') 81 | .then(async res => { 82 | console.log('response') 83 | 84 | const size = Number(res.headers.get('content-length')) 85 | if (size !== expectedSize) { 86 | console.log('failed size check on', short, 'got', size, 'expected', expectedSize) 87 | 88 | // check for copyright messages. 89 | const content = await res.text() 90 | 91 | if (content.includes('Digital Millennium Copyright Act')) { 92 | console.log('skipping because of dmca') 93 | return true 94 | } 95 | 96 | return false 97 | } 98 | 99 | return true 100 | }) 101 | .catch(e => { 102 | console.log(e) 103 | console.log(short) 104 | if (e === 'timeout') { 105 | return isFileAlreadyMigrated(short, expectedSize) 106 | } 107 | process.exit(0) 108 | return false 109 | }) 110 | } 111 | 112 | async function migrateFile(item) { 113 | const simple = simplify(item) 114 | 115 | const migrated = await isFileAlreadyMigrated(simple.name.short, simple.file.length) 116 | if (migrated) { 117 | return 118 | } 119 | 120 | const originalName = simple.name.original 121 | const extension = originalName.slice((originalName.lastIndexOf(".") - 1 >>> 0) + 2) 122 | const bucket = 'items' 123 | 124 | const folder = (await newCollection.fromReq({ 125 | user: simple.user._id ? new newUser({ _id: simple.user._id }) : undefined, 126 | ip: simple.user.ip 127 | })).path 128 | 129 | const filename = uuidv4() 130 | const filepath = path.posix.join(folder, filename) 131 | 132 | await client.fPutObject(bucket, filepath, simple.storage.store + '/' + simple.storage.filename, {}) 133 | 134 | const store = await newStore.create({ 135 | bucket, folder, filename, filepath, store: 'minio' 136 | }) 137 | 138 | let grab = { start: 0, end: 4100 } 139 | if (simple.file.length < 4100) { 140 | grab = { start: 0, end: simple.file.length } 141 | } 142 | 143 | let bytes = (await toArray(await store.getStream(grab)))[0] 144 | 145 | if (bytes === undefined) { 146 | bytes = Buffer.from([]) 147 | } 148 | 149 | const { data } = await Types.detect(store, bytes, { 150 | mimetype: simple.file.mime, 151 | encoding: simple.file.encoding 152 | }) 153 | 154 | const newItem = await newItemModel.create({ 155 | name: { 156 | original: originalName, 157 | extension: extension, 158 | filename: originalName.replace(/\.[^/.]+$/, '') 159 | }, 160 | metadata: { 161 | createdAt: simple.createdAt, 162 | updatedAt: simple.updatedAt, 163 | mime: simple.file.mime || 'application/octet-stream', 164 | encoding: simple.file.encoding, 165 | filetype: data.filetype, 166 | views: simple.views 167 | }, 168 | references: { 169 | storage: store.store 170 | }, 171 | user: { _id: simple.user._id, ip: simple.user.ip } 172 | }) 173 | 174 | const shortItem = await newShort.createReference(simple.name.short, newItem.item) 175 | await newItem.setCanonical(shortItem) 176 | 177 | await bustCache(simple.name.short) 178 | } 179 | 180 | async function migrateNewFile(item) { 181 | const simple = simplify(item) 182 | 183 | const short = simple.references.canonical.short 184 | const size = simple.metadata.size 185 | 186 | const migrated = await isFileAlreadyMigrated(short, size) 187 | if (migrated) { 188 | return 189 | } 190 | 191 | // console.log('item not migrated') 192 | 193 | const originalName = simple.name.original 194 | const extension = originalName.slice((originalName.lastIndexOf(".") - 1 >>> 0) + 2) 195 | const bucket = 'items' 196 | 197 | const folder = (await newCollection.fromReq({ 198 | user: simple.user._id ? new newUser({ _id: simple.user._id }) : undefined, 199 | ip: simple.user.ip 200 | })).path 201 | 202 | const filename = uuidv4() 203 | const filepath = path.posix.join(folder, filename) 204 | 205 | const storage = simple.references.storage 206 | const stream = await historicalClient.getObject(storage.bucket, storage.filepath) 207 | await client.putObject(bucket, filepath, stream) 208 | 209 | // console.log('item saved to storage') 210 | 211 | const store = await newStore.create({ 212 | bucket, folder, filename, filepath, store: 'minio' 213 | }) 214 | 215 | let grab = { start: 0, end: 4100 } 216 | if (simple.metadata.size < 4100) { 217 | grab = { start: 0, end: simple.metadata.size } 218 | } 219 | 220 | let bytes = (await toArray(await store.getStream(grab)))[0] 221 | if (bytes === undefined) { 222 | bytes = Buffer.from([]) 223 | } 224 | 225 | const { data } = await Types.detect(store, bytes, { 226 | mimetype: simple.metadata.mime, 227 | encoding: simple.metadata.encoding 228 | }) 229 | 230 | const newItem = await newItemModel.create({ 231 | name: { 232 | original: originalName, 233 | extension: extension, 234 | filename: originalName.replace(/\.[^/.]+$/, '') 235 | }, 236 | metadata: { 237 | createdAt: simple.metadata.createdAt, 238 | updatedAt: simple.metadata.updatedAt, 239 | mime: simple.metadata.mime || 'application/octet-stream', 240 | encoding: simple.metadata.encoding, 241 | filetype: data.filetype, 242 | views: simple.metadata.views 243 | }, 244 | references: { 245 | storage: store.store 246 | }, 247 | user: { _id: simple.user._id, ip: simple.user.ip } 248 | }) 249 | 250 | // console.log('model created') 251 | 252 | const shortItem = await newShort.createReference(short, newItem.item) 253 | await newItem.setCanonical(shortItem) 254 | 255 | // console.log('short created') 256 | 257 | await bustCache(short) 258 | } 259 | 260 | async function migrateURL(item) { 261 | const simple = simplify(item) 262 | 263 | // console.log(simple) 264 | } 265 | 266 | async function migrateNewURL(item) { 267 | const simple = simplify(item) 268 | 269 | const existing = await newShortModel.findOne({ short: item.references.canonical.short }) 270 | 271 | if (existing) { 272 | return 273 | } 274 | 275 | const newItem = await newItemModel.create({ 276 | name: { 277 | original: simple.name.original, 278 | }, 279 | metadata: simple.metadata, 280 | user: simple.user 281 | }) 282 | 283 | const shortItem = await newShort.createReference(item.references.canonical.short, newItem.item) 284 | await newItem.setCanonical(shortItem) 285 | 286 | await bustCache(item.references.canonical.short) 287 | } 288 | 289 | async function migrateItem(item) { 290 | const simple = simplify(item) 291 | 292 | if (item.user && item.user._id) { 293 | item.user._id = translations[item.user._id] 294 | } 295 | 296 | if (simple.file && simple.file.filetype) simple.type = { long: simple.file.filetype } 297 | 298 | if (simple.type.long === 'url') { 299 | return migrateURL(item) 300 | } 301 | 302 | if (['image', 'video', 'audio', 'binary', 'file', 'cast', 'text'].includes(simple.type.long)) { 303 | return migrateFile(item) 304 | } 305 | 306 | console.log(item, 'type not found') 307 | } 308 | 309 | async function migrateNewItem(item) { 310 | const simple = simplify(item) 311 | 312 | if (item.user && item.user._id) { 313 | item.user._id = translations[item.user._id] 314 | } 315 | 316 | if (!simple.references.canonical) { 317 | return 318 | } 319 | 320 | const short = simple.references.canonical.short 321 | const size = simple.metadata.size 322 | 323 | if (simple.metadata.filetype === 'url') { 324 | // check if url already exists 325 | // console.log(simple) 326 | 327 | console.log('url') 328 | return migrateNewURL(item) 329 | console.log('endUrl') 330 | } 331 | 332 | console.log('startTest') 333 | if (await isFileAlreadyMigrated(short, size)) { 334 | console.log('endTestIf') 335 | return 336 | } 337 | console.log('endTest') 338 | 339 | console.log('startFile') 340 | return migrateNewFile(item) 341 | console.log('endFile') 342 | 343 | // else, we should actually migrate it. 344 | } 345 | 346 | tunnel(ssh_config, async (err, server) => { 347 | const tunnelMinimalDesign = mongoose.createConnection('mongodb://127.0.0.1:27000/minimal_design', { 348 | useNewUrlParser: true, 349 | useUnifiedTopology: true 350 | }) 351 | 352 | const tunnelFileUploader = mongoose.createConnection('mongodb://127.0.0.1:27000/fileUploader', { 353 | useNewUrlParser: true, 354 | useUnifiedTopology: true 355 | }) 356 | 357 | const tunnelAuthenticationProvider = mongoose.createConnection('mongodb://127.0.0.1:27000/authenticationProvider', { 358 | useNewUrlParser: true, 359 | useUnifiedTopology: true 360 | }) 361 | 362 | const localFileUploader = mongoose.createConnection(config.get('mongo.uri') + config.get('mongo.db'), { 363 | useNewUrlParser: true, 364 | useUnifiedTopology: true, 365 | useFindAndModify: false, 366 | useCreateIndex: true 367 | }) 368 | 369 | const localAuthenticationProvider = mongoose.createConnection(config.get('mongo.uri') + 'authenticationProvider', { 370 | useNewUrlParser: true, 371 | useUnifiedTopology: true, 372 | useFindAndModify: false, 373 | useCreateIndex: true 374 | }) 375 | 376 | const tunnelFileUploaderItem = tunnelFileUploader.model('Item', newItemModel.schema) 377 | const tunnelMinimalDesignItem = tunnelMinimalDesign.model('Item', new mongoose.Schema({ transfer: String }, { strict: false })) 378 | const tunnelMinimalDesignUser = tunnelMinimalDesign.model('User', new mongoose.Schema({}, { strict: false })) 379 | const tunnelFileUploaderUser = tunnelFileUploader.model('User', new mongoose.Schema({}, { strict: false })) 380 | const tunnelAuthenticationProviderUser = tunnelAuthenticationProvider.model('User', new mongoose.Schema({}, { strict: false })) 381 | const localAuthenticationProviderUser = localAuthenticationProvider.model('User', new mongoose.Schema({ 382 | username: { type: String, required: true, index: { unique: true } }, 383 | password: { type: String, required: true }, 384 | createdAt: Date, 385 | updatedAt: Date 386 | }, { strict: false })) 387 | const localFileUploaderUser = localFileUploader.model('User', new mongoose.Schema({ 388 | user: { type: mongoose.Schema.Types.ObjectId }, 389 | apiKey: { type: String }, 390 | }, { strict: false })) 391 | 392 | // to migrate users, we first get all old users 393 | const userGenerator = await tunnelMinimalDesignUser.find({}, {}, { timeout: true }).cursor() 394 | 395 | const historicalUsers = {} 396 | const historicalUsersById = {} 397 | for (let user = await userGenerator.next(); user != null; user = await userGenerator.next()) { 398 | historicalUsers[user.username] = simplify(user) 399 | historicalUsersById[user._id] = simplify(user) 400 | } 401 | 402 | // then all new users 403 | const newUserGenerator = await tunnelAuthenticationProviderUser.find({}, {}, { timeout: true }).cursor() 404 | 405 | for (let user = await newUserGenerator.next(); user != null; user = await newUserGenerator.next()) { 406 | user = simplify(user) 407 | 408 | const uploadUser = await tunnelFileUploaderUser.findOne({ user: mongoose.Types.ObjectId(user._id) }) 409 | 410 | // this is just a web auth user, without the file uploader part. 411 | // most file uploader _ids represent web auth _ids. 412 | 413 | // console.log(uploadUser) 414 | 415 | let oldUserType 416 | if (!uploadUser) { 417 | oldUserType = { 418 | _id: user._id, 419 | username: user.username, 420 | password: user.password, 421 | createdAt: user.createdAt, 422 | updatedAt: user.updatedAt 423 | } 424 | 425 | continue 426 | } else { 427 | oldUserType = { 428 | _id: user._id, 429 | username: user.username, 430 | password: user.password, 431 | createdAt: user.createdAt, 432 | updatedAt: user.updatedAt, 433 | apiKey: simplify(uploadUser).apiKey 434 | } 435 | } 436 | 437 | const oldUsername = historicalUsers[user.username] 438 | 439 | if (!oldUserType.apiKey && oldUsername && oldUsername.apiKey) { 440 | // console.log('adding in api key') 441 | oldUserType.apiKey = oldUsername.apiKey 442 | } 443 | 444 | historicalUsers[user.username] = simplify(oldUserType) 445 | historicalUsersById[user._id] = simplify(oldUserType) 446 | } 447 | 448 | // console.log(Object.keys(historicalUsers).length) 449 | // console.log(Object.keys(historicalUsersById).length) 450 | 451 | console.log(await localAuthenticationProviderUser.deleteMany({})) 452 | console.log(await localFileUploaderUser.deleteMany({})) 453 | 454 | for (let username of Object.keys(historicalUsers)) { 455 | const user = historicalUsers[username] 456 | 457 | try { 458 | const authUser = new localAuthenticationProviderUser({ 459 | username: user.username, 460 | password: user.password, 461 | createdAt: user.createdAt, 462 | updatedAt: user.updatedAt 463 | }) 464 | 465 | const res = await authUser.save() 466 | 467 | historicalUsers[username].newId = simplify(res)._id 468 | 469 | translations[user._id] = simplify(res)._id 470 | 471 | if (user.apiKey) { 472 | const fileUser = new localFileUploaderUser({ 473 | user: simplify(res)._id, 474 | apiKey: user.apiKey, 475 | createdAt: user.createdAt, 476 | updatedAt: user.updatedAt 477 | }) 478 | 479 | await fileUser.save() 480 | 481 | } 482 | } catch (e) { 483 | console.log(username) 484 | } 485 | } 486 | 487 | // first we see how many we have left 488 | const total = await tunnelMinimalDesignItem.countDocuments({}) + await tunnelFileUploaderItem.countDocuments({}) 489 | const remaining = await tunnelMinimalDesignItem.find({}, {}, { timeout: true }).skip(4500).cursor() 490 | 491 | let i = 0 492 | 493 | // then we loop over, processing all from minimal_items 494 | // for (let item = await remaining.next(); item != null; item = await remaining.next()) { 495 | // i += 1 496 | 497 | // const simple = simplify(item) 498 | // console.log(`[${i} / ${total}] Processing [id: ${item._id}], [size: ${simple.file ? prettyBytes(simple.file.length || 0) : 'url'}] [name: ${simple.name.original}].`) 499 | // await migrateItem(item) 500 | // } 501 | 502 | // then we loop over, processing all remaining from femto.pw file uploader 503 | 504 | const catchup = await tunnelFileUploaderItem.find({}, {}, { timeout: true }).skip(1580).cursor() 505 | 506 | for (let item = await catchup.next(); item != null; item = await catchup.next()) { 507 | i += 1 508 | 509 | const simple = simplify(item) 510 | console.log(`[${i} / ${total}] Processing [id: ${item._id}], [size: ${simple.metadata ? prettyBytes(simple.metadata.size || 0) : 'url'}] [name: ${simple.name.original}].`) 511 | await migrateNewItem(item) 512 | } 513 | 514 | console.log('finished') 515 | }) -------------------------------------------------------------------------------- /src/css/uploader.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-highlight: #0DB4CE; 3 | --color-highlight-accent: #19647E; 4 | --color-highlight-hover: #19647E; 5 | --color-highlight-secondary: #EEC643; 6 | 7 | --color-primary-bg: #202020; 8 | --color-primary-fg: #d1d2d3; 9 | --color-inverted-bg: #d1d2d3; 10 | --color-inverted-fg: #1a1d21; 11 | --color-secondary-bg: #292c33; 12 | 13 | --color-foreground-max: #9a9c9e; 14 | --color-foreground-high: #75777a; 15 | --color-foreground-low: #323538; 16 | --color-foreground-min: #212428; 17 | 18 | --color-button-bg: #9b4dca; 19 | --color-button-fg: #ffffff; 20 | --color-button-hover: #606c76; 21 | 22 | --color-seperator: #1b1c1d; 23 | --color-hyplinker: #b881d9; 24 | } 25 | 26 | /** 27 | * Uppy overrides 28 | */ 29 | 30 | .uppy-Root { 31 | color: var(--color-primary-fg); 32 | } 33 | 34 | .uppy-Dashboard-inner { 35 | background-color: var(--color-primary-bg); 36 | border: 1px solid var(--color-foreground-high); 37 | } 38 | 39 | .uppy-StatusBar { 40 | background-color: var(--color-foreground-low); 41 | color: inherit; 42 | } 43 | 44 | .uppy-StatusBar-content { 45 | color: var(--color-primary-fg); 46 | } 47 | 48 | .uppy-DashboardContent-bar { 49 | background-color: var(--color-foreground-low); 50 | border-bottom: 1px solid var(--color-foreground-high); 51 | } 52 | 53 | .uppy-DashboardContent-back { 54 | color: var(--color-highlight); 55 | 56 | &:hover { 57 | color: var(--color-highlight); 58 | text-decoration: underline; 59 | } 60 | &:focus { 61 | background-color: var(--color-primary-bg); 62 | } 63 | } 64 | 65 | .uppy-DashboardContent-addMore { 66 | color: var(--color-highlight); 67 | 68 | &:hover { 69 | color: var(--color-highlight-hover); 70 | } 71 | &:focus { 72 | background-color: var(--color-primary-bg); 73 | } 74 | } 75 | 76 | .uppy-DashboardContent-addMoreCaption { 77 | color: var(--color-highlight); 78 | border-bottom: none; 79 | 80 | &:hover { 81 | color: var(--color-highlight-hover); 82 | text-decoration: underline; 83 | } 84 | } 85 | 86 | .uppy-size--md .uppy-DashboardAddFiles { 87 | border: 1px dashed var(--color-foreground-high); 88 | background-color: var(--color-primary-bg); 89 | } 90 | 91 | .uppy-Dashboard-AddFilesPanel { 92 | background: linear-gradient(0deg, var(--color-primary-bg) 35%, var(--color-foreground-high) 100%); 93 | } 94 | 95 | .uppy-Dashboard-browse { 96 | color: var(--color-highlight); 97 | 98 | &:hover, &:focus { 99 | text-decoration: underline; 100 | border-bottom: none; 101 | } 102 | } 103 | 104 | .uppy-StatusBar-progress { 105 | background-color: var(--color-highlight); 106 | } 107 | 108 | .uppy-StatusBar-spinner { 109 | fill: var(--color-highlight); 110 | } 111 | 112 | .uppy-StatusBar.is-waiting .uppy-StatusBar-actions { 113 | background-color: var(--color-secondary-bg); 114 | } 115 | 116 | .uppy-DashboardItem-previewInnerWrap { 117 | background-color: var(--color-primary-bg) !important; 118 | } 119 | 120 | .uppy-Dashboard-dropFilesHereHint { 121 | border: 1px dashed var(--color-highlight); 122 | background-image: url("data:image/svg+xml,%3Csvg width='48' height='48' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24 1v1C11.85 2 2 11.85 2 24s9.85 22 22 22 22-9.85 22-22S36.15 2 24 2V1zm0 0V0c13.254 0 24 10.746 24 24S37.254 48 24 48 0 37.254 0 24 10.746 0 24 0v1zm7.707 19.293a.999.999 0 1 1-1.414 1.414L25 16.414V34a1 1 0 1 1-2 0V16.414l-5.293 5.293a.999.999 0 1 1-1.414-1.414l7-7a.999.999 0 0 1 1.414 0l7 7z' fill='%230DB4CE' fill-rule='nonzero'/%3E%3C/svg%3E"); 123 | } 124 | 125 | .uppy-StatusBar-actionBtn, .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload, .uppy-Dashboard-FileCard-actionsBtn { 126 | background-color: var(--color-button-bg); 127 | color: var(--color-button-fg); 128 | } 129 | 130 | .uppy-StatusBar-actionBtn:hover, .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload:hover, .uppy-Dashboard-FileCard-actionsBtn:hover { 131 | background-color: var(--color-button-hover); 132 | color: var(--color-button-fg); 133 | } 134 | 135 | .uppy-StatusBar:not([aria-hidden="true"]).is-waiting { 136 | background-color: var(--color-secondary-bg); 137 | border-top: .1rem solid var(--color-seperator); 138 | } 139 | 140 | .uppy-DashboardContent-bar { 141 | border-bottom: .1rem solid var(--color-seperator); 142 | } 143 | 144 | .uppy-Dashboard-FileCard-preview, .uppy-Dashboard-FileCard-actions { 145 | background-color: var(--color-primary-bg) !important; 146 | } 147 | 148 | .uppy-Dashboard-FileCard { 149 | background-color: var(--color-secondary-bg); 150 | } 151 | 152 | .uppy-DashboardContent-bar { 153 | border-bottom: .1rem solid var(--color-seperator); 154 | } 155 | 156 | .uppy-DashboardContent-back:focus, .uppy-DashboardContent-addMoreCaption:focus, .uppy-Dashboard-browse:focus { 157 | background-color: #606c76; 158 | } 159 | 160 | .uppy-Dashboard-browse:focus, .uppy-Dashboard-browse:hover { 161 | border-bottom: 2px solid var(--color-hyplinker); 162 | } 163 | 164 | .uppy-DashboardContent-back, .uppy-DashboardContent-addMoreCaption, .uppy-DashboardContent-back:hover, .uppy-DashboardContent-addMoreCaption:hover, .uppy-Dashboard-browse, .uppy-Dashboard-browse:hover { 165 | color: var(--color-hyplinker); 166 | } 167 | 168 | .UppyIcon { 169 | fill: var(--color-hyplinker); 170 | } 171 | 172 | .comment { 173 | color: grey; 174 | } 175 | 176 | .link { 177 | text-decoration: underline; 178 | } 179 | 180 | .hidden { 181 | display: none; 182 | } -------------------------------------------------------------------------------- /src/stats.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | const prettyBytes = require('pretty-bytes') 3 | const abbreviate = require('number-abbreviate') 4 | 5 | function createChart(target, series, label = val => val) { 6 | var chart = new Chartist.Line(target, { 7 | series: series 8 | }, { 9 | axisX: { 10 | type: Chartist.FixedScaleAxis, 11 | divisor: 5, 12 | labelInterpolationFnc: function(value) { 13 | return moment(value).format('MMM D'); 14 | } 15 | }, 16 | axisY: { 17 | labelInterpolationFnc: label 18 | } 19 | }); 20 | } 21 | 22 | createChart('#users-chart', [ 23 | { 24 | name: 'users', 25 | data: users.map(val => ({ x: new Date(val.time), y: val.value })) 26 | } 27 | ]) 28 | 29 | createChart('#items-chart', [ 30 | { 31 | name: 'items', 32 | data: items.map(val => ({ x: new Date(val.time), y: val.value })) 33 | } 34 | ]) 35 | 36 | createChart('#views-chart', [ 37 | { 38 | name: 'views', 39 | data: views.map(val => ({ x: new Date(val.time), y: val.value })) 40 | } 41 | ], val => abbreviate(val)) 42 | 43 | createChart('#bandwidth-chart', [ 44 | { 45 | name: 'bandwidth', 46 | data: bandwidth.map(val => ({ x: new Date(val.time), y: val.value })) 47 | } 48 | ], val => prettyBytes(val)) -------------------------------------------------------------------------------- /src/uploader.js: -------------------------------------------------------------------------------- 1 | // Import the plugins 2 | const Uppy = require('@uppy/core') 3 | const XHRUpload = require('@uppy/xhr-upload') 4 | const Dashboard = require('@uppy/dashboard') 5 | 6 | // And their styles (for UI plugins) 7 | require('@uppy/core/dist/style.css') 8 | require('@uppy/dashboard/dist/style.css') 9 | require('./css/uploader.css') 10 | 11 | const uppy = Uppy({ 12 | autoProceed: false, 13 | }) 14 | .use(Dashboard, { 15 | trigger: '#upload-files', 16 | showProgressDetails: true, 17 | proudlyDisplayPoweredByUppy: false, // :( :( 18 | metaFields: [ 19 | { id: 'expiry', name: 'Expiry', placeholder: 'seconds until expiry' }, 20 | { id: 'name', name: 'Filename', placeholder: 'name of file' }, 21 | ] 22 | }) 23 | .use(XHRUpload, { 24 | endpoint: '/upload/multipart', 25 | fieldName: 'upload', 26 | getResponseData(text, resp) { 27 | const response = JSON.parse(text) 28 | return { short: response.data.short, url: location.href + response.data.short } 29 | } 30 | }) 31 | 32 | uppy.on('complete', (result) => { 33 | console.log('Upload complete! We’ve uploaded these files:', result.successful) 34 | }) 35 | 36 | uppy.on('upload-success', (file, resp) => { 37 | console.log('Single upload complete', file, resp) 38 | 39 | document.getElementById('upload_container').style.display = 'block' 40 | document.getElementById('uploads').innerHTML += 41 | `${resp.body.url} # ${file.data.name} (info)
` 42 | 43 | console.log('resp', resp) 44 | }) 45 | 46 | const shortenButton = document.getElementById('shorten-url') 47 | shortenButton.addEventListener('click', async () => { 48 | const url = prompt('URL to Shorten') 49 | 50 | if (url === null) return 51 | 52 | const resp = await fetch('/upload/url', { 53 | method: 'POST', 54 | headers: { 55 | 'Content-Type': 'application/json' 56 | }, 57 | body: JSON.stringify({ url }) 58 | }) 59 | .then(res => res.json()) 60 | 61 | document.getElementById('upload_container').style.display = 'block' 62 | document.getElementById('uploads').innerHTML += 63 | `${window.location.origin}/${resp.data.short} # ${url} (info)
` 64 | 65 | console.log('resp', resp) 66 | }) 67 | 68 | const apikey = document.getElementById('apikey') 69 | apikey.addEventListener('click', async () => { 70 | apikey.select() 71 | }) -------------------------------------------------------------------------------- /types/Base.js: -------------------------------------------------------------------------------- 1 | const memoize = require('p-memoize') 2 | const { createCanvas, loadImage } = require('canvas') 3 | const path = require('path') 4 | const { promisify } = require('util') 5 | const sendRanges = require('send-ranges') 6 | const { PassThrough } = require('stream') 7 | const contentDisposition = require('content-disposition') 8 | const config = require('@femto-apps/config') 9 | 10 | const Utils = require('../modules/Utils') 11 | 12 | const name = 'base' 13 | 14 | const generateThumb = memoize(async item => { 15 | // We don't know what this file is, so we have no idea what the thumb should look like. 16 | const canvas = createCanvas(256, 256) 17 | const ctx = canvas.getContext('2d') 18 | 19 | const unknownImagePath = path.posix.join(__dirname, '../public/images/unknown_file.png') 20 | const unknown = await loadImage(unknownImagePath) 21 | ctx.drawImage(unknown, 0, 0) 22 | 23 | ctx.font = '25px Sans' 24 | ctx.textAlign = 'center'; 25 | 26 | let name = await item.getName() 27 | 28 | if (name) { 29 | if (name.length > 19) { 30 | name = name.substring(0, 16) + '...' 31 | } 32 | 33 | ctx.fillText(name, 128, 220, 242) 34 | } 35 | 36 | const stream = canvas.createPNGStream() 37 | await item.setThumb(stream) 38 | }) 39 | 40 | class Base { 41 | constructor(item) { 42 | this.item = item 43 | this.generateThumb = generateThumb 44 | } 45 | 46 | get name() { 47 | return name 48 | } 49 | 50 | static async detect(store, bytes, data) { 51 | return { 52 | result: name 53 | } 54 | } 55 | 56 | static async match(item) { 57 | return await item.getFiletype() === name 58 | } 59 | 60 | async handleRange(req) { 61 | const stats = await this.item.getItemStat() 62 | const getStream = range => { 63 | const out = new PassThrough(); 64 | 65 | this.item.getItemStream(range) 66 | .then(stream => stream.pipe(out)) 67 | .catch(e => stream.emit('error', e)) 68 | 69 | return out 70 | } 71 | 72 | return { 73 | getStream, type: await this.getMime(), size: stats.size 74 | } 75 | } 76 | 77 | async checkDead(req, res) { 78 | if (await this.getExpired()) { 79 | res.send('This item has expired.') 80 | return true 81 | } 82 | 83 | if (await this.getDeleted()) { 84 | res.send('This item was removed.') 85 | return true 86 | } 87 | 88 | const virus = await this.item.getVirus() 89 | if (virus.detected) { 90 | res.send(`This item was detected as a virus and removed. We thought it was: ${virus.description}`) 91 | return true 92 | } 93 | 94 | if (config.get('clamav.warn') && config.get('clamav.enabled') && !virus.run && req.query.overrideVirusCheck !== 'true') { 95 | Utils.addQuery(req.originalUrl, 'overrideVirusCheck', true) 96 | res.send(`This item hasn't been scanned yet and could be a virus, are you sure you want to view it? Click Here`) 97 | return true 98 | } 99 | } 100 | 101 | async serve(req, res) { 102 | if (await this.checkDead(req, res)) return 103 | 104 | res.set('Content-Disposition', contentDisposition(await this.item.getName(), { type: 'inline' })) 105 | res.set('Content-Type', await this.getMime()) 106 | res.set('Cache-Control', 'public, max-age=604800, immutable') 107 | 108 | if (req.headers.range) { 109 | await promisify(sendRanges(this.handleRange.bind(this)))(req, res) 110 | } else { 111 | const streamPromise = this.item.getItemStream() 112 | const statsPromise = this.item.getItemStat() 113 | 114 | const [stream, stats] = await Promise.all([streamPromise, statsPromise]) 115 | 116 | res.set('Content-Length', stats.size) 117 | res.set('Accept-Ranges', 'bytes') 118 | res.writeHead(200) 119 | 120 | stream.pipe(res) 121 | 122 | await this.incrementViews() 123 | } 124 | } 125 | 126 | async incrementViews() { 127 | return this.item.incrementViews() 128 | } 129 | 130 | async raw(req, res) { 131 | 132 | } 133 | 134 | async thumb(req, res) { 135 | if (await this.checkDead(req, res)) return 136 | 137 | 138 | if (!(await this.hasThumb())) { 139 | console.log(`Generating thumb for ${await this.item.id()}`) 140 | await this.generateThumb(this) 141 | } 142 | 143 | res.set('Content-Disposition', contentDisposition(await this.item.getName(), { type: 'inline' })) 144 | res.set('Content-Type', 'image/png') 145 | res.set('Cache-Control', 'public, max-age=604800, immutable') 146 | 147 | const stream = await this.item.getThumbStream() 148 | stream.pipe(res) 149 | } 150 | 151 | async setThumb(stream) { 152 | return this.item.setThumb(stream) 153 | } 154 | 155 | async hasThumb() { 156 | return this.item.hasThumb() 157 | } 158 | 159 | async getName() { 160 | return this.item.getName() 161 | } 162 | 163 | async delete() { 164 | if (await this.item.hasThumb()) { 165 | const thumb = new Base(await this.item.getThumb()) 166 | 167 | await thumb.delete() 168 | } 169 | 170 | await this.item.markDeleted() 171 | } 172 | 173 | async getMime() { 174 | return this.item.getMime() 175 | } 176 | 177 | async getExpired() { 178 | return this.item.getExpired() 179 | } 180 | 181 | async getDeleted() { 182 | return this.item.getDeleted() 183 | } 184 | 185 | async generateThumb() { 186 | throw new Error('Dummy function, should be implemented in constructor.') 187 | } 188 | 189 | async ownedBy(user) { 190 | return this.item.ownedBy(user) 191 | } 192 | } 193 | 194 | module.exports = Base 195 | module.exports.baseThumb = generateThumb -------------------------------------------------------------------------------- /types/Binary.js: -------------------------------------------------------------------------------- 1 | const MultiStream = require('multistream') 2 | const toStream = require('buffer-to-stream') 3 | const isBinaryFile = require('isbinaryfile').isBinaryFile 4 | 5 | const Base = require('./Base') 6 | 7 | const name = 'binary' 8 | 9 | class Binary extends Base { 10 | constructor(item) { 11 | super(item) 12 | } 13 | 14 | get name() { 15 | return name 16 | } 17 | 18 | static async detect(store, bytes, data) { 19 | const isBinary = await isBinaryFile(bytes, bytes.length) 20 | 21 | return { 22 | result: isBinary ? name : undefined 23 | } 24 | } 25 | 26 | static async match(item) { 27 | return await item.getFiletype() === name 28 | } 29 | } 30 | 31 | module.exports = Binary -------------------------------------------------------------------------------- /types/Code.js: -------------------------------------------------------------------------------- 1 | const CombinedStream = require('combined-stream') 2 | const languageDetect = require('language-detect') 3 | 4 | const Text = require('./Text') 5 | 6 | const name = 'code' 7 | 8 | class Code extends Text { 9 | constructor(item) { 10 | super(item) 11 | } 12 | 13 | get name() { 14 | return name 15 | } 16 | 17 | static async detect(store, bytes, data) { 18 | // disabled for now 19 | return { result: undefined } 20 | 21 | const language = await languageDetect.classify(bytes.toString()) 22 | 23 | console.log(language) 24 | 25 | return { 26 | result: language ? name + '/' + language : undefined 27 | } 28 | } 29 | 30 | static async match(item) { 31 | return (await item.getFiletype()).startsWith(name) 32 | } 33 | } 34 | 35 | module.exports = Code -------------------------------------------------------------------------------- /types/Image.js: -------------------------------------------------------------------------------- 1 | const Base = require('./Base') 2 | const memoize = require('p-memoize') 3 | const smartcrop = require('smartcrop-sharp') 4 | const toArray = require('stream-to-array') 5 | const sharp = require('sharp') 6 | 7 | const name = 'image' 8 | 9 | const generateThumb = memoize(async item => { 10 | const body = Buffer.concat(await toArray(await item.item.getItemStream())) 11 | 12 | const result = await smartcrop.crop(body, { width: 256, height: 256 }) 13 | const crop = result.topCrop 14 | 15 | const buffer = await sharp(body) 16 | .extract({ width: crop.width, height: crop.height, left: crop.x, top: crop.y }) 17 | .resize(256, 256) 18 | .toBuffer() 19 | 20 | await item.setThumb(buffer) 21 | }) 22 | 23 | class Image extends Base { 24 | constructor(item) { 25 | super(item) 26 | 27 | this.generateThumb = generateThumb 28 | } 29 | 30 | get name() { 31 | return name 32 | } 33 | 34 | static async detect(store, bytes, data) { 35 | return { 36 | result: data.mime.startsWith('image/') ? name : undefined 37 | } 38 | } 39 | 40 | static async match(item) { 41 | return await item.getFiletype() === name 42 | } 43 | } 44 | 45 | module.exports = Image -------------------------------------------------------------------------------- /types/Text.js: -------------------------------------------------------------------------------- 1 | const memoize = require('p-memoize') 2 | const toArray = require('stream-to-array') 3 | const fs = require('fs').promises 4 | const tmp = require('tmp') 5 | const resolvePath = require('resolve-path') 6 | const execa = require('execa') 7 | const { v4: uuidv4 } = require('uuid') 8 | const sharp = require('sharp') 9 | const smartcrop = require('smartcrop-sharp') 10 | const config = require('@femto-apps/config') 11 | 12 | const Utils = require('../modules/Utils') 13 | 14 | const Base = require('./Base') 15 | 16 | const name = 'text' 17 | const tempDir = tmp.dirSync() 18 | 19 | const generateThumb = memoize(async item => { 20 | console.log('Generating text item thumbnail for', item) 21 | 22 | const uuid = uuidv4() 23 | 24 | // We don't know what this file is, so we have no idea what the thumb should look like. 25 | // 256, 256 smart crop 26 | const itemBuffer = await Utils.getFirstLines(await item.item.getItemStream(), 12) 27 | const tempPath = resolvePath(tempDir.name, (await item.getName()) + uuid) 28 | 29 | await fs.writeFile(tempPath, itemBuffer, 'utf-8') 30 | 31 | const {stdout, stderr} = await execa(config.get('carbon.path'), ['-h', tempPath, '-t', uuid]) 32 | await Utils.move(`${uuid}.png`, `${tempPath}.png`) 33 | const body = await fs.readFile(`${tempPath}.png`) 34 | 35 | const result = await smartcrop.crop(body, { width: 256, height: 256 }) 36 | const crop = result.topCrop 37 | 38 | const buffer = await sharp(body) 39 | .extract({ width: crop.width, height: crop.height, left: crop.x, top: crop.y }) 40 | .resize(256, 256) 41 | .toBuffer() 42 | 43 | await item.setThumb(buffer) 44 | }) 45 | 46 | class Text extends Base { 47 | constructor(item) { 48 | super(item) 49 | 50 | this.generateThumb = generateThumb 51 | } 52 | 53 | get name() { 54 | return name 55 | } 56 | 57 | static async detect(store, bytes, data) { 58 | // We assume we've already run binary file tests prior to this. 59 | return { 60 | result: name 61 | } 62 | } 63 | 64 | static async match(item) { 65 | return await item.getFiletype() === name 66 | } 67 | } 68 | 69 | module.exports = Text -------------------------------------------------------------------------------- /types/Url.js: -------------------------------------------------------------------------------- 1 | const memoize = require('memoizee') 2 | const puppeteer = require('puppeteer') 3 | const sharp = require('sharp') 4 | const validUrl = require('valid-url') 5 | const smartcrop = require('smartcrop-sharp') 6 | 7 | const Base = require('./Base') 8 | 9 | const name = 'url' 10 | 11 | const generateThumb = memoize(async item => { 12 | console.log('Generating thumb for URL: ', item) 13 | 14 | // We don't know what this file is, so we have no idea what the thumb should look like. 15 | const url = await item.getName() 16 | 17 | if (!validUrl.isWebUri(url)) { 18 | return Base.baseThumb(item) 19 | } 20 | 21 | let body 22 | 23 | try { 24 | const browser = await puppeteer.launch() 25 | const page = await browser.newPage() 26 | await page.setViewport({ width: 1920, height: 1080 }) 27 | await page.goto(url) 28 | body = await page.screenshot() 29 | await browser.close() 30 | } catch(e) { 31 | console.log(e) 32 | return Base.baseThumb(item) 33 | } 34 | 35 | const result = await smartcrop.crop(body, { width: 256, height: 256 }) 36 | const crop = result.topCrop 37 | 38 | const buffer = await sharp(body) 39 | .extract({ width: crop.width, height: crop.height, left: crop.x, top: crop.y }) 40 | .resize(256, 256) 41 | .toBuffer() 42 | 43 | await item.setThumb(buffer) 44 | }) 45 | 46 | class Url extends Base { 47 | constructor(item) { 48 | super(item) 49 | this.generateThumb = generateThumb 50 | } 51 | 52 | get name() { 53 | return name 54 | } 55 | 56 | static async detect(store, bytes, data) { 57 | return false 58 | } 59 | 60 | static async match(item) { 61 | return (await item.getFiletype()).startsWith(name) 62 | } 63 | 64 | async serve(req, res) { 65 | if (await this.checkDead(req, res)) return 66 | 67 | 68 | res.redirect(await this.getName()) 69 | await this.incrementViews() 70 | } 71 | 72 | async raw(req, res) { 73 | return res.send('Thumb hasn\'t been implemented for URLs.') 74 | } 75 | } 76 | 77 | module.exports = Url -------------------------------------------------------------------------------- /types/index.js: -------------------------------------------------------------------------------- 1 | const fileType = require('file-type') 2 | 3 | const types = [ 4 | require('./Url'), // matches URLs 5 | require('./Image'), // matches static images 6 | require('./Binary'), // matches binary 7 | require('./Code'), // matches programming codes 8 | require('./Text'), // matches text 9 | 10 | // this should never be reached. 11 | require('./Base') // matches everything 12 | ] 13 | 14 | class Types { 15 | static async detect(store, bytes, file) { 16 | // `stream` contains the data we wish to upload. It cannot be consumed without 17 | // a passthrough (if this happens, we'll have no data to upload to Minio). 18 | 19 | const data = { 20 | mime: file.mimetype, 21 | encoding: file.encoding 22 | } 23 | 24 | let type 25 | try { 26 | // We first pass it through some initial checks 27 | type = await fileType.fromBuffer(bytes) 28 | } catch(e) { 29 | type = { ext: 'unknown', mime: 'application/octet-stream' } 30 | } 31 | 32 | if (type && type.mime) { 33 | data.mime = type.mime 34 | } 35 | 36 | for (let Type of types) { 37 | const result = await Type.detect(store, bytes, data) 38 | 39 | if (result.result) { 40 | data.filetype = result.result 41 | break 42 | } 43 | } 44 | 45 | return { 46 | data 47 | } 48 | } 49 | 50 | static async match(item) { 51 | for (let Type of types) { 52 | if (await Type.match(item)) { 53 | return new Type(item) 54 | } 55 | } 56 | } 57 | } 58 | 59 | module.exports = Types -------------------------------------------------------------------------------- /views/components/base.pug: -------------------------------------------------------------------------------- 1 | include ./nav 2 | 3 | doctype html 4 | html(lang='en') 5 | head 6 | meta(charset='utf-8') 7 | title= page.title 8 | meta(name='description', content='Upload and share any file for free, instantly from anywhere.') 9 | meta(name='author', content='Femto Apps') 10 | //- link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic') 11 | link(rel='stylesheet', href='https://cdn.jsdelivr.net/gh/femto-apps/lib-style@92da5e3/dist/main.css') 12 | link(rel='stylesheet', href='/css/main.css') 13 | link(rel='apple-touch-icon', sizes='180x180', href='/apple-touch-icon.png') 14 | link(rel='icon', type='image/png', sizes='32x32', href='/favicon-32x32.png') 15 | link(rel='icon', type='image/png', sizes='16x16', href='/favicon-16x16.png') 16 | link(rel='manifest', href='/site.webmanifest') 17 | link(rel='mask-icon', href='/safari-pinned-tab.svg', color='#5bbad5') 18 | meta(name='apple-mobile-web-app-title', content='Femto') 19 | meta(name='application-name', content='Femto') 20 | meta(name='msapplication-TileColor', content='#9f00a7') 21 | meta(name='theme-color', content='#000000') 22 | meta(name='viewport' content='width=device-width, initial-scale=1') 23 | block head 24 | 25 | body 26 | container 27 | +nav(nav.title, nav.links) 28 | 29 | main 30 | block main 31 | 32 | buffer 33 | 34 | footer 35 | section. 36 | #[a(href='https://github.com/femto-apps/web-file-uploader') Source] | 37 | #[a(href='/privacy') Privacy] | 38 | #[a(href='/terms') Terms] | 39 | #[a(href='/stats') Stats] | 40 | #[a(href='/issue') Report Issue] | 41 | #[a(href='/discord') Discord] 42 | 43 | block scripts -------------------------------------------------------------------------------- /views/components/nav.pug: -------------------------------------------------------------------------------- 1 | mixin nav(title, links) 2 | nav 3 | section 4 | a.nav-title(href='/')= title 5 | ul.nav-list.float-right 6 | for link in links 7 | li.nav-item 8 | a(href=link.href)= link.title -------------------------------------------------------------------------------- /views/home.pug: -------------------------------------------------------------------------------- 1 | extends components/base 2 | 3 | block main 4 | section 5 | center 6 | p 7 | b Upload and share any file for free, instantly from anywhere. 8 | 9 | section 10 | row 11 | column 12 | center 13 | button#upload-files Upload Files 14 | column 15 | center 16 | button#shorten-url Shorten URL 17 | if user 18 | column 19 | center 20 | a(href='/uploads') 21 | button#see-uploads See Uploads 22 | 23 | section 24 | pre 25 | code#info. 26 | #[span.comment # Upload using ShareX (Windows)] 27 | $ Download #[a(href='https://getsharex.com/').link ShareX] then install #[a(href='/sharex/uploader.sxcu').link our uploader] and #[a(href='/sharex/shortener.sxcu').link our shortener] 28 | 29 | #[span.comment # Upload from your terminal] 30 | $ curl -F "upload=@'UPLOAD_PATH'" #[span= origin + 'upload'] 31 | 32 | #[span.comment # See more ways to upload #[a(href='https://github.com/femto-apps/web-file-uploader/wiki/Ways-to-Upload') here]] 33 | #[#upload_container.hidden #[span.comment #[br]# Uploaded files]#[#uploads]] 34 | if user 35 | br 36 | br 37 | section 38 | row 39 | column(style='width:10%;') 40 | p API Key: 41 | column(style='width:90%;') 42 | input#apikey.spoiler(readonly, value=key) 43 | 44 | block scripts 45 | script(src='/js/uploader.bundle.js') -------------------------------------------------------------------------------- /views/info.pug: -------------------------------------------------------------------------------- 1 | extends components/base 2 | 3 | mixin row(header, value) 4 | tr 5 | th= header 6 | td= value 7 | 8 | block main 9 | section 10 | center 11 | p 12 | h3 Info for #[span= item.name.original] 13 | 14 | section 15 | h4 General Information 16 | 17 | table 18 | tbody 19 | +row('Original Name', item.name.original) 20 | +row('Size', prettyBytes(item.metadata.size)) 21 | +row('Created At', dateFormat(item.metadata.createdAt, 'longDate')) 22 | +row('Canonical Name', item.references.canonical.short) 23 | +row('Views', item.metadata.views) 24 | 25 | section 26 | h4 Developer Information 27 | 28 | table 29 | tbody 30 | +row('Extracted Filename', item.name.filename) 31 | +row('Extracted Extension', item.name.extension) 32 | +row('Virus Scan', `[Run: ${item.metadata.virus.run}], [Virus: ${item.metadata.virus.detected}], [Desc: ${item.metadata.virus.description}]`) 33 | +row('Mime Type', item.metadata.mime) 34 | +row('Encoding', item.metadata.encoding) 35 | +row('Filetype', item.metadata.filetype) 36 | +row('Deleted', item.deleted) 37 | 38 | if item.references && item.references.storage 39 | section 40 | h4 Storage Information 41 | 42 | table 43 | tbody 44 | +row('Store', item.references.storage.store) 45 | +row('Bucket', item.references.storage.bucket) 46 | +row('Folder', `Redacted`) 47 | +row('Filename', item.references.storage.filename) 48 | 49 | if isOwner 50 | section 51 | h4 Owner Actions 52 | 53 | a(href=`/delete/${item.references.canonical.short}`) 54 | button Delete Item 55 | 56 | section 57 | h4 Generated Thumb 58 | 59 | img(src=`/thumb/${item.references.canonical.short}`) -------------------------------------------------------------------------------- /views/issues.pug: -------------------------------------------------------------------------------- 1 | extends components/base 2 | 3 | block main 4 | //- +flash() 5 | 6 | section 7 | form(action='/issue', method='post') 8 | 9 | h3 Post Issue 10 | 11 | fieldset 12 | label(for='issue')#issue Describe Issue / Comment / Suggestion 13 | textarea(name='issue' rows='10') 14 | 15 | label(for='contact')#contact Contact 16 | input(type='text', name='contact', placeholder='How should we contact you?' autocorrect='off', spellcheck='false', autocapitalize='off', autofocus='true')#contact 17 | p You can fill in the above form if you want us to contact you about this at a later point. Post either an email, discord handle, etc. 18 | 19 | input(type='submit', value='Send')#send.button-outline -------------------------------------------------------------------------------- /views/privacy.pug: -------------------------------------------------------------------------------- 1 | extends components/base 2 | 3 | block main 4 | section 5 | h2 Privacy Policy 6 | 7 | section 8 | p Your privacy is important to us. It is Femto's policy to respect your privacy regarding any information we may collect from you across our website, #[a(href='https://femto.pw') https://femto.pw ], and other sites we own and operate. 9 | p We only ask for your personal information when it is required to maintain the stability of our service, primarily in anti-abuse measures. 10 | p We only retain collected information for as long as necessary to provide you with your requested service. What data we store, we’ll protect within commercially acceptable means to prevent loss and theft, as well as unauthorised access, disclosure, copying, use or modification. 11 | p Our website may link to external sites that are not operated by us. Please be aware that we have no control over the content and practices of these sites, and cannot accept responsibility or liability for their respective privacy policies. 12 | p You are free to refuse our request for your personal information, with the understanding that we may be unable to provide you with some of your desired services. 13 | p Your continued use of our website will be regarded as acceptance of our practices around privacy and personal information. If you have any questions about how we handle user data and personal information, feel free to #[a(href='/contact') contact us]. 14 | 15 | b This policy is effective as of 11 August 2018. -------------------------------------------------------------------------------- /views/stats.pug: -------------------------------------------------------------------------------- 1 | extends components/base 2 | 3 | mixin row(header, value) 4 | tr 5 | th= header 6 | td= value 7 | 8 | block main 9 | section 10 | center 11 | p 12 | h3 Usage Statistics 13 | 14 | blockquote To conserve CPU cycles, some of these statistics are estimates. 15 | 16 | section 17 | h4 Usage 18 | 19 | h6 Users 20 | .ct-chart#users-chart 21 | 22 | h6 Items 23 | .ct-chart#items-chart 24 | 25 | h6 Views 26 | .ct-chart#views-chart 27 | 28 | h6 Bandwidth 29 | .ct-chart#bandwidth-chart 30 | 31 | block head 32 | link(rel="stylesheet", href="//cdn.jsdelivr.net/chartist.js/latest/chartist.min.css") 33 | style. 34 | .ct-label, .ct-grid { 35 | color: #b9b9b9 !important; 36 | stroke: #b9b9b9 !important; 37 | } 38 | 39 | .ct-point { 40 | stroke-width: 0px; 41 | } 42 | 43 | block scripts 44 | script. 45 | var users = !{JSON.stringify(stats.users)} 46 | var items = !{JSON.stringify(stats.items)} 47 | var views = !{JSON.stringify(stats.views)} 48 | var bandwidth = !{JSON.stringify(stats.bandwidth)} 49 | 50 | script(src='//cdn.jsdelivr.net/chartist.js/latest/chartist.min.js') 51 | script(src='/js/stats.bundle.js') -------------------------------------------------------------------------------- /views/terms.pug: -------------------------------------------------------------------------------- 1 | extends components/base 2 | 3 | block main 4 | section 5 | h2 Terms and Conditions 6 | 7 | blockquote These terms and conditions are not final and may be changed at any time. They were automatically generated and may not represent our wishes. If you disagree with any statements included in these terms of service, please contact us and we will endeavour to change them to accomodate your request. 8 | 9 | section 10 | p 11 | | By accessing this website we assume you accept these terms and conditions. Do not continue to use Femto if you do not agree to take all of the terms and conditions stated on this page. 12 | p 13 | | The following terminology applies to these Terms and Conditions, Privacy Statement and Disclaimer Notice and all Agreements: "Client", "You" and "Your" refers to you, the person log on this website and compliant to the Company’s terms and conditions. "The Company", "Ourselves", "We", "Our" and "Us", refers to our Company. "Party", "Parties", or "Us", refers to both the Client and ourselves. All terms refer to the offer, acceptance and consideration of payment necessary to undertake the process of our assistance to the Client in the most appropriate manner for the express purpose of meeting the Client’s needs in respect of provision of the Company’s stated services, in accordance with and subject to, prevailing law of Netherlands. Any use of the above terminology or other words in the singular, plural, capitalization and/or he/she or they, are taken as interchangeable and therefore as referring to same. 14 | h3 Cookies 15 | p 16 | | We employ the use of cookies. By accessing Femto, you agreed to use cookies in agreement with the Femto's Privacy Policy. 17 | p 18 | | Most interactive websites use cookies to let us retrieve the user’s details for each visit. Cookies are used by our website to enable the functionality of certain areas to make it easier for people visiting our website. Some of our affiliate/advertising partners may also use cookies. 19 | h3 License 20 | p 21 | | Unless otherwise stated, Femto and/or its licensors own the intellectual property rights for all material on Femto. All intellectual property rights are reserved. You may access this from Femto for your own personal use subjected to restrictions set in these terms and conditions. 22 | p You must not: 23 | ul 24 | li Republish material from Femto 25 | li Sell, rent or sub-license material from Femto 26 | li Reproduce, duplicate or copy material from Femto 27 | li Redistribute content from Femto 28 | p This Agreement shall begin on the date hereof. 29 | p 30 | | Parts of this website offer an opportunity for users to post and exchange opinions and information in certain areas of the website. Femto does not filter, edit, publish or review Comments prior to their presence on the website. Comments do not reflect the views and opinions of Femto,its agents and/or affiliates. Comments reflect the views and opinions of the person who post their views and opinions. To the extent permitted by applicable laws, Femto shall not be liable for the Comments or for any liability, damages or expenses caused and/or suffered as a result of any use of and/or posting of and/or appearance of the Comments on this website. 31 | p 32 | | Femto reserves the right to monitor all Comments and to remove any Comments which can be considered inappropriate, offensive or causes breach of these Terms and Conditions. 33 | p You warrant and represent that: 34 | ul 35 | li 36 | | You are entitled to post the Comments on our website and have all necessary licenses and consents to do so; 37 | li 38 | | The Comments do not invade any intellectual property right, including without limitation copyright, patent or trademark of any third party; 39 | li 40 | | The Comments do not contain any defamatory, libelous, offensive, indecent or otherwise unlawful material which is an invasion of privacy 41 | li 42 | | The Comments will not be used to solicit or promote business or custom or present commercial activities or unlawful activity. 43 | p 44 | | You hereby grant Femto a non-exclusive license to use, reproduce, edit and authorize others to use, reproduce and edit any of your Comments in any and all forms, formats or media. 45 | h3 Hyperlinking to our Content 46 | p Any organisations and users are able to Hyperlink to our website without prior permission being provided. 47 | h3 iFrames 48 | p 49 | | Without prior approval and written permission, you may not create frames around our Webpages that alter in any way the visual presentation or appearance of our Website. 50 | h3 Content Liability 51 | p 52 | | We shall not be hold responsible for any content that appears on your Website. You agree to protect and defend us against all claims that is rising on your Website. No link(s) should appear on any Website that may be interpreted as libelous, obscene or criminal, or which infringes, otherwise violates, or advocates the infringement or other violation of, any third party rights. 53 | h3 Your Privacy 54 | p Please read #[a(href='/privacy') our Privacy Policy] 55 | h3 Reservation of Rights 56 | p 57 | | We reserve the right to request that you remove all links or any particular link to our Website. You approve to immediately remove all links to our Website upon request. We also reserve the right to amen these terms and conditions and it’s linking policy at any time. By continuously linking to our Website, you agree to be bound to and follow these linking terms and conditions. 58 | h3 Removal of links from our website 59 | p 60 | | If you find any link on our Website that is offensive for any reason, you are free to contact and inform us any moment. We will consider requests to remove links but we are not obligated to or so or to respond to you directly. 61 | p 62 | | We do not ensure that the information on this website is correct, we do not warrant its completeness or accuracy; nor do we promise to ensure that the website remains available or that the material on the website is kept up to date. 63 | h3 Disclaimer 64 | p 65 | | To the maximum extent permitted by applicable law, we exclude all representations, warranties and conditions relating to our website and the use of this website. Nothing in this disclaimer will: 66 | ul 67 | li limit or exclude our or your liability for death or personal injury; 68 | li 69 | | limit or exclude our or your liability for fraud or fraudulent misrepresentation; 70 | li 71 | | limit any of our or your liabilities in any way that is not permitted under applicable law; or 72 | li 73 | | exclude any of our or your liabilities that may not be excluded under applicable law. 74 | p 75 | | The limitations and prohibitions of liability set in this Section and elsewhere in this disclaimer: (a) are subject to the preceding paragraph; and (b) govern all liabilities arising under the disclaimer, including liabilities arising in contract, in tort and for breach of statutory duty. 76 | p 77 | | As long as the website and the information and services on the website are provided free of charge, we will not be liable for any loss or damage of any nature. 78 | 79 | b This policy is effective as of 11 August 2018. -------------------------------------------------------------------------------- /views/uploads.pug: -------------------------------------------------------------------------------- 1 | extends components/base 2 | 3 | block main 4 | section 5 | grid.uploads 6 | for item in items 7 | - const hit = `${item.item.metadata.views} hit${item.item.metadata.views != 1 ? 's' : ''}` 8 | - const message = `${item.item.name.original} (${hit})` 9 | if item.item.references.canonical !== null 10 | a.thumb-link(href=`/${item.item.references.canonical.short}${item.item.name.extension ? `.${item.item.name.extension}` : ``}`) 11 | img.thumb(src=`/thumb/${item.item.references.canonical.short}${item.item.name.extension ? `.${item.item.name.extension}` : ``}` alt=message title=message) 12 | else 13 | - console.log(`warning, trying to render ${item.item._id} but it didn't have a canonical link.`) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: { 6 | uploader: './src/uploader.js', 7 | stats: './src/stats.js' 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, 'public/js'), 11 | filename: '[name].bundle.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.css$/i, 17 | use: ['style-loader', 'css-loader'], 18 | }, 19 | ], 20 | }, 21 | } --------------------------------------------------------------------------------