├── .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 |
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 |
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}
`
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}
`
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 | }
--------------------------------------------------------------------------------