6
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
.data
|
.data
|
||||||
out
|
out
|
44
.vscode/tasks.json
vendored
|
@ -1,23 +1,23 @@
|
||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command":"npx tsc",
|
"command":"tsc\nsass src/style:out/style\nrollup -c",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"label": "Build (Bot Server)"
|
"label": "Build (Bot Server)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command":"npx tsc\nnode ./out/index.js\ndel ./out/* -Recurse",
|
"command":"tsc\nsass src/style:out/style\nrollup -c\nnode ./out/server/index.js\ndel ./out/* -Recurse",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"label": "Build & Test"
|
"label": "Build & Test"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
46
LICENSE
|
@ -1,24 +1,24 @@
|
||||||
This is free and unencumbered software released into the public domain.
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
distribute this software, either in source code form or as a compiled
|
distribute this software, either in source code form or as a compiled
|
||||||
binary, for any purpose, commercial or non-commercial, and by any
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
means.
|
means.
|
||||||
|
|
||||||
In jurisdictions that recognize copyright laws, the author or authors
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
of this software dedicate any and all copyright interest in the
|
of this software dedicate any and all copyright interest in the
|
||||||
software to the public domain. We make this dedication for the benefit
|
software to the public domain. We make this dedication for the benefit
|
||||||
of the public at large and to the detriment of our heirs and
|
of the public at large and to the detriment of our heirs and
|
||||||
successors. We intend this dedication to be an overt act of
|
successors. We intend this dedication to be an overt act of
|
||||||
relinquishment in perpetuity of all present and future rights to this
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
software under copyright law.
|
software under copyright law.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
For more information, please refer to <http://unlicense.org/>
|
For more information, please refer to <http://unlicense.org/>
|
107
README.md
|
@ -1,32 +1,75 @@
|
||||||
# monofile
|
# monofile
|
||||||
File sharing via Discord
|
The open-source, Discord-based file sharing service.
|
||||||
|
[Live instance](https://fyle.uk)
|
||||||
<br>
|
|
||||||
|
<br>
|
||||||
## .env
|
|
||||||
|
## Setup
|
||||||
```
|
|
||||||
TOKEN=KILL-YOURSELF.NOW
|
First, install monofile's prerequisites...
|
||||||
```
|
```
|
||||||
|
npm i
|
||||||
## versions & planned updates
|
```
|
||||||
|
|
||||||
- [X] 1.0.0 initial release
|
Then, add your bot token...
|
||||||
- [X] 1.1.0 add file cloning endpoint
|
```
|
||||||
- [X] 1.1.1 add file cloning webpage
|
echo "TOKEN=INSERT-TOKEN.HERE" > .env
|
||||||
- [X] 1.1.2 fix file cloning with binary data
|
```
|
||||||
- [X] 1.1.3 display current version on pages
|
and, in addition, SMTP authentication...
|
||||||
- [X] 1.1.4 serve /assets as static files & make /server endpoint
|
```
|
||||||
- [X] 1.2.0 add file parameters section + custom ids
|
echo "\nMAIL_USER=user@example.com" > .env
|
||||||
- [X] 1.2.1 add file counter to main page
|
echo "\nMAIL_PASS=password here" > .env
|
||||||
- [X] 1.2.2 clean up this shitty code
|
```
|
||||||
- [X] 1.2.3 bugfixes
|
|
||||||
- [ ] 1.2.4 add id locks, allowing you to set a key for a file that allows you to overwrite the file in the future
|
Invite your bot to a server, and create a new `config.json` in the project root:
|
||||||
- [ ] 1.2.5 prevent cloning of local/private ip addresses
|
```js
|
||||||
- [ ] 1.3.0 add simple moderation tools
|
// config.json
|
||||||
- [ ] 2.0.0 rewrite using theUnfunny's code as a base/rewrite using monofile-core
|
{
|
||||||
|
"maxDiscordFiles": 20,
|
||||||
also todo: monofile-core (written in eris)
|
"maxDiscordFileSize": 26214400,
|
||||||
|
"targetGuild": "1024080490677936248",
|
||||||
## Disclaimer!
|
"targetChannel": "1024080525993971913",
|
||||||
This project does some stuff that can be considered questionable. Discord may not like you uploading files this way, and it's a grey area in Discord's TOS. We take no responsibility if Discord locks your account for API abuse.
|
"requestTimeout":120000,
|
||||||
|
"maxUploadIdLength":30,
|
||||||
|
|
||||||
|
"accounts": {
|
||||||
|
"registrationEnabled": true,
|
||||||
|
"requiredForUpload": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"webdrop": {
|
||||||
|
"accountRequired": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"mail": { // nodemailer transport options
|
||||||
|
"host": "smtp.fastmail.com", // or your mail provider of choice
|
||||||
|
"port": 465,
|
||||||
|
"secure": true,
|
||||||
|
"auth": {
|
||||||
|
"user": "REPLACE-WITH-YOUR-ALIAS@YOURDOMAIN.COM",
|
||||||
|
"pass": "REPLACE-WITH-YOUR-GENERATED-PASSWORD"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, compile:
|
||||||
|
```
|
||||||
|
tsc && sass src/style:out/style && rollup -c
|
||||||
|
```
|
||||||
|
and start.
|
||||||
|
```
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
monofile should now be running on either `env.MONOFILE_PORT` or port `3000`.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Although we believe monofile is not against Discord's developer terms of service, monofile's contributors are not liable if Discord takes action against you for running an instance.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Code written by monofile's contributors is currently licensed under [Unlicense](https://github.com/nbitzz/monofile/blob/main/LICENSE).
|
||||||
|
|
||||||
|
Icons under `/assets/icons` were created by Microsoft, and as such are licensed under [different terms](https://github.com/nbitzz/monofile/blob/1.3.0/assets/icons/README.md) (MIT).
|
BIN
assets/fonts/FiraCode-Bold.ttf
Normal file
BIN
assets/fonts/FiraCode-Light.ttf
Normal file
BIN
assets/fonts/FiraCode-Medium.ttf
Normal file
BIN
assets/fonts/FiraCode-Regular.ttf
Normal file
BIN
assets/fonts/FiraCode-SemiBold.ttf
Normal file
BIN
assets/fonts/Inconsolata-Black.ttf
Normal file
BIN
assets/fonts/Inconsolata-Bold.ttf
Normal file
BIN
assets/fonts/Inconsolata-ExtraBold.ttf
Normal file
BIN
assets/fonts/Inconsolata-ExtraLight.ttf
Normal file
BIN
assets/fonts/Inconsolata-Light.ttf
Normal file
BIN
assets/fonts/Inconsolata-Medium.ttf
Normal file
BIN
assets/fonts/Inconsolata-Regular.ttf
Normal file
BIN
assets/fonts/Inconsolata-SemiBold.ttf
Normal file
BIN
assets/fonts/SourceSansPro-Bold.ttf
Normal file
BIN
assets/fonts/SourceSansPro-BoldItalic.ttf
Normal file
BIN
assets/fonts/SourceSansPro-Italic.ttf
Normal file
BIN
assets/fonts/SourceSansPro-Regular.ttf
Normal file
BIN
assets/fonts/SourceSansPro-SemiBold.ttf
Normal file
BIN
assets/fonts/SourceSansPro-SemiBoldItalic.ttf
Normal file
34
assets/fonts/fira_code.css
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: url("/static/assets/fonts/FiraCode-Light.ttf");
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: url("/static/assets/fonts/FiraCode-Regular.ttf");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: url("/static/assets/fonts/FiraCode-Medium.ttf");
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: url("/static/assets/fonts/FiraCode-SemiBold.ttf");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: url("/static/assets/fonts/FiraCode-Bold.ttf");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
55
assets/fonts/inconsolata.css
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inconsolata";
|
||||||
|
src: url("/static/assets/fonts/Inconsolata-ExtraLight.ttf");
|
||||||
|
font-weight: 200;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inconsolata";
|
||||||
|
src: url("/static/assets/fonts/Inconsolata-Light.ttf");
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inconsolata";
|
||||||
|
src: url("/static/assets/fonts/Inconsolata-Regular.ttf");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inconsolata";
|
||||||
|
src: url("/static/assets/fonts/Inconsolata-Medium.ttf");
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inconsolata";
|
||||||
|
src: url("/static/assets/fonts/Inconsolata-SemiBold.ttf");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inconsolata";
|
||||||
|
src: url("/static/assets/fonts/Inconsolata-Bold.ttf");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inconsolata";
|
||||||
|
src: url("/static/assets/fonts/Inconsolata-ExtraBold.ttf");
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Inconsolata";
|
||||||
|
src: url("/static/assets/fonts/Inconsolata-Black.ttf");
|
||||||
|
font-weight: 900;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
1
assets/fonts/license
Normal file
|
@ -0,0 +1 @@
|
||||||
|
These fonts are licensed under the OFL
|
41
assets/fonts/source_sans.css
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
src: url("/static/assets/fonts/SourceSansPro-Regular.ttf");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
src: url("/static/assets/fonts/SourceSansPro-Italic.ttf");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
src: url("/static/assets/fonts/SourceSansPro-SemiBold.ttf");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
src: url("/static/assets/fonts/SourceSansPro-SemiBoldItalic.ttf");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
src: url("/static/assets/fonts/SourceSansPro-Bold.ttf");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
src: url("/static/assets/fonts/SourceSansPro-BoldItalic.ttf");
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
28
assets/icons/README.md
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
These icons were originally distributed by Microsoft as part of the Fluent System UI icon collection.
|
||||||
|
https://github.com/microsoft/fluentui-system-icons
|
||||||
|
|
||||||
|
They are licensed under separate terms, those being:
|
||||||
|
|
||||||
|
```
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Microsoft Corporation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
```
|
1
assets/icons/admin/delete_file.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2v6a2 2 0 0 0 2 2h6v10a2 2 0 0 1-2 2h-6.81A6.5 6.5 0 0 0 4 11.498V4a2 2 0 0 1 2-2h6Z" fill="#DDDDDD"/><path d="M13.5 2.5V8a.5.5 0 0 0 .5.5h5.5l-6-6ZM6.5 12a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11Zm2.478 3.731-1.77 1.77 1.77 1.769a.5.5 0 1 1-.707.707l-1.77-1.77-1.769 1.768a.5.5 0 1 1-.707-.708L5.794 17.5 4.025 15.73a.5.5 0 1 1 .707-.707l1.77 1.769 1.77-1.769a.5.5 0 0 1 .706.707Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 510 B |
1
assets/icons/admin/elevate_user.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M14 14.05V14H4.253a2.249 2.249 0 0 0-2.25 2.25v.919c0 .572.18 1.13.511 1.596C4.056 20.929 6.58 22 10 22c.715 0 1.39-.046 2.026-.14A2.51 2.51 0 0 1 12 21.5v-5a2.5 2.5 0 0 1 2-2.45ZM10 2.005a5 5 0 1 1 0 10 5 5 0 0 1 0-10ZM15 15v-1a2.5 2.5 0 0 1 5 0v1h.5a1.5 1.5 0 0 1 1.5 1.5v5a1.5 1.5 0 0 1-1.5 1.5h-6a1.5 1.5 0 0 1-1.5-1.5v-5a1.5 1.5 0 0 1 1.5-1.5h.5Zm1.5-1v1h2v-1a1 1 0 1 0-2 0Zm2 5a1 1 0 1 0-2 0 1 1 0 0 0 2 0Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 540 B |
1
assets/icons/anonymous.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 14A6.47 6.47 0 0 0 11 17.5c0 1.63.6 3.12 1.592 4.261-.796.16-1.66.24-2.592.24-3.42 0-5.944-1.072-7.486-3.237a2.75 2.75 0 0 1-.51-1.595v-.92a2.249 2.249 0 0 1 2.249-2.25h7.77Zm5.478-2a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm0 7.75a.625.625 0 1 0 0 1.25.625.625 0 0 0 0-1.25Zm0-5.876c-1.048 0-1.864.817-1.853 1.954a.5.5 0 1 0 1-.01c-.006-.579.36-.944.853-.944.473 0 .854.392.854.95 0 .192-.056.342-.224.56l-.094.117-.1.113-.265.29-.136.157c-.384.457-.535.793-.535 1.31a.5.5 0 1 0 1 0c0-.203.059-.359.239-.59l.085-.104.1-.116.267-.29.134-.155c.378-.45.529-.783.529-1.293 0-1.103-.823-1.95-1.854-1.95ZM10 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 772 B |
1
assets/icons/change_password.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.95 8.6a6.554 6.554 0 0 1 6.55-6.55c3.596 0 6.55 2.819 6.55 6.45a6.554 6.554 0 0 1-6.55 6.55c-.531 0-1.055-.076-1.552-.204A1.25 1.25 0 0 1 12.7 16.05h-1.75v1.75c0 .69-.56 1.25-1.25 1.25H7.95v1.25a1.75 1.75 0 0 1-1.75 1.75H3.7a1.75 1.75 0 0 1-1.75-1.75v-2.172c0-.73.29-1.429.806-1.944L8.99 9.948a.275.275 0 0 0 .07-.244A6.386 6.386 0 0 1 8.95 8.6Zm9.3-1.6a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 529 B |
1
assets/icons/change_username.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M11 15c0-.35.06-.687.17-1H4.253a2.249 2.249 0 0 0-2.249 2.249v.92c0 .572.179 1.13.51 1.596C4.057 20.929 6.58 22 10 22c.397 0 .783-.014 1.156-.043A2.997 2.997 0 0 1 11 21v-6ZM10 2.005a5 5 0 1 1 0 10 5 5 0 0 1 0-10ZM12 15a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2h-7a2 2 0 0 1-2-2v-6Zm2.5 1a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 496 B |
1
assets/icons/delete.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m4.21 4.387.083-.094a1 1 0 0 1 1.32-.083l.094.083L12 10.585l6.293-6.292a1 1 0 1 1 1.414 1.414L13.415 12l6.292 6.293a1 1 0 0 1 .083 1.32l-.083.094a1 1 0 0 1-1.32.083l-.094-.083L12 13.415l-6.293 6.292a1 1 0 0 1-1.414-1.414L10.585 12 4.293 5.707a1 1 0 0 1-.083-1.32l.083-.094-.083.094Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 410 B |
1
assets/icons/delete_account.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.5 12a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm-5.478 2A6.47 6.47 0 0 0 11 17.5c0 1.644.61 3.145 1.617 4.29-.802.141-1.675.21-2.617.21-2.89 0-5.128-.656-6.691-2a3.75 3.75 0 0 1-1.305-2.843v-.907A2.25 2.25 0 0 1 4.254 14h7.768Zm3.071.966-.07.058-.057.07a.5.5 0 0 0 0 .568l.058.069 1.77 1.77-1.768 1.766-.057.07a.5.5 0 0 0 0 .568l.058.07.069.057a.5.5 0 0 0 .568 0l.07-.058 1.766-1.767 1.77 1.77.069.058a.5.5 0 0 0 .568 0l.07-.058.058-.07a.5.5 0 0 0 0-.568l-.058-.07-1.77-1.768 1.772-1.77.058-.07a.5.5 0 0 0 0-.568l-.058-.069-.069-.058a.5.5 0 0 0-.569 0l-.069.058-1.771 1.77-1.77-1.77-.07-.058a.5.5 0 0 0-.492-.043l-.076.043ZM10 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 791 B |
1
assets/icons/error.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm3.53 6.47-.084-.073a.75.75 0 0 0-.882-.007l-.094.08L12 10.939l-2.47-2.47-.084-.072a.75.75 0 0 0-.882-.007l-.094.08-.073.084a.75.75 0 0 0-.007.882l.08.094L10.939 12l-2.47 2.47-.072.084a.75.75 0 0 0-.007.882l.08.094.084.073a.75.75 0 0 0 .882.007l.094-.08L12 13.061l2.47 2.47.084.072a.75.75 0 0 0 .882.007l.094-.08.073-.084a.75.75 0 0 0 .007-.882l-.08-.094L13.061 12l2.47-2.47.072-.084a.75.75 0 0 0 .007-.882l-.08-.094-.084-.073.084.073Z" fill="#ed8796"/></svg>
|
After Width: | Height: | Size: 635 B |
1
assets/icons/file.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2v6a2 2 0 0 0 2 2h6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h6Z" fill="#DDDDDD"/><path d="M13.5 2.5V8a.5.5 0 0 0 .5.5h5.5l-6-6Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 267 B |
1
assets/icons/file_icon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2v6a2 2 0 0 0 2 2h6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h6Z" fill="#a6da95"/><path d="M13.5 2.5V8a.5.5 0 0 0 .5.5h5.5l-6-6Z" fill="#a6da95"/></svg>
|
After Width: | Height: | Size: 267 B |
1
assets/icons/icon_temp.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m11.256 13 .238-1.5h1.481l-.237 1.5h-1.482Z" fill="#8aadf4"/><path d="M17.75 2.001a2.25 2.25 0 0 1 2.245 2.096L20 4.25v15.498a2.25 2.25 0 0 1-2.096 2.245l-.154.005H6.25a2.25 2.25 0 0 1-2.245-2.096L4 19.75V4.251a2.25 2.25 0 0 1 2.096-2.245l.154-.005h11.5Zm-5.355 13.16a.75.75 0 1 0 1.482.234l.142-.895h.731a.75.75 0 0 0 0-1.5h-.494l.238-1.5h.756a.75.75 0 0 0 0-1.5h-.519l.162-1.025a.75.75 0 1 0-1.481-.234l-.2 1.259h-1.48l.161-1.025a.75.75 0 1 0-1.481-.234l-.2 1.259H9.25a.75.75 0 1 0 0 1.5h.725L9.738 13H8.75a.75.75 0 1 0 0 1.5h.75l-.105.66a.75.75 0 0 0 1.482.235l.142-.895H12.5l-.105.66Z" fill="#8aadf4"/></svg>
|
After Width: | Height: | Size: 716 B |
1
assets/icons/image.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m11.475 13.718.083-.071a.75.75 0 0 1 .874-.007l.093.078 6.928 6.8A3.235 3.235 0 0 1 17.75 21H6.25a3.235 3.235 0 0 1-1.703-.481l6.928-6.801.083-.071-.083.07ZM17.75 3A3.25 3.25 0 0 1 21 6.25v11.5c0 .627-.178 1.213-.485 1.71l-6.939-6.813-.128-.116a2.25 2.25 0 0 0-2.889-.006l-.135.123-6.939 6.811A3.235 3.235 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h11.5Zm-1.998 3a2.252 2.252 0 1 0 0 4.504 2.252 2.252 0 0 0 0-4.504Zm0 1.5a.752.752 0 1 1 0 1.504.752.752 0 0 1 0-1.504Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 595 B |
1
assets/icons/link.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.502 3.003a3.5 3.5 0 0 0-3.5 3.5v6a3.5 3.5 0 0 0 3.5 3.5H7v-2h-.497a1.5 1.5 0 0 1-1.5-1.5v-6a1.5 1.5 0 0 1 1.5-1.5h6a1.5 1.5 0 0 1 1.5 1.5v6a1.5 1.5 0 0 1-1.5 1.5h-1.506v2h1.506a3.5 3.5 0 0 0 3.5-3.5v-6a3.5 3.5 0 0 0-3.5-3.5h-6Z" fill="#DDDDDD"/><path d="M10 11.5a1.5 1.5 0 0 1 1.5-1.5h1.499V8H11.5A3.5 3.5 0 0 0 8 11.5v6a3.5 3.5 0 0 0 3.5 3.5h6a3.5 3.5 0 0 0 3.5-3.5v-6A3.5 3.5 0 0 0 17.5 8h-.495v2h.495a1.5 1.5 0 0 1 1.5 1.5v6a1.5 1.5 0 0 1-1.5 1.5h-6a1.5 1.5 0 0 1-1.5-1.5v-6Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 609 B |
1
assets/icons/logout.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.25 2.75a1.5 1.5 0 0 0-1.5 1.5v15.5a1.5 1.5 0 0 0 1.5 1.5h5.94a6.5 6.5 0 0 1 7.06-10.012V4.25a1.5 1.5 0 0 0-1.5-1.5H6.25Zm2.25 10.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3Zm9 9.75a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11Zm3.5-5.5a.5.5 0 0 1-.5.5h-4.793l1.647 1.646a.5.5 0 0 1-.708.708l-2.5-2.5a.5.5 0 0 1 0-.708l2.5-2.5a.5.5 0 0 1 .708.708L15.707 17H20.5a.5.5 0 0 1 .5.5Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 494 B |
1
assets/icons/logout_all.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7.207 2.543a1 1 0 0 1 0 1.414L5.414 5.75h7.836a8 8 0 1 1-8 8 1 1 0 1 1 2 0 6 6 0 1 0 6-6H5.414l1.793 1.793a1 1 0 0 1-1.414 1.414l-3.5-3.5a1 1 0 0 1 0-1.414l3.5-3.5a1 1 0 0 1 1.414 0Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 311 B |
1
assets/icons/mail.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22 8.608v8.142a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75V8.608l9.652 5.056a.75.75 0 0 0 .696 0L22 8.608ZM5.25 4h13.5a3.25 3.25 0 0 1 3.234 2.924L12 12.154l-9.984-5.23a3.25 3.25 0 0 1 3.048-2.919L5.25 4h13.5-13.5Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 377 B |
1
assets/icons/more.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8 12a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM14 12a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM18 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 232 B |
1
assets/icons/multiselect.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.707 3.293a1 1 0 0 0-1.414 0L4 4.586l-.293-.293a1 1 0 0 0-1.414 1.414l1 1a1 1 0 0 0 1.414 0l2-2a1 1 0 0 0 0-1.414ZM10 16.993h11.003a1 1 0 0 1 .117 1.994l-.117.006H10A1 1 0 0 1 9.883 17l.117-.007ZM10 11h11.003a1 1 0 0 1 .117 1.993l-.117.007H10a1 1 0 0 1-.117-1.993L10 11Zm0-6h11.003a1 1 0 0 1 .117 1.993L21.003 7H10a1 1 0 0 1-.117-1.993L10 5ZM5.293 16.293a1 1 0 0 1 1.414 1.414l-2 2a1 1 0 0 1-1.414 0l-1-1a1 1 0 1 1 1.414-1.414l.293.293 1.293-1.293Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 578 B |
1
assets/icons/paint.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2.25a.75.75 0 0 0-1.5 0V3.5a2.24 2.24 0 0 0-.841.53L2.78 10.91a2.25 2.25 0 0 0 0 3.182L7.66 18.97a2.25 2.25 0 0 0 3.182 0l6.879-6.879a2.25 2.25 0 0 0 0-3.182L12.84 4.03A2.24 2.24 0 0 0 12 3.5V2.25Zm-1.5 3.06v1.44a.75.75 0 0 0 1.5 0V5.31l4.659 4.66a.75.75 0 0 1 0 1.06l-.97.97H3.812l.029-.03L10.5 5.31ZM19.521 13.602a.874.874 0 0 0-1.542 0l-2.008 3.766C14.85 19.466 16.372 22 18.75 22s3.898-2.534 2.78-4.632l-2.009-3.766Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 552 B |
1
assets/icons/person.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.918a2.75 2.75 0 0 1-.513 1.599C17.945 20.929 15.42 22 12 22c-3.422 0-5.945-1.072-7.487-3.237a2.75 2.75 0 0 1-.51-1.595v-.92a2.249 2.249 0 0 1 2.249-2.25h11.501ZM12 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 369 B |
1
assets/icons/pound.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M10.985 3.165a1 1 0 0 0-1.973-.33l-.86 5.163L3.998 8a1 1 0 1 0 .002 2l3.817-.002-.667 4L3 14a1 1 0 1 0 0 2l3.817-.002-.807 4.838a1 1 0 1 0 1.973.329l.862-5.167 4.975-.003-.806 4.84a1 1 0 1 0 1.972.33l.862-5.17L20 15.992a1 1 0 0 0 0-2l-3.819.001.667-4.001L21 9.99a1 1 0 0 0 0-2l-3.818.002.804-4.827a1 1 0 1 0-1.972-.33l-.86 5.159-4.975.003.806-4.832Zm-1.14 6.832 4.976-.003-.667 4.001-4.976.002.667-4Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 528 B |
1
assets/icons/private.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M2.22 2.22a.75.75 0 0 0-.073.976l.073.084 4.034 4.035a9.986 9.986 0 0 0-3.955 5.75.75.75 0 0 0 1.455.364 8.49 8.49 0 0 1 3.58-5.034l1.81 1.81A4 4 0 0 0 14.8 15.86l5.919 5.92a.75.75 0 0 0 1.133-.977l-.073-.084-6.113-6.114.001-.002-6.95-6.946.002-.002-1.133-1.13L3.28 2.22a.75.75 0 0 0-1.06 0ZM12 5.5c-1 0-1.97.148-2.889.425l1.237 1.236a8.503 8.503 0 0 1 9.899 6.272.75.75 0 0 0 1.455-.363A10.003 10.003 0 0 0 12 5.5Zm.195 3.51 3.801 3.8a4.003 4.003 0 0 0-3.801-3.8Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 592 B |
1
assets/icons/public.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 9.005a4 4 0 1 1 0 8 4 4 0 0 1 0-8ZM12 5.5c4.613 0 8.596 3.15 9.701 7.564a.75.75 0 1 1-1.455.365 8.503 8.503 0 0 0-16.493.004.75.75 0 0 1-1.455-.363A10.003 10.003 0 0 1 12 5.5Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 307 B |
1
assets/icons/refresh.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10Zm3.27-11.25H14a.75.75 0 0 0 0 1.5h2.75a.75.75 0 0 0 .75-.75V8.25a.75.75 0 0 0-1.5 0V9a4.991 4.991 0 0 0-4-2c-1.537 0-2.904.66-3.827 1.77a.75.75 0 0 0 1.154.96C9.963 8.963 10.907 8.5 12 8.5c1.492 0 2.767.934 3.27 2.25Zm-7.27 5V15a5.013 5.013 0 0 0 7.821.237.75.75 0 1 0-1.142-.972 3.513 3.513 0 0 1-5.842-.765H10a.75.75 0 0 0 0-1.5H7.25a.75.75 0 0 0-.75.75v3a.75.75 0 0 0 1.5 0Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 578 B |
1
assets/icons/small_image.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M21.25 13a.75.75 0 0 1 .743.648l.007.102v5a3.25 3.25 0 0 1-3.066 3.245L18.75 22h-4.668c.536-.385.973-.9 1.265-1.499l3.403-.001a1.75 1.75 0 0 0 1.744-1.607l.006-.143v-5a.75.75 0 0 1 .75-.75ZM9.447 17.165l.114.103 4.085 4.086.023.019A3.235 3.235 0 0 1 11.75 22h-6.5a3.235 3.235 0 0 1-1.92-.627l.024-.02 4.085-4.085.114-.103a1.5 1.5 0 0 1 1.894 0ZM11.75 9A3.25 3.25 0 0 1 15 12.25v6.5c0 .718-.233 1.382-.627 1.92l-.02-.024-4.085-4.085-.13-.122a2.5 2.5 0 0 0-3.269-.006l-.137.128-4.086 4.085-.019.023A3.235 3.235 0 0 1 2 18.75v-6.5A3.25 3.25 0 0 1 5.25 9h6.5ZM11 12a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm7.75-10a3.25 3.25 0 0 1 3.245 3.066L22 5.25v5a.75.75 0 0 1-1.493.102l-.007-.102v-5a1.75 1.75 0 0 0-1.606-1.744L18.75 3.5h-5a.75.75 0 0 1-.102-1.493L13.75 2h5Zm-8.5 0a.75.75 0 0 1 .102 1.493l-.102.007h-5a1.75 1.75 0 0 0-1.744 1.606L3.5 5.25v3.402c-.6.292-1.114.73-1.5 1.266V5.25a3.25 3.25 0 0 1 3.066-3.245L5.25 2h5Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 1 KiB |
1
assets/icons/tag.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465ZM17 5.502a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 358 B |
1
assets/icons/tag_remove.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-.026.026a6.5 6.5 0 0 0-9.028 8.92 3.256 3.256 0 0 1-4.043-.442L3.489 16.06a3.25 3.25 0 0 1-.004-4.596l8.5-8.51a3.25 3.25 0 0 1 2.3-.953h5.465ZM17 5.502a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3ZM23 17.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0Zm-7.147-2.354a.5.5 0 0 0-.707.708l1.647 1.646-1.647 1.646a.5.5 0 0 0 .707.708l1.647-1.647 1.646 1.647a.5.5 0 0 0 .707-.708L18.207 17.5l1.646-1.646a.5.5 0 0 0-.707-.708L17.5 16.793l-1.647-1.647Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 623 B |
1
assets/icons/update.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22 12.001c0-5.523-4.476-10-10-10-5.522 0-10 4.477-10 10s4.478 10 10 10c5.524 0 10-4.477 10-10Zm-14.53.28a.75.75 0 0 1-.073-.976l.073-.085 4-4a.75.75 0 0 1 .977-.073l.085.073 4 4.001a.75.75 0 0 1-.977 1.133l-.084-.072-2.72-2.722v6.691a.75.75 0 0 1-.649.744L12 17a.75.75 0 0 1-.743-.648l-.007-.102v-6.69l-2.72 2.72a.75.75 0 0 1-.976.073l-.084-.073Z" fill="#DDDDDD"/></svg>
|
After Width: | Height: | Size: 475 B |
|
@ -1,30 +0,0 @@
|
||||||
document.getElementById("uploadButton").addEventListener("click",() => {
|
|
||||||
let ask = prompt("Input a URL to clone.")
|
|
||||||
|
|
||||||
if (ask) {
|
|
||||||
let opt = getOptionsForUploading()
|
|
||||||
updateBtnTxt("Requesting clone. Please wait.")
|
|
||||||
|
|
||||||
let xmlhttp = new XMLHttpRequest()
|
|
||||||
|
|
||||||
xmlhttp.addEventListener("error",function(e) {
|
|
||||||
updateBtnTxt(`Upload failed.<br/>${e.toString()}`)
|
|
||||||
console.error(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
xmlhttp.addEventListener("load",function() {
|
|
||||||
if (xmlhttp.status == 200) {
|
|
||||||
document.getElementById("CopyTB").value = `https://${location.hostname}/download/${xmlhttp.responseText}`
|
|
||||||
updateBtnTxt(`Upload complete.<br/><a style="color:blue;font-family:monospace;" href="javascript:document.getElementById('CopyTB').focus();document.getElementById('CopyTB').select();document.execCommand('copy');document.getElementById('CopyTB').blur();">Copy URL</a> <a style="color:blue;font-family:monospace;" href="javascript:prompt('This is your download URL.', document.getElementById('CopyTB').value);null">View URL</a>`)
|
|
||||||
} else {
|
|
||||||
updateBtnTxt(`Upload failed.<br/>${xmlhttp.responseText}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
xmlhttp.open("POST","/clone")
|
|
||||||
xmlhttp.send(JSON.stringify({
|
|
||||||
url: ask,
|
|
||||||
...opt
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
|
@ -1,37 +0,0 @@
|
||||||
let FileUpload = document.createElement("input")
|
|
||||||
FileUpload.setAttribute("type","file")
|
|
||||||
|
|
||||||
document.getElementById("uploadButton").addEventListener("click",() => FileUpload.click())
|
|
||||||
|
|
||||||
FileUpload.addEventListener("input",() => {
|
|
||||||
if (FileUpload.files[0]) {
|
|
||||||
let opt = getOptionsForUploading()
|
|
||||||
let file = FileUpload.files[0]
|
|
||||||
|
|
||||||
updateBtnTxt("Uploading file. This may take a while, so stay put.")
|
|
||||||
|
|
||||||
let xmlhttp = new XMLHttpRequest()
|
|
||||||
|
|
||||||
xmlhttp.addEventListener("error",function(e) {
|
|
||||||
updateBtnTxt(`Upload failed.<br/>${e.toString()}`)
|
|
||||||
console.error(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
xmlhttp.addEventListener("load",function() {
|
|
||||||
if (xmlhttp.status == 200) {
|
|
||||||
document.getElementById("CopyTB").value = `https://${location.hostname}/download/${xmlhttp.responseText}`
|
|
||||||
updateBtnTxt(`Upload complete.<br/><a style="color:blue;font-family:monospace;" href="javascript:document.getElementById('CopyTB').focus();document.getElementById('CopyTB').select();document.execCommand('copy');document.getElementById('CopyTB').blur();">Copy URL</a> <a style="color:blue;font-family:monospace;" href="javascript:prompt('This is your download URL.', document.getElementById('CopyTB').value);null">View URL</a>`)
|
|
||||||
} else {
|
|
||||||
updateBtnTxt(`Upload failed.<br/>${xmlhttp.responseText}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let fd = new FormData()
|
|
||||||
fd.append('file',file)
|
|
||||||
|
|
||||||
xmlhttp.open("POST","/upload")
|
|
||||||
xmlhttp.setRequestHeader("monofile-upload-id",opt.uploadId)
|
|
||||||
xmlhttp.send(fd)
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
33
config.json
|
@ -1,7 +1,28 @@
|
||||||
{
|
{
|
||||||
"maxDiscordFiles": 20,
|
"maxDiscordFiles": 20,
|
||||||
"maxDiscordFileSize": 8388608,
|
"maxDiscordFileSize": 26214400,
|
||||||
"targetGuild": "1024080490677936248",
|
"targetGuild": "1024080490677936248",
|
||||||
"targetChannel": "1024080525993971913",
|
"targetChannel": "1024080525993971913",
|
||||||
"requestTimeout":120000
|
"requestTimeout":120000,
|
||||||
|
"maxUploadIdLength":30,
|
||||||
|
|
||||||
|
"accounts": {
|
||||||
|
"registrationEnabled": true,
|
||||||
|
"requiredForUpload": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"webdrop": {
|
||||||
|
"accountRequired": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"mail": {
|
||||||
|
"transport": {
|
||||||
|
"host": "smtp.fastmail.com",
|
||||||
|
"port": 465,
|
||||||
|
"secure": true
|
||||||
|
},
|
||||||
|
"send": {
|
||||||
|
"from": "mono@fyle.uk"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
5021
package-lock.json
generated
69
package.json
|
@ -1,28 +1,41 @@
|
||||||
{
|
{
|
||||||
"name": "monofile",
|
"name": "monofile",
|
||||||
"version": "1.2.3",
|
"version": "1.3.0",
|
||||||
"description": "Discord-based file sharing",
|
"description": "Discord-based file sharing",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./out/index.js",
|
"start": "node ./out/server/index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "nbitzz",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=v18"
|
"node": ">=v18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/body-parser": "^1.19.2",
|
"@types/body-parser": "^1.19.2",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"axios": "^0.27.2",
|
"@types/nodemailer": "^6.4.8",
|
||||||
"body-parser": "^1.20.0",
|
"axios": "^0.27.2",
|
||||||
"discord.js": "^14.7.1",
|
"body-parser": "^1.20.0",
|
||||||
"dotenv": "^16.0.2",
|
"bytes": "^3.1.2",
|
||||||
"express": "^4.18.1",
|
"cookie-parser": "^1.4.6",
|
||||||
"multer": "^1.4.5-lts.1",
|
"discord.js": "^14.7.1",
|
||||||
"typescript": "^4.8.3"
|
"dotenv": "^16.0.2",
|
||||||
}
|
"express": "^4.18.1",
|
||||||
}
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"nodemailer": "^6.9.3",
|
||||||
|
"typescript": "^4.8.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
|
"@types/bytes": "^3.1.1",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
|
"rollup": "^3.11.0",
|
||||||
|
"rollup-plugin-svelte": "^7.1.0",
|
||||||
|
"sass": "^1.57.1",
|
||||||
|
"svelte": "^3.55.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
173
pages/base.html
|
@ -1,173 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>monofile</title>
|
|
||||||
|
|
||||||
<meta name="og:site_name" content="$Version">
|
|
||||||
<meta name="application-name" content="monofile">
|
|
||||||
<meta name="description" content="Discord-based filesharing">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
* {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
h1,h2 {
|
|
||||||
text-align:center;
|
|
||||||
}
|
|
||||||
#content {
|
|
||||||
position:fixed;
|
|
||||||
left:50%;
|
|
||||||
top:50%;
|
|
||||||
transform:translate(-50%,-50%);
|
|
||||||
background-color:white;
|
|
||||||
width:450px;
|
|
||||||
/*height:100%;*/
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
background-color: darkgray;
|
|
||||||
}
|
|
||||||
#btnContainer {
|
|
||||||
width:calc( 100% - 50px );
|
|
||||||
height:50px;
|
|
||||||
position:relative;
|
|
||||||
left:50%;
|
|
||||||
transform:translate(-50%,0);
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
color:black;
|
|
||||||
font-weight:bold;
|
|
||||||
border:none;
|
|
||||||
position:absolute;
|
|
||||||
font-size:20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
border: 1px solid gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
#uploadButton {
|
|
||||||
width:calc( 100% - 50px );
|
|
||||||
height:100%;
|
|
||||||
background-color: #66AAFF;
|
|
||||||
left:0px;
|
|
||||||
top:0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#optionsButton {
|
|
||||||
width:50px;
|
|
||||||
height:100%;
|
|
||||||
background-color: #AAAAAA;
|
|
||||||
left:calc( 100% - 50px );
|
|
||||||
top:0px;
|
|
||||||
font-size:15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
padding:5px;
|
|
||||||
position:relative;
|
|
||||||
width:calc( 100% - 62px );
|
|
||||||
left:25px;
|
|
||||||
border:1px solid #AAAAAAFF;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #AAAAAA66;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* this is horrible css lmao */
|
|
||||||
|
|
||||||
#options > input {
|
|
||||||
font-family:monospace;
|
|
||||||
width:250px;
|
|
||||||
font-size:14px;
|
|
||||||
border: 1px solid #777777;
|
|
||||||
background-color:#AAAAAA;
|
|
||||||
outline:none;
|
|
||||||
text-align:center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#options {
|
|
||||||
height:30px;
|
|
||||||
display:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#customId {
|
|
||||||
position:absolute;
|
|
||||||
left:50%;
|
|
||||||
top:50%;
|
|
||||||
transform:translate(-50%,-50%)
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 450px) {
|
|
||||||
#content {
|
|
||||||
position:fixed;
|
|
||||||
left:0%;
|
|
||||||
top:0%;
|
|
||||||
transform:translate(0%,0%);
|
|
||||||
background-color:white;
|
|
||||||
width:100%;
|
|
||||||
height:100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<input type="text" id="CopyTB" value="" readonly style="opacity:0;width:10px;height:0%;">
|
|
||||||
<div id="content">
|
|
||||||
<h1>
|
|
||||||
monofile<span style="font-style:italic;font-weight:bold;font-size:16px;color:#999999"> $Version</span>
|
|
||||||
</h1>
|
|
||||||
<h2><em>Discord-based file sharing</em></h2>
|
|
||||||
<div class="note" style="border-color:#FFAAAAFF;background-color:#FFAAAA66">
|
|
||||||
Before sharing files, please remember:
|
|
||||||
<ul>
|
|
||||||
<li>Do NOT share sensitive information via monofile</li>
|
|
||||||
<li>Do NOT share illegal content via monofile</li>
|
|
||||||
<li>The owner of this instance reserves the right to remove your files</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div style="width:100%;height:25px"></div>
|
|
||||||
<div id="btnContainer">
|
|
||||||
<button id="uploadButton">$UploadButtonText</button>
|
|
||||||
<button id="optionsButton">• • •</button>
|
|
||||||
</div>
|
|
||||||
<div id="options" class="note" style="border-radius:0px;">
|
|
||||||
<input id="customId" autocomplete="off" placeholder = "custom id (30char, no spaces)" maxlength="30">
|
|
||||||
</div>
|
|
||||||
<p style="font-family:monospace;position:relative;width:calc( 100% - 50px );left:25px;text-align: center;">
|
|
||||||
Max filesize on instance: $MaxInstanceFilesize
|
|
||||||
<br />
|
|
||||||
Hosting <strong style="font-family:monospace">$FileNum</strong> files
|
|
||||||
</p>
|
|
||||||
<p style="font-family:monospace;position:relative;width:calc( 100% - 50px );left:25px;text-align: center;font-size:12px;color:gray;">made by nbitzz — <a style="color:gray;font-family:monospace;font-size:12px;" href="https://github.com/nbitzz/monofile">github</a> — <a style="color:gray;font-family:monospace;font-size:12px;" href="$otherPath">$otherText</a></p>
|
|
||||||
<div style="width:100%;height:25px"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<!-- bad thing to do but i'm being rushed -->
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let uploading = false
|
|
||||||
const updateBtnTxt = (btntxt) => {document.getElementById("btnContainer").innerHTML = `<div class="note" style="width:calc( 100% - 10px );height:calc( 100% - 10px );position:absolute;left:0px;top:0px;"><p style="font-family:monospace;position:relative;width:calc( 100% - 50px );left:25px;text-align: center;">${btntxt}</p></div>`}
|
|
||||||
const getOptionsForUploading = () => {
|
|
||||||
uploading = true
|
|
||||||
document.getElementById("options").style.display = "none"
|
|
||||||
|
|
||||||
return {
|
|
||||||
uploadId: document.getElementById("customId").value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("optionsButton").addEventListener("click",() => {
|
|
||||||
let display = document.getElementById("options").style.display
|
|
||||||
if (!uploading) document.getElementById("options").style.display = !display || display == "none" ? "block" : "none"
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script src="/static/script/$Handler.js"></script>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,102 +1,54 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>monofile</title>
|
<title>$FileId</title>
|
||||||
|
|
||||||
<meta name="og:site_name" content="monofile $Version">
|
<!--metaTags-->
|
||||||
<meta name="title" content="$FileName">
|
|
||||||
<meta name="description" content="ID: $FileId">
|
<meta name="og:site_name" content="$Uploader">
|
||||||
|
<meta name="title" content="$FileName">
|
||||||
<style>
|
<meta name="description" content="$FileSize file on monofile $Version, the Discord-based file sharing service">
|
||||||
|
|
||||||
* {
|
<link
|
||||||
font-family: sans-serif;
|
rel="stylesheet"
|
||||||
}
|
href="/static/style/downloads.css"
|
||||||
h1,h2 {
|
>
|
||||||
text-align:center;
|
|
||||||
}
|
<link
|
||||||
#content {
|
rel="stylesheet"
|
||||||
position:fixed;
|
href="/auth/customCSS"
|
||||||
left:50%;
|
>
|
||||||
top:50%;
|
|
||||||
transform:translate(-50%,-50%);
|
<link
|
||||||
background-color:white;
|
rel="icon"
|
||||||
width:450px;
|
type="image/svg"
|
||||||
/*height:100%;*/
|
href="/static/assets/icons/file_icon.svg"
|
||||||
}
|
>
|
||||||
body {
|
|
||||||
background-color: darkgray;
|
</head>
|
||||||
}
|
<body>
|
||||||
#btnContainer {
|
<div id="appContent">
|
||||||
width:calc( 100% - 50px );
|
<div id="uploadWindow">
|
||||||
height:50px;
|
<h1>
|
||||||
position:relative;
|
$FileName
|
||||||
left:50%;
|
</h1>
|
||||||
transform:translate(-50%,0);
|
<p style="color:#999999">
|
||||||
}
|
<span class="number">$FileSize</span> — uploaded by <span class="number">$Uploader</span>
|
||||||
#dlbtn {
|
</p>
|
||||||
width:100%;
|
|
||||||
height:100%;
|
<!--preview-->
|
||||||
background-color: #66AAFF;
|
|
||||||
border:none;
|
<button style="position:relative;width:100%;top:10px;">
|
||||||
position:absolute;
|
<a id="dlbtn" href="/file/$FileId" download="$FileName" style="position:absolute;left:0px;top:0px;height:100%;width:100%;"></a>
|
||||||
left:0px;
|
download
|
||||||
top:0px;
|
</button>
|
||||||
text-decoration: none;
|
|
||||||
display:flex; /* This is a mess but I give up. */
|
<div style="min-height:15px" />
|
||||||
flex-direction: column;
|
</div>
|
||||||
justify-content: center;
|
</div>
|
||||||
}
|
</body>
|
||||||
#btnContainer:hover {
|
|
||||||
outline: 1px solid gray;
|
|
||||||
}
|
|
||||||
#dlbtn > p {
|
|
||||||
color:black;
|
|
||||||
font-weight:bold;
|
|
||||||
font-size:20px;
|
|
||||||
text-align: center;
|
|
||||||
width:100%;
|
|
||||||
line-height:20px;
|
|
||||||
}
|
|
||||||
.note {
|
|
||||||
padding:5px;
|
|
||||||
position:relative;
|
|
||||||
width:calc( 100% - 60px );
|
|
||||||
left:25px;
|
|
||||||
border:1px solid #AAAAAAFF;
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #AAAAAA66;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 450px) {
|
|
||||||
#content {
|
|
||||||
position:fixed;
|
|
||||||
left:0%;
|
|
||||||
top:0%;
|
|
||||||
transform:translate(0%,0%);
|
|
||||||
background-color:white;
|
|
||||||
width:100%;
|
|
||||||
height:100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="content">
|
|
||||||
<h1>
|
|
||||||
$FileName
|
|
||||||
</h1>
|
|
||||||
<h2 style="font-family:monospace">file id: $FileId</h2>
|
|
||||||
<div id="btnContainer"><a id="dlbtn" href="/file/$FileId" download="$FileName"><p>Download file</p></a></div>
|
|
||||||
<p style="font-family:monospace;position:relative;width:calc( 100% - 50px );left:25px;text-align: center;">
|
|
||||||
May take a while to download. Stay put.
|
|
||||||
</p>
|
|
||||||
<div style="width:100%;height:25px"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
111
pages/error.html
|
@ -1,72 +1,41 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
|
||||||
<head>
|
<html lang="en">
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
|
<link
|
||||||
<meta name="og:site_name" content="monofile $Version">
|
rel="stylesheet"
|
||||||
<meta name="application-name" content="$ErrorCode">
|
href="/static/style/error.css"
|
||||||
<meta name="description" content="$ErrorMessage">
|
>
|
||||||
|
|
||||||
<title>monofile</title>
|
<link
|
||||||
|
rel="icon"
|
||||||
<style>
|
type="image/svg"
|
||||||
|
href="/static/assets/icons/error.svg"
|
||||||
* {
|
>
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
<link
|
||||||
h1,h2 {
|
rel="stylesheet"
|
||||||
text-align:center;
|
href="/auth/customCSS"
|
||||||
}
|
>
|
||||||
#content {
|
|
||||||
position:fixed;
|
<meta
|
||||||
left:50%;
|
name="viewport"
|
||||||
top:50%;
|
content="width=device-width, initial-scale=1.0, user-scalable=0"
|
||||||
transform:translate(-50%,-50%);
|
>
|
||||||
background-color:white;
|
|
||||||
width:450px;
|
<title>$code</title>
|
||||||
/*height:100%;*/
|
|
||||||
}
|
<meta name="theme-color" content="rgb(30, 33, 36)">
|
||||||
body {
|
|
||||||
background-color: darkgray;
|
</head>
|
||||||
}
|
|
||||||
.note {
|
<body>
|
||||||
padding:5px;
|
<p class="error">
|
||||||
position:relative;
|
<span class="code">$code</span>
|
||||||
width:calc( 100% - 60px );
|
$text
|
||||||
left:25px;
|
</p>
|
||||||
border:1px solid #AAAAAAFF;
|
</body>
|
||||||
border-radius: 8px;
|
|
||||||
background-color: #AAAAAA66;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 450px) {
|
|
||||||
#content {
|
|
||||||
position:fixed;
|
|
||||||
left:0%;
|
|
||||||
top:0%;
|
|
||||||
transform:translate(0%,0%);
|
|
||||||
background-color:white;
|
|
||||||
width:100%;
|
|
||||||
height:100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="content">
|
|
||||||
<h1>
|
|
||||||
monofile<span style="font-style:italic;font-weight:bold;font-size:16px;color:#999999"> $Version</span>
|
|
||||||
</h1>
|
|
||||||
<h2><em>Discord-based file sharing</em></h2>
|
|
||||||
<div class="note" style="border-color:#FFAAAAFF;background-color:#FFAAAA66">
|
|
||||||
<h1>$ErrorCode</h1>
|
|
||||||
<p style="width:calc( 100% - 50px );left:25px;position:relative;">$ErrorMessage</p>
|
|
||||||
</div>
|
|
||||||
<div style="width:100%;height:25px"></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
47
pages/index.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<!--
|
||||||
|
for some reason (don't know why)
|
||||||
|
certain things break
|
||||||
|
when not in quirks mode
|
||||||
|
so i'm not adding in the
|
||||||
|
doctype html
|
||||||
|
-->
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="/static/style/app.css"
|
||||||
|
>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/svg"
|
||||||
|
href="/static/assets/icons/icon_temp.svg"
|
||||||
|
>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="/auth/customCSS"
|
||||||
|
>
|
||||||
|
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, user-scalable=0"
|
||||||
|
>
|
||||||
|
|
||||||
|
<script type="module" src="/static/js/index.js"></script>
|
||||||
|
|
||||||
|
<title>monofile</title>
|
||||||
|
|
||||||
|
<meta name="title" content="monofile">
|
||||||
|
<meta name="description" content="The open-source Discord-based file sharing service">
|
||||||
|
<meta name="theme-color" content="rgb(30, 33, 36)">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
17
rollup.config.mjs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import svelte from 'rollup-plugin-svelte'
|
||||||
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
input: "src/client/index.js",
|
||||||
|
output: {
|
||||||
|
file: 'out/client/index.js',
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap:true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
resolve({ browser: true }),
|
||||||
|
svelte({})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
5
src/client/index.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import App from "../svelte/App.svelte"
|
||||||
|
|
||||||
|
new App({
|
||||||
|
target: document.body
|
||||||
|
})
|
172
src/index.ts
|
@ -1,172 +0,0 @@
|
||||||
/*
|
|
||||||
i really should split this up into different modules
|
|
||||||
*/
|
|
||||||
|
|
||||||
import bodyParser from "body-parser"
|
|
||||||
import multer, {memoryStorage} from "multer"
|
|
||||||
import Discord, { IntentsBitField, Client } from "discord.js"
|
|
||||||
import express from "express"
|
|
||||||
import fs from "fs"
|
|
||||||
import axios, { AxiosResponse } from "axios"
|
|
||||||
|
|
||||||
import Files from "./lib/files"
|
|
||||||
require("dotenv").config()
|
|
||||||
|
|
||||||
const multerSetup = multer({storage:memoryStorage()})
|
|
||||||
let pkg = require(`${process.cwd()}/package.json`)
|
|
||||||
let app = express()
|
|
||||||
let config = require(`${process.cwd()}/config.json`)
|
|
||||||
|
|
||||||
app.use("/static",express.static("assets"))
|
|
||||||
app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
|
|
||||||
// funcs
|
|
||||||
|
|
||||||
function ThrowError(response:express.Response,code:number,errorMessage:string) {
|
|
||||||
fs.readFile(__dirname+"/../pages/error.html",(err,buf) => {
|
|
||||||
if (err) {response.sendStatus(500);console.log(err);return}
|
|
||||||
response.status(code)
|
|
||||||
response.send(buf.toString().replace(/\$ErrorCode/g,code.toString()).replace(/\$ErrorMessage/g,errorMessage).replace(/\$Version/g,pkg.version))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// init data
|
|
||||||
|
|
||||||
if (!fs.existsSync(__dirname+"/../.data/")) fs.mkdirSync(__dirname+"/../.data/")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// discord
|
|
||||||
|
|
||||||
let client = new Client({intents:[
|
|
||||||
IntentsBitField.Flags.GuildMessages,
|
|
||||||
IntentsBitField.Flags.MessageContent
|
|
||||||
],rest:{timeout:config.requestTimeout}})
|
|
||||||
|
|
||||||
let files = new Files(client,config)
|
|
||||||
|
|
||||||
// routes (could probably make these use routers)
|
|
||||||
|
|
||||||
// index, clone
|
|
||||||
|
|
||||||
app.get("/", function(req,res) {
|
|
||||||
fs.readFile(__dirname+"/../pages/base.html",(err,buf) => {
|
|
||||||
if (err) {res.sendStatus(500);console.log(err);return}
|
|
||||||
res.send(
|
|
||||||
buf.toString()
|
|
||||||
.replace("$MaxInstanceFilesize",`${(config.maxDiscordFileSize*config.maxDiscordFiles)/1048576}MB`)
|
|
||||||
.replace(/\$Version/g,pkg.version)
|
|
||||||
.replace(/\$Handler/g,"upload_file")
|
|
||||||
.replace(/\$UploadButtonText/g,"Upload file")
|
|
||||||
.replace(/\$otherPath/g,"/clone")
|
|
||||||
.replace(/\$otherText/g,"clone from url...")
|
|
||||||
.replace(/\$FileNum/g,Object.keys(files.files).length.toString())
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get("/clone", function(req,res) {
|
|
||||||
fs.readFile(__dirname+"/../pages/base.html",(err,buf) => {
|
|
||||||
if (err) {res.sendStatus(500);console.log(err);return}
|
|
||||||
res.send(
|
|
||||||
buf.toString()
|
|
||||||
.replace("$MaxInstanceFilesize",`${(config.maxDiscordFileSize*config.maxDiscordFiles)/1048576}MB`)
|
|
||||||
.replace(/\$Version/g,pkg.version)
|
|
||||||
.replace(/\$Handler/g,"clone_file")
|
|
||||||
.replace(/\$UploadButtonText/g,"Input a URL")
|
|
||||||
.replace(/\$otherPath/g,"/")
|
|
||||||
.replace(/\$otherText/g,"upload file...")
|
|
||||||
.replace(/\$FileNum/g,Object.keys(files.files).length.toString())
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// upload handlers
|
|
||||||
|
|
||||||
app.post("/upload",multerSetup.single('file'),async (req,res) => {
|
|
||||||
if (req.file) {
|
|
||||||
try {
|
|
||||||
files.uploadFile({name:req.file.originalname,mime:req.file.mimetype,uploadId:req.header("monofile-upload-id")},req.file.buffer)
|
|
||||||
.then((uID) => res.send(uID))
|
|
||||||
.catch((stat) => {res.status(stat.status);res.send(`[err] ${stat.message}`)})
|
|
||||||
} catch {
|
|
||||||
res.status(400)
|
|
||||||
res.send("[err] bad request")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.status(400)
|
|
||||||
res.send("[err] bad request")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.post("/clone",(req,res) => {
|
|
||||||
try {
|
|
||||||
let j = JSON.parse(req.body)
|
|
||||||
if (!j.url) {
|
|
||||||
res.status(400)
|
|
||||||
res.send("[err] invalid url")
|
|
||||||
}
|
|
||||||
axios.get(j.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => {
|
|
||||||
files.uploadFile({name:j.url.split("/")[req.body.split("/").length-1] || "generic",mime:data.headers["content-type"],uploadId:j.uploadId},Buffer.from(data.data))
|
|
||||||
.then((uID) => res.send(uID))
|
|
||||||
.catch((stat) => {res.status(stat.status);res.send(`[err] ${stat.message}`)})
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
res.status(400)
|
|
||||||
res.send(`[err] failed to fetch data`)
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
res.status(500)
|
|
||||||
res.send("[err] an error occured")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// serve files & download page
|
|
||||||
|
|
||||||
app.get("/download/:fileId",(req,res) => {
|
|
||||||
if (files.getFilePointer(req.params.fileId)) {
|
|
||||||
let file = files.getFilePointer(req.params.fileId)
|
|
||||||
|
|
||||||
fs.readFile(__dirname+"/../pages/download.html",(err,buf) => {
|
|
||||||
if (err) {res.sendStatus(500);console.log(err);return}
|
|
||||||
res.send(
|
|
||||||
buf.toString()
|
|
||||||
.replace(/\$FileId/g,req.params.fileId)
|
|
||||||
.replace(/\$Version/g,pkg.version)
|
|
||||||
.replace(/\$FileName/g,
|
|
||||||
file.filename
|
|
||||||
.replace(/\&/g,"&")
|
|
||||||
.replace(/\</g,"<")
|
|
||||||
.replace(/\>/g,">")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
ThrowError(res,404,"File not found.")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get("/file/:fileId",async (req,res) => {
|
|
||||||
files.readFileStream(req.params.fileId).then(f => {
|
|
||||||
res.setHeader("Content-Type",f.contentType)
|
|
||||||
res.status(200)
|
|
||||||
f.dataStream.pipe(res)
|
|
||||||
}).catch((err) => {
|
|
||||||
ThrowError(res,err.status,err.message)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get("*",(req,res) => {
|
|
||||||
ThrowError(res,404,"Page not found.")
|
|
||||||
})
|
|
||||||
|
|
||||||
app.get("/server",(req,res) => {
|
|
||||||
res.send(JSON.stringify({...config,version:pkg.version}))
|
|
||||||
})
|
|
||||||
|
|
||||||
// listen on 3000 or MONOFILE_PORT
|
|
||||||
|
|
||||||
app.listen(process.env.MONOFILE_PORT || 3000,function() {
|
|
||||||
console.log("Web OK!")
|
|
||||||
})
|
|
||||||
|
|
||||||
client.login(process.env.TOKEN)
|
|
246
src/lib/files.ts
|
@ -1,246 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
import Discord, { Client, TextBasedChannel } from "discord.js";
|
|
||||||
import { readFile, writeFile } from "fs";
|
|
||||||
import { Readable } from "node:stream"
|
|
||||||
|
|
||||||
export let id_check_regex = /[A-Za-z0-9_\-\.]+/
|
|
||||||
|
|
||||||
|
|
||||||
export interface FileUploadSettings {
|
|
||||||
name?: string,
|
|
||||||
mime: string,
|
|
||||||
uploadId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Configuration {
|
|
||||||
maxDiscordFiles: number,
|
|
||||||
maxDiscordFileSize: number,
|
|
||||||
targetGuild: string,
|
|
||||||
targetChannel: string,
|
|
||||||
requestTimeout: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FilePointer {
|
|
||||||
filename:string,
|
|
||||||
mime:string,
|
|
||||||
messageids:string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StatusCodeError {
|
|
||||||
status: number,
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/* */
|
|
||||||
|
|
||||||
export default class Files {
|
|
||||||
|
|
||||||
config: Configuration
|
|
||||||
client: Client
|
|
||||||
files: {[key:string]:FilePointer} = {}
|
|
||||||
uploadChannel?: TextBasedChannel
|
|
||||||
|
|
||||||
constructor(client: Client, config: Configuration) {
|
|
||||||
|
|
||||||
this.config = config;
|
|
||||||
this.client = client;
|
|
||||||
|
|
||||||
client.on("ready",() => {
|
|
||||||
console.log("Discord OK!")
|
|
||||||
|
|
||||||
client.guilds.fetch(config.targetGuild).then((g) => {
|
|
||||||
g.channels.fetch(config.targetChannel).then((a) => {
|
|
||||||
if (a?.isTextBased()) {
|
|
||||||
this.uploadChannel = a
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
readFile(process.cwd()+"/.data/files.json",(err,buf) => {
|
|
||||||
if (err) {console.log(err);return}
|
|
||||||
this.files = JSON.parse(buf.toString() || "{}")
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise<string|StatusCodeError> {
|
|
||||||
return new Promise<string>(async (resolve,reject) => {
|
|
||||||
if (!this.uploadChannel) {
|
|
||||||
reject({status:503,message:"server is not ready - please try again later"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!settings.name || !settings.mime) {
|
|
||||||
reject({status:400,message:"missing name/mime"});
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let uploadId = (settings.uploadId || Math.random().toString().slice(2)).toString();
|
|
||||||
|
|
||||||
if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > 30) {
|
|
||||||
reject({status:400,message:"invalid id"});return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.files[uploadId]) {
|
|
||||||
reject({status:400,message:"a file with this id already exists"});
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.name.length > 128) {
|
|
||||||
reject({status:400,message:"name too long"});
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.mime.length > 128) {
|
|
||||||
reject({status:400,message:"mime too long"});
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get buffer
|
|
||||||
if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) {
|
|
||||||
reject({status:400,message:"file too large"});
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate buffers to upload
|
|
||||||
let toUpload = []
|
|
||||||
for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) {
|
|
||||||
toUpload.push(
|
|
||||||
fBuffer.subarray(
|
|
||||||
i*this.config.maxDiscordFileSize,
|
|
||||||
Math.min(
|
|
||||||
fBuffer.byteLength,
|
|
||||||
(i+1)*this.config.maxDiscordFileSize
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// begin uploading
|
|
||||||
let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => {
|
|
||||||
return new Discord.AttachmentBuilder(e)
|
|
||||||
.setName(Math.random().toString().slice(2))
|
|
||||||
})
|
|
||||||
let uploadGroups = []
|
|
||||||
for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) {
|
|
||||||
uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10)))
|
|
||||||
}
|
|
||||||
|
|
||||||
let msgIds = []
|
|
||||||
|
|
||||||
for (let i = 0; i < uploadGroups.length; i++) {
|
|
||||||
|
|
||||||
let ms = await this.uploadChannel.send({
|
|
||||||
files:uploadGroups[i]
|
|
||||||
}).catch((e) => {console.error(e)})
|
|
||||||
|
|
||||||
if (ms) {
|
|
||||||
msgIds.push(ms.id)
|
|
||||||
} else {
|
|
||||||
reject({status:500,message:"please try again"}); return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// save
|
|
||||||
|
|
||||||
resolve(await this.writeFile(
|
|
||||||
uploadId,
|
|
||||||
{
|
|
||||||
filename:settings.name,
|
|
||||||
messageids:msgIds,
|
|
||||||
mime:settings.mime
|
|
||||||
}
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// fs
|
|
||||||
|
|
||||||
writeFile(uploadId: string, file: FilePointer):Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
this.files[uploadId] = file
|
|
||||||
|
|
||||||
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
reject({status:500,message:"please try again"});
|
|
||||||
delete this.files[uploadId];
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(uploadId)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: move read code here
|
|
||||||
|
|
||||||
readFileStream(uploadId: string):Promise<{dataStream:Readable,contentType:string}> {
|
|
||||||
return new Promise(async (resolve,reject) => {
|
|
||||||
if (!this.uploadChannel) {
|
|
||||||
reject({status:503,message:"server is not ready - please try again later"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.files[uploadId]) {
|
|
||||||
let file = this.files[uploadId]
|
|
||||||
|
|
||||||
let dataStream = new Readable({
|
|
||||||
read(){}
|
|
||||||
})
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
contentType: file.mime,
|
|
||||||
dataStream: dataStream
|
|
||||||
})
|
|
||||||
|
|
||||||
for (let i = 0; i < file.messageids.length; i++) {
|
|
||||||
let msg = await this.uploadChannel.messages.fetch(file.messageids[i]).catch(() => {return null})
|
|
||||||
if (msg?.attachments) {
|
|
||||||
let attach = Array.from(msg.attachments.values())
|
|
||||||
for (let i = 0; i < attach.length; i++) {
|
|
||||||
let d = await axios.get(attach[i].url,{responseType:"arraybuffer"}).catch((e:Error) => {console.error(e)})
|
|
||||||
if (d) {
|
|
||||||
dataStream.push(d.data)
|
|
||||||
} else {
|
|
||||||
reject({status:500,message:"internal server error"})
|
|
||||||
dataStream.destroy(new Error("file read error"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataStream.push(null)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
reject({status:404,message:"not found"})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
unlink(uploadId:string):Promise<void> {
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
let tmp = this.files[uploadId];
|
|
||||||
delete this.files[uploadId];
|
|
||||||
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
|
|
||||||
if (err) {
|
|
||||||
this.files[uploadId] = tmp
|
|
||||||
reject()
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilePointer(uploadId:string):FilePointer {
|
|
||||||
return this.files[uploadId]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
164
src/server/index.ts
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
import cookieParser from "cookie-parser";
|
||||||
|
import { IntentsBitField, Client } from "discord.js"
|
||||||
|
import express from "express"
|
||||||
|
import fs from "fs"
|
||||||
|
import bytes from "bytes";
|
||||||
|
|
||||||
|
import ServeError from "./lib/errors"
|
||||||
|
import Files from "./lib/files"
|
||||||
|
import * as auth from "./lib/auth"
|
||||||
|
import * as Accounts from "./lib/accounts"
|
||||||
|
|
||||||
|
import * as authRoutes from "./routes/authRoutes";
|
||||||
|
import * as fileApiRoutes from "./routes/fileApiRoutes";
|
||||||
|
import * as adminRoutes from "./routes/adminRoutes";
|
||||||
|
import * as primaryApi from "./routes/primaryApi";
|
||||||
|
|
||||||
|
require("dotenv").config()
|
||||||
|
|
||||||
|
let pkg = require(`${process.cwd()}/package.json`)
|
||||||
|
let app = express()
|
||||||
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
|
app.use("/static/assets",express.static("assets"))
|
||||||
|
app.use("/static/style",express.static("out/style"))
|
||||||
|
app.use("/static/js",express.static("out/client"))
|
||||||
|
|
||||||
|
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
|
||||||
|
|
||||||
|
app.use(cookieParser())
|
||||||
|
|
||||||
|
app.get("/server",(req,res) => {
|
||||||
|
res.send(JSON.stringify({
|
||||||
|
...config,
|
||||||
|
version:pkg.version,
|
||||||
|
files:Object.keys(files.files).length
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
app
|
||||||
|
.use("/auth",authRoutes.authRoutes)
|
||||||
|
.use("/admin",adminRoutes.adminRoutes)
|
||||||
|
.use("/files", fileApiRoutes.fileApiRoutes)
|
||||||
|
.use(primaryApi.primaryApi)
|
||||||
|
// funcs
|
||||||
|
|
||||||
|
// init data
|
||||||
|
|
||||||
|
if (!fs.existsSync(__dirname+"/../.data/")) fs.mkdirSync(__dirname+"/../.data/")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// discord
|
||||||
|
|
||||||
|
let client = new Client({intents:[
|
||||||
|
IntentsBitField.Flags.GuildMessages,
|
||||||
|
IntentsBitField.Flags.MessageContent
|
||||||
|
],rest:{timeout:config.requestTimeout}})
|
||||||
|
|
||||||
|
let files = new Files(client,config)
|
||||||
|
|
||||||
|
authRoutes.setFilesObj(files)
|
||||||
|
adminRoutes.setFilesObj(files)
|
||||||
|
fileApiRoutes.setFilesObj(files)
|
||||||
|
primaryApi.setFilesObj(files)
|
||||||
|
|
||||||
|
// routes (could probably make these use routers)
|
||||||
|
|
||||||
|
// index, clone
|
||||||
|
|
||||||
|
app.get("/", function(req,res) {
|
||||||
|
res.sendFile(process.cwd()+"/pages/index.html")
|
||||||
|
})
|
||||||
|
|
||||||
|
// serve download page
|
||||||
|
|
||||||
|
app.get("/download/:fileId",(req,res) => {
|
||||||
|
if (files.getFilePointer(req.params.fileId)) {
|
||||||
|
let file = files.getFilePointer(req.params.fileId)
|
||||||
|
|
||||||
|
if (file.visibility == "private" && Accounts.getFromToken(req.cookies.auth)?.id != file.owner) {
|
||||||
|
ServeError(res,403,"you do not own this file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.readFile(process.cwd()+"/pages/download.html",(err,buf) => {
|
||||||
|
let fileOwner = file.owner ? Accounts.getFromId(file.owner) : undefined;
|
||||||
|
if (err) {res.sendStatus(500);console.log(err);return}
|
||||||
|
res.send(
|
||||||
|
buf.toString()
|
||||||
|
.replace(/\$FileId/g,req.params.fileId)
|
||||||
|
.replace(/\$Version/g,pkg.version)
|
||||||
|
.replace(/\$FileSize/g,file.sizeInBytes ? bytes(file.sizeInBytes) : "[File size unknown]")
|
||||||
|
.replace(/\$FileName/g,
|
||||||
|
file.filename
|
||||||
|
.replace(/\&/g,"&")
|
||||||
|
.replace(/\</g,"<")
|
||||||
|
.replace(/\>/g,">")
|
||||||
|
)
|
||||||
|
.replace(/\<\!\-\-metaTags\-\-\>/g,
|
||||||
|
(
|
||||||
|
file.mime.startsWith("image/")
|
||||||
|
? `<meta name="og:image" content="https://${req.headers.host}/file/${req.params.fileId}" />`
|
||||||
|
: (
|
||||||
|
file.mime.startsWith("video/")
|
||||||
|
? (
|
||||||
|
`<meta property="og:video:url" content="https://${req.headers.host}/cpt/${req.params.fileId}/video.${file.mime.split("/")[1] == "quicktime" ? "mov" : file.mime.split("/")[1]}" />
|
||||||
|
<meta property="og:video:secure_url" content="https://${req.headers.host}/cpt/${req.params.fileId}/video.${file.mime.split("/")[1] == "quicktime" ? "mov" : file.mime.split("/")[1]}" />
|
||||||
|
<meta property="og:type" content="video.other">
|
||||||
|
<!-- honestly probably good enough for now -->
|
||||||
|
<meta property="twitter:image" content="0">`
|
||||||
|
// quick lazy fix as a fallback
|
||||||
|
// maybe i'll improve this later, but probably not.
|
||||||
|
+ ((file.sizeInBytes||0) >= 26214400 ? `
|
||||||
|
<meta property="og:video:width" content="1280">
|
||||||
|
<meta property="og:video:height" content="720">` : "")
|
||||||
|
)
|
||||||
|
: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
fileOwner?.embed?.largeImage && file.visibility!="anonymous"
|
||||||
|
? `<meta name="twitter:card" content="summary_large_image">`
|
||||||
|
: ""
|
||||||
|
)
|
||||||
|
+ `\n<meta name="theme-color" content="${fileOwner?.embed?.color && file.visibility!="anonymous" && (req.headers["user-agent"]||"").includes("Discordbot") ? `#${fileOwner.embed.color}` : "rgb(30, 33, 36)"}">`
|
||||||
|
)
|
||||||
|
.replace(/\<\!\-\-preview\-\-\>/g,
|
||||||
|
file.mime.startsWith("image/")
|
||||||
|
? `<div style="min-height:10px"></div><img src="/file/${req.params.fileId}" />`
|
||||||
|
: (
|
||||||
|
file.mime.startsWith("video/")
|
||||||
|
? `<div style="min-height:10px"></div><video src="/file/${req.params.fileId}" controls></video>`
|
||||||
|
: (
|
||||||
|
file.mime.startsWith("audio/")
|
||||||
|
? `<div style="min-height:10px"></div><audio src="/file/${req.params.fileId}" controls></audio>`
|
||||||
|
: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.replace(/\$Uploader/g,!file.owner||file.visibility=="anonymous" ? "Anonymous" : `@${fileOwner?.username || "Deleted User"}`)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ServeError(res,404,"file not found")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
routes should be in this order:
|
||||||
|
|
||||||
|
index
|
||||||
|
api
|
||||||
|
dl pages
|
||||||
|
file serving
|
||||||
|
*/
|
||||||
|
|
||||||
|
// listen on 3000 or MONOFILE_PORT
|
||||||
|
|
||||||
|
app.listen(process.env.MONOFILE_PORT || 3000,function() {
|
||||||
|
console.log("Web OK!")
|
||||||
|
})
|
||||||
|
|
||||||
|
client.login(process.env.TOKEN)
|
132
src/server/lib/accounts.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import crypto from "crypto"
|
||||||
|
import * as auth from "./auth";
|
||||||
|
import { readFile, writeFile } from "fs/promises"
|
||||||
|
import { FileVisibility } from "./files";
|
||||||
|
|
||||||
|
// this is probably horrible
|
||||||
|
// but i don't even care anymore
|
||||||
|
|
||||||
|
export let Accounts: Account[] = []
|
||||||
|
|
||||||
|
export interface Account {
|
||||||
|
id : string
|
||||||
|
username : string
|
||||||
|
email? : string
|
||||||
|
password : {
|
||||||
|
hash : string
|
||||||
|
salt : string
|
||||||
|
}
|
||||||
|
files : string[]
|
||||||
|
admin : boolean
|
||||||
|
defaultFileVisibility : FileVisibility
|
||||||
|
customCSS? : string
|
||||||
|
|
||||||
|
embed? : {
|
||||||
|
color? : string
|
||||||
|
largeImage? : boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create(username:string,pwd:string,admin:boolean=false):Promise<string> {
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
let accId = crypto.randomBytes(12).toString("hex")
|
||||||
|
|
||||||
|
Accounts.push(
|
||||||
|
{
|
||||||
|
id: accId,
|
||||||
|
username: username,
|
||||||
|
password: password.hash(pwd),
|
||||||
|
files: [],
|
||||||
|
admin: admin,
|
||||||
|
defaultFileVisibility: "public"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
save().then(() => resolve(accId))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFromUsername(username:string) {
|
||||||
|
return Accounts.find(e => e.username == username)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFromId(id:string) {
|
||||||
|
return Accounts.find(e => e.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFromToken(token:string) {
|
||||||
|
let accId = auth.validate(token)
|
||||||
|
if (!accId) return
|
||||||
|
return getFromId(accId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAccount(id:string) {
|
||||||
|
Accounts.splice(Accounts.findIndex(e => e.id == id),1)
|
||||||
|
return save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace password {
|
||||||
|
export function hash(password:string,_salt?:string) {
|
||||||
|
let salt = _salt || crypto.randomBytes(12).toString('base64')
|
||||||
|
let hash = crypto.createHash('sha256').update(`${salt}${password}`).digest('hex')
|
||||||
|
|
||||||
|
return {
|
||||||
|
salt:salt,
|
||||||
|
hash:hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set(id:string,password:string) {
|
||||||
|
let acc = Accounts.find(e => e.id == id)
|
||||||
|
if (!acc) return
|
||||||
|
|
||||||
|
acc.password = hash(password)
|
||||||
|
return save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function check(id:string,password:string) {
|
||||||
|
let acc = Accounts.find(e => e.id == id)
|
||||||
|
if (!acc) return
|
||||||
|
|
||||||
|
return acc.password.hash == hash(password,acc.password.salt).hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace files {
|
||||||
|
export function index(accountId:string,fileId:string) {
|
||||||
|
// maybe replace with a obj like
|
||||||
|
// { x:true }
|
||||||
|
// for faster lookups? not sure if it would be faster
|
||||||
|
let acc = Accounts.find(e => e.id == accountId)
|
||||||
|
if (!acc) return
|
||||||
|
if (acc.files.find(e => e == fileId)) return
|
||||||
|
|
||||||
|
acc.files.push(fileId)
|
||||||
|
return save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deindex(accountId:string,fileId:string, noWrite:boolean=false) {
|
||||||
|
let acc = Accounts.find(e => e.id == accountId)
|
||||||
|
if (!acc) return
|
||||||
|
let fi = acc.files.findIndex(e => e == fileId)
|
||||||
|
if (fi) {
|
||||||
|
acc.files.splice(fi,1)
|
||||||
|
if (!noWrite) return save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save() {
|
||||||
|
return writeFile(`${process.cwd()}/.data/accounts.json`,JSON.stringify(Accounts))
|
||||||
|
.catch((err) => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile(`${process.cwd()}/.data/accounts.json`)
|
||||||
|
.then((buf) => {
|
||||||
|
Accounts = JSON.parse(buf.toString())
|
||||||
|
}).catch(err => console.error(err))
|
||||||
|
.finally(() => {
|
||||||
|
if (!Accounts.find(e => e.admin)) {
|
||||||
|
create("admin","admin",true)
|
||||||
|
}
|
||||||
|
})
|
58
src/server/lib/auth.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import crypto from "crypto"
|
||||||
|
import { readFile, writeFile } from "fs/promises"
|
||||||
|
export let AuthTokens: AuthToken[] = []
|
||||||
|
export let AuthTokenTO:{[key:string]:NodeJS.Timeout} = {}
|
||||||
|
|
||||||
|
export interface AuthToken {
|
||||||
|
account: string,
|
||||||
|
token: string,
|
||||||
|
expire: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create(id:string,expire:number=(24*60*60*1000)) {
|
||||||
|
let token = {
|
||||||
|
account:id,
|
||||||
|
token:crypto.randomBytes(12).toString('hex'),
|
||||||
|
expire:Date.now()+expire
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthTokens.push(token)
|
||||||
|
tokenTimer(token)
|
||||||
|
|
||||||
|
save()
|
||||||
|
|
||||||
|
return token.token
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validate(token:string) {
|
||||||
|
return AuthTokens.find(e => e.token == token && Date.now() < e.expire)?.account
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tokenTimer(token:AuthToken) {
|
||||||
|
if (Date.now() >= token.expire) {
|
||||||
|
invalidate(token.token)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthTokenTO[token.token] = setTimeout(() => invalidate(token.token),token.expire-Date.now())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidate(token:string) {
|
||||||
|
if (AuthTokenTO[token]) {
|
||||||
|
clearTimeout(AuthTokenTO[token])
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthTokens.splice(AuthTokens.findIndex(e => e.token == token),1)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save() {
|
||||||
|
writeFile(`${process.cwd()}/.data/tokens.json`,JSON.stringify(AuthTokens))
|
||||||
|
.catch((err) => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile(`${process.cwd()}/.data/tokens.json`)
|
||||||
|
.then((buf) => {
|
||||||
|
AuthTokens = JSON.parse(buf.toString())
|
||||||
|
AuthTokens.forEach(e => tokenTimer(e))
|
||||||
|
}).catch(err => console.error(err))
|
37
src/server/lib/errors.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Response } from "express";
|
||||||
|
import { readFile } from "fs/promises"
|
||||||
|
|
||||||
|
let errorPage:string
|
||||||
|
|
||||||
|
export default async function ServeError(
|
||||||
|
res:Response,
|
||||||
|
code:number,
|
||||||
|
reason:string
|
||||||
|
) {
|
||||||
|
// fetch error page if not cached
|
||||||
|
if (!errorPage) {
|
||||||
|
errorPage =
|
||||||
|
(
|
||||||
|
await readFile(`${process.cwd()}/pages/error.html`)
|
||||||
|
.catch((err) => console.error(err))
|
||||||
|
|| "<pre>$code $text</pre>"
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve error
|
||||||
|
res.statusMessage = reason
|
||||||
|
res.status(code)
|
||||||
|
res.header("x-backup-status-message", reason) // glitch default nginx configuration
|
||||||
|
res.send(
|
||||||
|
errorPage
|
||||||
|
.replace(/\$code/g,code.toString())
|
||||||
|
.replace(/\$text/g,reason)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Redirect(res:Response,url:string) {
|
||||||
|
res.status(302)
|
||||||
|
res.header("Location",url)
|
||||||
|
res.send()
|
||||||
|
}
|
437
src/server/lib/files.ts
Normal file
|
@ -0,0 +1,437 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import Discord, { Client, TextBasedChannel } from "discord.js";
|
||||||
|
import { readFile, writeFile } from "fs";
|
||||||
|
import { Readable } from "node:stream";
|
||||||
|
import { files } from "./accounts";
|
||||||
|
|
||||||
|
import * as Accounts from "./accounts";
|
||||||
|
|
||||||
|
export let id_check_regex = /[A-Za-z0-9_\-\.\!]+/
|
||||||
|
export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
|
||||||
|
|
||||||
|
// bad solution but whatever
|
||||||
|
|
||||||
|
export type FileVisibility = "public" | "anonymous" | "private"
|
||||||
|
|
||||||
|
export function generateFileId(length:number=5) {
|
||||||
|
let fid = ""
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
fid += alphanum[Math.floor(Math.random()*alphanum.length)]
|
||||||
|
}
|
||||||
|
return fid
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadSettings {
|
||||||
|
name?: string,
|
||||||
|
mime: string,
|
||||||
|
uploadId?: string,
|
||||||
|
owner?:string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Configuration {
|
||||||
|
maxDiscordFiles: number,
|
||||||
|
maxDiscordFileSize: number,
|
||||||
|
targetGuild: string,
|
||||||
|
targetChannel: string,
|
||||||
|
requestTimeout: number,
|
||||||
|
maxUploadIdLength: number,
|
||||||
|
|
||||||
|
accounts: {
|
||||||
|
registrationEnabled: boolean,
|
||||||
|
requiredForUpload: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilePointer {
|
||||||
|
filename:string,
|
||||||
|
mime:string,
|
||||||
|
messageids:string[],
|
||||||
|
owner?:string,
|
||||||
|
sizeInBytes?:number,
|
||||||
|
tag?:string,
|
||||||
|
visibility?:FileVisibility,
|
||||||
|
reserved?: boolean,
|
||||||
|
chunkSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusCodeError {
|
||||||
|
status: number,
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* */
|
||||||
|
|
||||||
|
export default class Files {
|
||||||
|
|
||||||
|
config: Configuration
|
||||||
|
client: Client
|
||||||
|
files: {[key:string]:FilePointer} = {}
|
||||||
|
uploadChannel?: TextBasedChannel
|
||||||
|
|
||||||
|
constructor(client: Client, config: Configuration) {
|
||||||
|
|
||||||
|
this.config = config;
|
||||||
|
this.client = client;
|
||||||
|
|
||||||
|
client.on("ready",() => {
|
||||||
|
console.log("Discord OK!")
|
||||||
|
|
||||||
|
client.guilds.fetch(config.targetGuild).then((g) => {
|
||||||
|
g.channels.fetch(config.targetChannel).then((a) => {
|
||||||
|
if (a?.isTextBased()) {
|
||||||
|
this.uploadChannel = a
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
readFile(process.cwd()+"/.data/files.json",(err,buf) => {
|
||||||
|
if (err) {console.log(err);return}
|
||||||
|
this.files = JSON.parse(buf.toString() || "{}")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise<string|StatusCodeError> {
|
||||||
|
return new Promise<string>(async (resolve,reject) => {
|
||||||
|
if (!this.uploadChannel) {
|
||||||
|
reject({status:503,message:"server is not ready - please try again later"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.name || !settings.mime) {
|
||||||
|
reject({status:400,message:"missing name/mime"});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings.owner && this.config.accounts.requiredForUpload) {
|
||||||
|
reject({status:401,message:"an account is required for upload"});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploadId = (settings.uploadId || generateFileId()).toString();
|
||||||
|
|
||||||
|
if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > this.config.maxUploadIdLength) {
|
||||||
|
reject({status:400,message:"invalid id"});return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.files[uploadId] && (settings.owner ? this.files[uploadId].owner != settings.owner : true)) {
|
||||||
|
reject({status:400,message:"you are not the owner of this file id"});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.files[uploadId] && this.files[uploadId].reserved) {
|
||||||
|
reject({status:400,message:"already uploading this file. if your file is stuck in this state, contact an administrator"});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.name.length > 128) {
|
||||||
|
reject({status:400,message:"name too long"});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.mime.length > 128) {
|
||||||
|
reject({status:400,message:"mime too long"});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserve file, hopefully should prevent
|
||||||
|
// large files breaking
|
||||||
|
|
||||||
|
let ogf = this.files[uploadId]
|
||||||
|
|
||||||
|
this.files[uploadId] = {
|
||||||
|
filename:settings.name,
|
||||||
|
messageids:[],
|
||||||
|
mime:settings.mime,
|
||||||
|
sizeInBytes:0,
|
||||||
|
|
||||||
|
owner:settings.owner,
|
||||||
|
visibility: settings.owner ? "private" : "public",
|
||||||
|
reserved: true,
|
||||||
|
|
||||||
|
chunkSize: this.config.maxDiscordFileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// save
|
||||||
|
|
||||||
|
if (settings.owner) {
|
||||||
|
await files.index(settings.owner,uploadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get buffer
|
||||||
|
if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) {
|
||||||
|
reject({status:400,message:"file too large"});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate buffers to upload
|
||||||
|
let toUpload = []
|
||||||
|
for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) {
|
||||||
|
toUpload.push(
|
||||||
|
fBuffer.subarray(
|
||||||
|
i*this.config.maxDiscordFileSize,
|
||||||
|
Math.min(
|
||||||
|
fBuffer.byteLength,
|
||||||
|
(i+1)*this.config.maxDiscordFileSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin uploading
|
||||||
|
let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => {
|
||||||
|
return new Discord.AttachmentBuilder(e)
|
||||||
|
.setName(Math.random().toString().slice(2))
|
||||||
|
})
|
||||||
|
let uploadGroups = []
|
||||||
|
for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) {
|
||||||
|
uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10)))
|
||||||
|
}
|
||||||
|
|
||||||
|
let msgIds = []
|
||||||
|
|
||||||
|
for (let i = 0; i < uploadGroups.length; i++) {
|
||||||
|
|
||||||
|
let ms = await this.uploadChannel.send({
|
||||||
|
files:uploadGroups[i]
|
||||||
|
}).catch((e) => {console.error(e)})
|
||||||
|
|
||||||
|
if (ms) {
|
||||||
|
msgIds.push(ms.id)
|
||||||
|
} else {
|
||||||
|
if (!ogf) delete this.files[uploadId]
|
||||||
|
else this.files[uploadId] = ogf
|
||||||
|
reject({status:500,message:"please try again"}); return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this code deletes the files from discord, btw
|
||||||
|
// if need be, replace with job queue system
|
||||||
|
|
||||||
|
if (ogf&&this.uploadChannel) {
|
||||||
|
for (let x of ogf.messageids) {
|
||||||
|
this.uploadChannel.messages.delete(x).catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(await this.writeFile(
|
||||||
|
uploadId,
|
||||||
|
{
|
||||||
|
filename:settings.name,
|
||||||
|
messageids:msgIds,
|
||||||
|
mime:settings.mime,
|
||||||
|
sizeInBytes:fBuffer.byteLength,
|
||||||
|
|
||||||
|
owner:settings.owner,
|
||||||
|
visibility: ogf ? ogf.visibility
|
||||||
|
: (
|
||||||
|
settings.owner
|
||||||
|
? Accounts.getFromId(settings.owner)?.defaultFileVisibility
|
||||||
|
: undefined
|
||||||
|
),
|
||||||
|
// so that json.stringify doesnt include tag:undefined
|
||||||
|
...((ogf||{}).tag ? {tag:ogf.tag} : {}),
|
||||||
|
|
||||||
|
chunkSize: this.config.maxDiscordFileSize
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// fs
|
||||||
|
|
||||||
|
writeFile(uploadId: string, file: FilePointer):Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
this.files[uploadId] = file
|
||||||
|
|
||||||
|
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
reject({status:500,message:"server may be misconfigured, contact admin for help"});
|
||||||
|
delete this.files[uploadId];
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(uploadId)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: move read code here
|
||||||
|
|
||||||
|
readFileStream(uploadId: string, range?: {start:number, end:number}):Promise<Readable> {
|
||||||
|
return new Promise(async (resolve,reject) => {
|
||||||
|
if (!this.uploadChannel) {
|
||||||
|
reject({status:503,message:"server is not ready - please try again later"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.files[uploadId]) {
|
||||||
|
let file = this.files[uploadId]
|
||||||
|
|
||||||
|
let
|
||||||
|
scan_msg_begin = 0,
|
||||||
|
scan_msg_end = file.messageids.length-1,
|
||||||
|
scan_files_begin = 0,
|
||||||
|
scan_files_end = -1
|
||||||
|
|
||||||
|
let useRanges = range && file.chunkSize && file.sizeInBytes;
|
||||||
|
|
||||||
|
// todo: figure out how to get typesccript to accept useRanges
|
||||||
|
// i'm too tired to look it up or write whatever it wnats me to do
|
||||||
|
if (range && file.chunkSize && file.sizeInBytes) {
|
||||||
|
|
||||||
|
// Calculate where to start file scans...
|
||||||
|
|
||||||
|
scan_files_begin = Math.floor(range.start / file.chunkSize)
|
||||||
|
scan_files_end = Math.ceil(range.end / file.chunkSize) - 1
|
||||||
|
|
||||||
|
scan_msg_begin = Math.floor(scan_files_begin / 10)
|
||||||
|
scan_msg_end = Math.ceil(scan_files_end / 10)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachments: Discord.Attachment[] = [];
|
||||||
|
|
||||||
|
/* File updates */
|
||||||
|
let file_updates: Pick<FilePointer, "chunkSize" | "sizeInBytes"> = {}
|
||||||
|
let atSIB: number[] = [] // kepes track of the size of each file...
|
||||||
|
|
||||||
|
for (let xi = scan_msg_begin; xi < scan_msg_end+1; xi++) {
|
||||||
|
|
||||||
|
let msg = await this.uploadChannel.messages.fetch(file.messageids[xi]).catch(() => {return null})
|
||||||
|
if (msg?.attachments) {
|
||||||
|
|
||||||
|
let attach = Array.from(msg.attachments.values())
|
||||||
|
for (let i = (useRanges && xi == scan_msg_begin ? ( scan_files_begin - (xi*10) ) : 0); i < (useRanges && xi == scan_msg_end ? ( scan_files_end - (xi*10) + 1 ) : attach.length); i++) {
|
||||||
|
|
||||||
|
attachments.push(attach[i])
|
||||||
|
atSIB.push(attach[i].size)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.sizeInBytes) file_updates.sizeInBytes = atSIB.reduce((a,b) => a+b);
|
||||||
|
if (!file.chunkSize) file_updates.chunkSize = atSIB[0]
|
||||||
|
if (Object.keys(file_updates).length) { // if file_updates not empty
|
||||||
|
// i gotta do these weird workarounds, ts is weird sometimes
|
||||||
|
// originally i was gonna do key is keyof FilePointer but for some reason
|
||||||
|
// it ended up making typeof file[key] never??? so
|
||||||
|
// its 10pm and chinese people suck at being quiet so i just wanna get this over with
|
||||||
|
// chinese is the worst language in terms of volume lmao
|
||||||
|
let valid_fp_keys = ["sizeInBytes", "chunkSize"]
|
||||||
|
let isValidFilePointerKey = (key: string): key is "sizeInBytes" | "chunkSize" => valid_fp_keys.includes(key)
|
||||||
|
|
||||||
|
for (let [key,value] of Object.entries(file_updates)) {
|
||||||
|
if (isValidFilePointerKey(key)) file[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
let getNextChunk = async () => {
|
||||||
|
let scanning_chunk = attachments[position]
|
||||||
|
if (!scanning_chunk) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let d = await axios.get(
|
||||||
|
scanning_chunk.url,
|
||||||
|
{
|
||||||
|
responseType:"arraybuffer",
|
||||||
|
headers: {
|
||||||
|
...(useRanges ? {
|
||||||
|
"Range": `bytes=${position == 0 && range && file.chunkSize ? range.start-(scan_files_begin*file.chunkSize) : "0"}-${position == attachments.length-1 && range && file.chunkSize ? range.end-(scan_files_end*file.chunkSize) : ""}`
|
||||||
|
} : {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).catch((e:Error) => {console.error(e)})
|
||||||
|
|
||||||
|
position++;
|
||||||
|
|
||||||
|
if (d) {
|
||||||
|
return d.data
|
||||||
|
} else {
|
||||||
|
reject({status:500,message:"internal server error"})
|
||||||
|
return "__ERR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ord:number[] = []
|
||||||
|
// hopefully this regulates it?
|
||||||
|
let lastChunkSent = true
|
||||||
|
|
||||||
|
let dataStream = new Readable({
|
||||||
|
read(){
|
||||||
|
if (!lastChunkSent) return
|
||||||
|
lastChunkSent = false
|
||||||
|
getNextChunk().then(async (nextChunk) => {
|
||||||
|
if (nextChunk == "__ERR") {this.destroy(new Error("file read error")); return}
|
||||||
|
let response = this.push(nextChunk)
|
||||||
|
|
||||||
|
if (!nextChunk) return // EOF
|
||||||
|
|
||||||
|
while (response) {
|
||||||
|
let nextChunk = await getNextChunk()
|
||||||
|
response = this.push(nextChunk)
|
||||||
|
if (!nextChunk) return
|
||||||
|
}
|
||||||
|
lastChunkSent = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resolve(dataStream)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
reject({status:404,message:"not found"})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink(uploadId:string, noWrite: boolean = false):Promise<void> {
|
||||||
|
return new Promise(async (resolve,reject) => {
|
||||||
|
let tmp = this.files[uploadId];
|
||||||
|
if (!tmp) {resolve(); return}
|
||||||
|
if (tmp.owner) {
|
||||||
|
let id = files.deindex(tmp.owner,uploadId,noWrite);
|
||||||
|
if (id) await id
|
||||||
|
}
|
||||||
|
// this code deletes the files from discord, btw
|
||||||
|
// if need be, replace with job queue system
|
||||||
|
|
||||||
|
if (!this.uploadChannel) {reject(); return}
|
||||||
|
for (let x of tmp.messageids) {
|
||||||
|
this.uploadChannel.messages.delete(x).catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.files[uploadId];
|
||||||
|
if (noWrite) {resolve(); return}
|
||||||
|
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {
|
||||||
|
if (err) {
|
||||||
|
this.files[uploadId] = tmp // !! this may not work, since tmp is a link to this.files[uploadId]?
|
||||||
|
reject()
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilePointer(uploadId:string):FilePointer {
|
||||||
|
return this.files[uploadId]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
src/server/lib/mail.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { createTransport } from "nodemailer";
|
||||||
|
|
||||||
|
// required i guess
|
||||||
|
require("dotenv").config()
|
||||||
|
|
||||||
|
let
|
||||||
|
mailConfig =
|
||||||
|
require( process.cwd() + "/config.json" ).mail,
|
||||||
|
transport =
|
||||||
|
createTransport(
|
||||||
|
{
|
||||||
|
...mailConfig.transport,
|
||||||
|
auth: {
|
||||||
|
user: process.env.MAIL_USER,
|
||||||
|
pass: process.env.MAIL_PASS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// lazy but
|
||||||
|
|
||||||
|
export function sendMail(to: string, subject: string, content: string) {
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
transport.sendMail({
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
"from": mailConfig.send.from,
|
||||||
|
"html": `<span style="font-size:x-large;font-weight:600;">monofile <span style="opacity:0.5">accounts</span></span><br><span style="opacity:0.5">Gain control of your uploads.</span><hr><br>${
|
||||||
|
content
|
||||||
|
.replace(/\<span username\>/g, `<span code><span style="color:#DDAA66;padding-right:3px;">@</span>`)
|
||||||
|
.replace(/\<span code\>/g,`<span style="font-family:monospace;padding:3px 5px 3px 5px;border-radius:8px;background-color:#1C1C1C;color:#DDDDDD;">`)
|
||||||
|
}<br><br><span style="opacity:0.5">If you do not believe that you are the intended recipient of this email, please disregard this message.</span>`
|
||||||
|
}, (err, info) => {
|
||||||
|
if (err) reject(err)
|
||||||
|
else resolve(info)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
24
src/server/lib/middleware.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import * as Accounts from "./accounts";
|
||||||
|
import express, { type RequestHandler } from "express"
|
||||||
|
import ServeError from "../lib/errors";
|
||||||
|
|
||||||
|
export let getAccount: RequestHandler = function(req, res, next) {
|
||||||
|
res.locals.acc = Accounts.getFromToken(req.cookies.auth)
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export let requiresAccount: RequestHandler = function(_req, res, next) {
|
||||||
|
if (!res.locals.acc) {
|
||||||
|
ServeError(res, 401, "not logged in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export let requiresAdmin: RequestHandler = function(_req, res, next) {
|
||||||
|
if (!res.locals.acc.admin) {
|
||||||
|
ServeError(res, 403, "you are not an administrator")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
45
src/server/lib/ratelimit.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { RequestHandler } from "express"
|
||||||
|
import { type Account } from "./accounts"
|
||||||
|
import ServeError from "./errors"
|
||||||
|
|
||||||
|
interface ratelimitSettings {
|
||||||
|
|
||||||
|
requests: number
|
||||||
|
per: number
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accountRatelimit( settings: ratelimitSettings ): RequestHandler {
|
||||||
|
let activeLimits: {
|
||||||
|
[ key: string ]: {
|
||||||
|
requests: number,
|
||||||
|
expirationHold: NodeJS.Timeout
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
return (req, res, next) => {
|
||||||
|
if (res.locals.acc) {
|
||||||
|
let accId = res.locals.acc.id
|
||||||
|
let aL = activeLimits[accId]
|
||||||
|
|
||||||
|
if (!aL) {
|
||||||
|
activeLimits[accId] = {
|
||||||
|
requests: 0,
|
||||||
|
expirationHold: setTimeout(() => delete activeLimits[accId], settings.per)
|
||||||
|
}
|
||||||
|
aL = activeLimits[accId]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aL.requests < settings.requests) {
|
||||||
|
res.locals.undoCount = () => {
|
||||||
|
if (activeLimits[accId]) {
|
||||||
|
activeLimits[accId].requests--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
ServeError(res, 429, "too many requests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
195
src/server/routes/adminRoutes.ts
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
import { Router } from "express";
|
||||||
|
import * as Accounts from "../lib/accounts";
|
||||||
|
import * as auth from "../lib/auth";
|
||||||
|
import bytes from "bytes"
|
||||||
|
import {writeFile} from "fs";
|
||||||
|
import { sendMail } from "../lib/mail";
|
||||||
|
import { getAccount, requiresAccount, requiresAdmin } from "../lib/middleware"
|
||||||
|
|
||||||
|
import ServeError from "../lib/errors";
|
||||||
|
import Files from "../lib/files";
|
||||||
|
|
||||||
|
let parser = bodyParser.json({
|
||||||
|
type: ["text/plain","application/json"]
|
||||||
|
})
|
||||||
|
|
||||||
|
export let adminRoutes = Router();
|
||||||
|
adminRoutes
|
||||||
|
.use(getAccount)
|
||||||
|
.use(requiresAccount)
|
||||||
|
.use(requiresAdmin)
|
||||||
|
let files:Files
|
||||||
|
|
||||||
|
export function setFilesObj(newFiles:Files) {
|
||||||
|
files = newFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
|
adminRoutes.post("/reset", parser, (req,res) => {
|
||||||
|
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.target !== "string" || typeof req.body.password !== "string") {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetAccount = Accounts.getFromUsername(req.body.target)
|
||||||
|
if (!targetAccount) {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Accounts.password.set ( targetAccount.id, req.body.password )
|
||||||
|
auth.AuthTokens.filter(e => e.account == targetAccount?.id).forEach((v) => {
|
||||||
|
auth.invalidate(v.token)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (targetAccount.email) {
|
||||||
|
sendMail(targetAccount.email, `Your login details have been updated`, `<b>Hello there!</b> This email is to notify you of a password change that an administrator, <span username>${acc.username}</span>, has initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
|
||||||
|
res.send("OK")
|
||||||
|
}).catch((err) => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
res.send()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
adminRoutes.post("/elevate", parser, (req,res) => {
|
||||||
|
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.target !== "string") {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetAccount = Accounts.getFromUsername(req.body.target)
|
||||||
|
if (!targetAccount) {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targetAccount.admin = true;
|
||||||
|
Accounts.save()
|
||||||
|
res.send()
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
adminRoutes.post("/delete", parser, (req,res) => {
|
||||||
|
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.target !== "string") {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetFile = files.getFilePointer(req.body.target)
|
||||||
|
|
||||||
|
if (!targetFile) {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.unlink(req.body.target).then(() => {
|
||||||
|
res.status(200)
|
||||||
|
}).catch(() => {
|
||||||
|
res.status(500)
|
||||||
|
}).finally(() => res.send())
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
adminRoutes.post("/delete_account", parser, async (req,res) => {
|
||||||
|
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.target !== "string") {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetAccount = Accounts.getFromUsername(req.body.target)
|
||||||
|
if (!targetAccount) {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let accId = targetAccount.id
|
||||||
|
|
||||||
|
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
||||||
|
auth.invalidate(v.token)
|
||||||
|
})
|
||||||
|
|
||||||
|
let cpl = () => Accounts.deleteAccount(accId).then(_ => {
|
||||||
|
if (targetAccount?.email) {
|
||||||
|
sendMail(targetAccount.email, "Notice of account deletion", `Your account, <span username>${targetAccount.username}</span>, has been deleted by <span username>${acc.username}</span> for the following reason: <br><br><span style="font-weight:600">${req.body.reason || "(no reason specified)"}</span><br><br> Your files ${req.body.deleteFiles ? "have been deleted" : "have not been modified"}. Thank you for using monofile.`)
|
||||||
|
}
|
||||||
|
res.send("account deleted")
|
||||||
|
})
|
||||||
|
|
||||||
|
if (req.body.deleteFiles) {
|
||||||
|
let f = targetAccount.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
|
||||||
|
for (let v of f) {
|
||||||
|
files.unlink(v,true).catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
||||||
|
if (err) console.log(err)
|
||||||
|
cpl()
|
||||||
|
})
|
||||||
|
} else cpl()
|
||||||
|
})
|
||||||
|
|
||||||
|
adminRoutes.post("/transfer", parser, (req,res) => {
|
||||||
|
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.target !== "string" || typeof req.body.owner !== "string") {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetFile = files.getFilePointer(req.body.target)
|
||||||
|
if (!targetFile) {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let newOwner = Accounts.getFromUsername(req.body.owner || "")
|
||||||
|
|
||||||
|
// clear old owner
|
||||||
|
|
||||||
|
if (targetFile.owner) {
|
||||||
|
let oldOwner = Accounts.getFromId(targetFile.owner)
|
||||||
|
if (oldOwner) {
|
||||||
|
Accounts.files.deindex(oldOwner.id, req.body.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newOwner) {
|
||||||
|
Accounts.files.index(newOwner.id, req.body.target)
|
||||||
|
}
|
||||||
|
targetFile.owner = newOwner ? newOwner.id : undefined;
|
||||||
|
|
||||||
|
files.writeFile(req.body.target, targetFile).then(() => {
|
||||||
|
res.send()
|
||||||
|
}).catch(() => {
|
||||||
|
res.status(500)
|
||||||
|
res.send()
|
||||||
|
}) // wasting a reassignment but whatee
|
||||||
|
|
||||||
|
})
|
461
src/server/routes/authRoutes.ts
Normal file
|
@ -0,0 +1,461 @@
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
import { Router } from "express";
|
||||||
|
import * as Accounts from "../lib/accounts";
|
||||||
|
import * as auth from "../lib/auth";
|
||||||
|
import { sendMail } from "../lib/mail";
|
||||||
|
import { getAccount, requiresAccount } from "../lib/middleware"
|
||||||
|
import { accountRatelimit } from "../lib/ratelimit"
|
||||||
|
|
||||||
|
import ServeError from "../lib/errors";
|
||||||
|
import Files, { FileVisibility, generateFileId, id_check_regex } from "../lib/files";
|
||||||
|
|
||||||
|
import { writeFile } from "fs";
|
||||||
|
|
||||||
|
let parser = bodyParser.json({
|
||||||
|
type: ["text/plain","application/json"]
|
||||||
|
})
|
||||||
|
|
||||||
|
export let authRoutes = Router();
|
||||||
|
authRoutes.use(getAccount)
|
||||||
|
|
||||||
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
|
let files:Files
|
||||||
|
|
||||||
|
export function setFilesObj(newFiles:Files) {
|
||||||
|
files = newFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
authRoutes.post("/login", parser, (req,res) => {
|
||||||
|
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
|
||||||
|
ServeError(res,400,"please provide a username or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.validate(req.cookies.auth)) return
|
||||||
|
|
||||||
|
/*
|
||||||
|
check if account exists
|
||||||
|
*/
|
||||||
|
|
||||||
|
let acc = Accounts.getFromUsername(req.body.username)
|
||||||
|
|
||||||
|
if (!acc) {
|
||||||
|
ServeError(res,401,"username or password incorrect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Accounts.password.check(acc.id,req.body.password)) {
|
||||||
|
ServeError(res,401,"username or password incorrect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
assign token
|
||||||
|
*/
|
||||||
|
|
||||||
|
res.cookie("auth",auth.create(acc.id,(3*24*60*60*1000)))
|
||||||
|
res.status(200)
|
||||||
|
res.end()
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/create", parser, (req,res) => {
|
||||||
|
if (!config.accounts.registrationEnabled) {
|
||||||
|
ServeError(res,403,"account registration disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.validate(req.cookies.auth)) return
|
||||||
|
|
||||||
|
if (typeof req.body.username != "string" || typeof req.body.password != "string") {
|
||||||
|
ServeError(res,400,"please provide a username or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
check if account exists
|
||||||
|
*/
|
||||||
|
|
||||||
|
let acc = Accounts.getFromUsername(req.body.username)
|
||||||
|
|
||||||
|
if (acc) {
|
||||||
|
ServeError(res,400,"account with this username already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body.username.length < 3 || req.body.username.length > 20) {
|
||||||
|
ServeError(res,400,"username must be over or equal to 3 characters or under or equal to 20 characters in length")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
|
||||||
|
ServeError(res,400,"username contains invalid characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body.password.length < 8) {
|
||||||
|
ServeError(res,400,"password must be 8 characters or longer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Accounts.create(req.body.username,req.body.password)
|
||||||
|
.then((newAcc) => {
|
||||||
|
/*
|
||||||
|
assign token
|
||||||
|
*/
|
||||||
|
|
||||||
|
res.cookie("auth",auth.create(newAcc,(3*24*60*60*1000)))
|
||||||
|
res.status(200)
|
||||||
|
res.end()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
ServeError(res,500,"internal server error")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/logout", (req,res) => {
|
||||||
|
if (!auth.validate(req.cookies.auth)) {
|
||||||
|
ServeError(res, 401, "not logged in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.invalidate(req.cookies.auth)
|
||||||
|
res.send("logged out")
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/dfv", requiresAccount, parser, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (['public','private','anonymous'].includes(req.body.defaultFileVisibility)) {
|
||||||
|
acc.defaultFileVisibility = req.body.defaultFileVisibility
|
||||||
|
Accounts.save()
|
||||||
|
res.send(`dfv has been set to ${acc.defaultFileVisibility}`)
|
||||||
|
} else {
|
||||||
|
res.status(400)
|
||||||
|
res.send("invalid dfv")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/customcss", requiresAccount, parser, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.fileId != "string") req.body.fileId = undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
|
||||||
|
!req.body.fileId
|
||||||
|
|| (req.body.fileId.match(id_check_regex) == req.body.fileId
|
||||||
|
&& req.body.fileId.length <= config.maxUploadIdLength)
|
||||||
|
|
||||||
|
) {
|
||||||
|
acc.customCSS = req.body.fileId || undefined
|
||||||
|
if (!req.body.fileId) delete acc.customCSS
|
||||||
|
Accounts.save()
|
||||||
|
res.send(`custom css saved`)
|
||||||
|
} else {
|
||||||
|
res.status(400)
|
||||||
|
res.send("invalid fileid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/embedcolor", requiresAccount, parser, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.color != "string") req.body.color = undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
|
||||||
|
!req.body.color
|
||||||
|
|| (req.body.color.toLowerCase().match(/[a-f0-9]+/) == req.body.color)
|
||||||
|
&& req.body.color.length == 6
|
||||||
|
|
||||||
|
) {
|
||||||
|
if (!acc.embed) acc.embed = {}
|
||||||
|
acc.embed.color = req.body.color || undefined
|
||||||
|
if (!req.body.color) delete acc.embed.color
|
||||||
|
Accounts.save()
|
||||||
|
res.send(`custom embed color saved`)
|
||||||
|
} else {
|
||||||
|
res.status(400)
|
||||||
|
res.send("invalid hex code")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/embedsize", requiresAccount, parser, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.largeImage != "boolean") req.body.color = false;
|
||||||
|
|
||||||
|
if (!acc.embed) acc.embed = {}
|
||||||
|
acc.embed.largeImage = req.body.largeImage
|
||||||
|
if (!req.body.largeImage) delete acc.embed.largeImage
|
||||||
|
Accounts.save()
|
||||||
|
res.send(`custom embed image size saved`)
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/delete_account", requiresAccount, parser, async (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
let accId = acc.id
|
||||||
|
|
||||||
|
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
||||||
|
auth.invalidate(v.token)
|
||||||
|
})
|
||||||
|
|
||||||
|
let cpl = () => Accounts.deleteAccount(accId).then(_ => res.send("account deleted"))
|
||||||
|
|
||||||
|
if (req.body.deleteFiles) {
|
||||||
|
let f = acc.files.map(e=>e) // make shallow copy so that iterating over it doesnt Die
|
||||||
|
for (let v of f) {
|
||||||
|
files.unlink(v,true).catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
||||||
|
if (err) console.log(err)
|
||||||
|
cpl()
|
||||||
|
})
|
||||||
|
} else cpl()
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/change_username", requiresAccount, parser, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.username != "string" || req.body.username.length < 3 || req.body.username.length > 20) {
|
||||||
|
ServeError(res,400,"username must be between 3 and 20 characters in length")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let _acc = Accounts.getFromUsername(req.body.username)
|
||||||
|
|
||||||
|
if (_acc) {
|
||||||
|
ServeError(res,400,"account with this username already exists")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((req.body.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != req.body.username) {
|
||||||
|
ServeError(res,400,"username contains invalid characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.username = req.body.username
|
||||||
|
Accounts.save()
|
||||||
|
|
||||||
|
if (acc.email) {
|
||||||
|
sendMail(acc.email, `Your login details have been updated`, `<b>Hello there!</b> Your username has been updated to <span username>${req.body.username}</span>. Please update your devices accordingly. Thank you for using monofile.`).then(() => {
|
||||||
|
res.send("OK")
|
||||||
|
}).catch((err) => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send("username changed")
|
||||||
|
})
|
||||||
|
|
||||||
|
// shit way to do this but...
|
||||||
|
|
||||||
|
let verificationCodes = new Map<string, {code: string, email: string, expiry: NodeJS.Timeout}>()
|
||||||
|
|
||||||
|
authRoutes.post("/request_email_change", requiresAccount, accountRatelimit({ requests: 4, per: 60*60*1000 }), parser, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof req.body.email != "string" || !req.body.email) {
|
||||||
|
ServeError(res,400, "supply an email")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let vcode = verificationCodes.get(acc.id)
|
||||||
|
|
||||||
|
// delete previous if any
|
||||||
|
let e = vcode?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
verificationCodes.delete(acc?.id||"")
|
||||||
|
|
||||||
|
let code = generateFileId(12).toUpperCase()
|
||||||
|
|
||||||
|
// set
|
||||||
|
|
||||||
|
verificationCodes.set(acc.id, {
|
||||||
|
code,
|
||||||
|
email: req.body.email,
|
||||||
|
expiry: setTimeout( () => verificationCodes.delete(acc?.id||""), 15*60*1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
// this is a mess but it's fine
|
||||||
|
|
||||||
|
sendMail(req.body.email, `Hey there, ${acc.username} - let's connect your email`, `<b>Hello there!</b> You are recieving this message because you decided to link your email, <span code>${req.body.email.split("@")[0]}<span style="opacity:0.5">@${req.body.email.split("@")[1]}</span></span>, to your account, <span username>${acc.username}</span>. If you would like to continue, please <a href="https://${req.header("Host")}/auth/confirm_email/${code}"><span code>click here</span></a>, or go to https://${req.header("Host")}/auth/confirm_email/${code}.`).then(() => {
|
||||||
|
res.send("OK")
|
||||||
|
}).catch((err) => {
|
||||||
|
let e = verificationCodes.get(acc?.id||"")?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
verificationCodes.delete(acc?.id||"")
|
||||||
|
res.locals.undoCount();
|
||||||
|
ServeError(res, 500, err?.toString())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.get("/confirm_email/:code", requiresAccount, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
|
||||||
|
let vcode = verificationCodes.get(acc.id)
|
||||||
|
|
||||||
|
if (!vcode) { ServeError(res, 400, "nothing to confirm"); return }
|
||||||
|
|
||||||
|
if (typeof req.params.code == "string" && req.params.code.toUpperCase() == vcode.code) {
|
||||||
|
acc.email = vcode.email
|
||||||
|
Accounts.save();
|
||||||
|
|
||||||
|
let e = verificationCodes.get(acc?.id||"")?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
verificationCodes.delete(acc?.id||"")
|
||||||
|
|
||||||
|
res.redirect("/")
|
||||||
|
} else {
|
||||||
|
ServeError(res, 400, "invalid code")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let pwReset = new Map<string, {code: string, expiry: NodeJS.Timeout, requestedAt:number}>()
|
||||||
|
let prcIdx = new Map<string, string>()
|
||||||
|
|
||||||
|
authRoutes.post("/request_emergency_login", parser, (req,res) => {
|
||||||
|
if (auth.validate(req.cookies.auth || "")) return
|
||||||
|
|
||||||
|
if (typeof req.body.account != "string" || !req.body.account) {
|
||||||
|
ServeError(res,400, "supply a username")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let acc = Accounts.getFromUsername(req.body.account)
|
||||||
|
if (!acc || !acc.email) {
|
||||||
|
ServeError(res, 400, "this account either does not exist or does not have an email attached; please contact the server's admin for a reset if you would still like to access it")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let pResetCode = pwReset.get(acc.id)
|
||||||
|
|
||||||
|
if (pResetCode && pResetCode.requestedAt+(15*60*1000) > Date.now()) {
|
||||||
|
ServeError(res, 429, `Please wait a few moments to request another emergency login.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// delete previous if any
|
||||||
|
let e = pResetCode?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
pwReset.delete(acc?.id||"")
|
||||||
|
prcIdx.delete(pResetCode?.code||"")
|
||||||
|
|
||||||
|
let code = generateFileId(12).toUpperCase()
|
||||||
|
|
||||||
|
// set
|
||||||
|
|
||||||
|
pwReset.set(acc.id, {
|
||||||
|
code,
|
||||||
|
expiry: setTimeout( () => { pwReset.delete(acc?.id||""); prcIdx.delete(pResetCode?.code||"") }, 15*60*1000),
|
||||||
|
requestedAt: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
prcIdx.set(code, acc.id)
|
||||||
|
|
||||||
|
// this is a mess but it's fine
|
||||||
|
|
||||||
|
sendMail(acc.email, `Emergency login requested for ${acc.username}`, `<b>Hello there!</b> You are recieving this message because you forgot your password to your monofile account, <span username>${acc.username}</span>. To log in, please <a href="https://${req.header("Host")}/auth/emergency_login/${code}"><span code>click here</span></a>, or go to https://${req.header("Host")}/auth/emergency_login/${code}. If it doesn't appear that you are logged in after visiting this link, please try refreshing. Once you have successfully logged in, you may reset your password.`).then(() => {
|
||||||
|
res.send("OK")
|
||||||
|
}).catch((err) => {
|
||||||
|
let e = pwReset.get(acc?.id||"")?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
pwReset.delete(acc?.id||"")
|
||||||
|
prcIdx.delete(code||"")
|
||||||
|
ServeError(res, 500, err?.toString())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.get("/emergency_login/:code", (req,res) => {
|
||||||
|
if (auth.validate(req.cookies.auth || "")) {
|
||||||
|
ServeError(res, 403, "already logged in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let vcode = prcIdx.get(req.params.code)
|
||||||
|
|
||||||
|
if (!vcode) { ServeError(res, 400, "invalid emergency login code"); return }
|
||||||
|
|
||||||
|
if (typeof req.params.code == "string" && vcode) {
|
||||||
|
res.cookie("auth",auth.create(vcode,(3*24*60*60*1000)))
|
||||||
|
res.redirect("/")
|
||||||
|
|
||||||
|
let e = pwReset.get(vcode)?.expiry
|
||||||
|
if (e) clearTimeout(e)
|
||||||
|
pwReset.delete(vcode)
|
||||||
|
prcIdx.delete(req.params.code)
|
||||||
|
} else {
|
||||||
|
ServeError(res, 400, "invalid code")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/change_password", requiresAccount, parser, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
if (typeof req.body.password != "string" || req.body.password.length < 8) {
|
||||||
|
ServeError(res,400,"password must be 8 characters or longer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let accId = acc.id
|
||||||
|
|
||||||
|
Accounts.password.set(accId,req.body.password)
|
||||||
|
|
||||||
|
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
||||||
|
auth.invalidate(v.token)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (acc.email) {
|
||||||
|
sendMail(acc.email, `Your login details have been updated`, `<b>Hello there!</b> This email is to notify you of a password change that you have initiated. You have been logged out of your devices. Thank you for using monofile.`).then(() => {
|
||||||
|
res.send("OK")
|
||||||
|
}).catch((err) => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send("password changed - logged out all sessions")
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.post("/logout_sessions", requiresAccount, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
let accId = acc.id
|
||||||
|
|
||||||
|
auth.AuthTokens.filter(e => e.account == accId).forEach((v) => {
|
||||||
|
auth.invalidate(v.token)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.send("logged out all sessions")
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.get("/me", requiresAccount, (req,res) => {
|
||||||
|
let acc = res.locals.acc as Accounts.Account
|
||||||
|
|
||||||
|
let accId = acc.id
|
||||||
|
res.send({
|
||||||
|
...acc,
|
||||||
|
sessionCount: auth.AuthTokens.filter(e => e.account == accId && e.expire > Date.now()).length,
|
||||||
|
sessionExpires: auth.AuthTokens.find(e => e.token == req.cookies.auth)?.expire,
|
||||||
|
password: undefined
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
authRoutes.get("/customCSS", (req,res) => {
|
||||||
|
if (!auth.validate(req.cookies.auth)) {
|
||||||
|
ServeError(res, 401, "not logged in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazy rn so
|
||||||
|
|
||||||
|
let acc = Accounts.getFromToken(req.cookies.auth)
|
||||||
|
if (acc) {
|
||||||
|
if (acc.customCSS) {
|
||||||
|
res.redirect(`/file/${acc.customCSS}`)
|
||||||
|
} else {
|
||||||
|
res.send("")
|
||||||
|
}
|
||||||
|
} else res.send("")
|
||||||
|
})
|
104
src/server/routes/fileApiRoutes.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
import { Router } from "express";
|
||||||
|
import * as Accounts from "../lib/accounts";
|
||||||
|
import * as auth from "../lib/auth";
|
||||||
|
import bytes from "bytes"
|
||||||
|
import {writeFile} from "fs";
|
||||||
|
|
||||||
|
import ServeError from "../lib/errors";
|
||||||
|
import Files from "../lib/files";
|
||||||
|
|
||||||
|
let parser = bodyParser.json({
|
||||||
|
type: ["text/plain","application/json"]
|
||||||
|
})
|
||||||
|
|
||||||
|
export let fileApiRoutes = Router();
|
||||||
|
let files:Files
|
||||||
|
|
||||||
|
export function setFilesObj(newFiles:Files) {
|
||||||
|
files = newFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
|
fileApiRoutes.get("/list", (req,res) => {
|
||||||
|
|
||||||
|
if (!auth.validate(req.cookies.auth)) {
|
||||||
|
ServeError(res, 401, "not logged in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let acc = Accounts.getFromToken(req.cookies.auth)
|
||||||
|
|
||||||
|
if (!acc) return
|
||||||
|
let accId = acc.id
|
||||||
|
|
||||||
|
res.send(acc.files.map((e) => {
|
||||||
|
let fp = files.getFilePointer(e)
|
||||||
|
if (!fp) { Accounts.files.deindex(accId, e); return null }
|
||||||
|
return {
|
||||||
|
...fp,
|
||||||
|
messageids: null,
|
||||||
|
owner: null,
|
||||||
|
id:e
|
||||||
|
}
|
||||||
|
}).filter(e=>e))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
fileApiRoutes.post("/manage", parser, (req,res) => {
|
||||||
|
|
||||||
|
if (!auth.validate(req.cookies.auth)) {
|
||||||
|
ServeError(res, 401, "not logged in")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let acc = Accounts.getFromToken(req.cookies.auth) as Accounts.Account
|
||||||
|
|
||||||
|
if (!acc) return
|
||||||
|
if (!req.body.target || !(typeof req.body.target == "object") || req.body.target.length < 1) return
|
||||||
|
|
||||||
|
let modified = 0
|
||||||
|
|
||||||
|
req.body.target.forEach((e:string) => {
|
||||||
|
if (!acc.files.includes(e)) return
|
||||||
|
|
||||||
|
let fp = files.getFilePointer(e)
|
||||||
|
|
||||||
|
if (fp.reserved) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch( req.body.action ) {
|
||||||
|
case "delete":
|
||||||
|
files.unlink(e, true)
|
||||||
|
modified++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "changeFileVisibility":
|
||||||
|
if (!["public","anonymous","private"].includes(req.body.value)) return;
|
||||||
|
files.files[e].visibility = req.body.value;
|
||||||
|
modified++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "setTag":
|
||||||
|
if (!req.body.value) delete files.files[e].tag
|
||||||
|
else {
|
||||||
|
if (req.body.value.toString().length > 30) return
|
||||||
|
files.files[e].tag = req.body.value.toString().toLowerCase()
|
||||||
|
}
|
||||||
|
modified++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Accounts.save().then(() => {
|
||||||
|
writeFile(process.cwd()+"/.data/files.json",JSON.stringify(files.files), (err) => {
|
||||||
|
if (err) console.log(err)
|
||||||
|
res.contentType("text/plain")
|
||||||
|
res.send(`modified ${modified} files`)
|
||||||
|
})
|
||||||
|
}).catch((err) => console.error(err))
|
||||||
|
|
||||||
|
|
||||||
|
})
|
171
src/server/routes/primaryApi.ts
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import bodyParser from "body-parser";
|
||||||
|
import express, { Router } from "express";
|
||||||
|
import * as Accounts from "../lib/accounts";
|
||||||
|
import * as auth from "../lib/auth";
|
||||||
|
import axios, { AxiosResponse } from "axios"
|
||||||
|
import { type Range } from "range-parser";
|
||||||
|
import multer, {memoryStorage} from "multer"
|
||||||
|
|
||||||
|
import ServeError from "../lib/errors";
|
||||||
|
import Files from "../lib/files";
|
||||||
|
|
||||||
|
let parser = bodyParser.json({
|
||||||
|
type: ["text/plain","application/json"]
|
||||||
|
})
|
||||||
|
|
||||||
|
export let primaryApi = Router();
|
||||||
|
let files:Files
|
||||||
|
|
||||||
|
export function setFilesObj(newFiles:Files) {
|
||||||
|
files = newFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
const multerSetup = multer({storage:memoryStorage()})
|
||||||
|
|
||||||
|
let config = require(`${process.cwd()}/config.json`)
|
||||||
|
|
||||||
|
|
||||||
|
primaryApi.get(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], async (req:express.Request,res:express.Response) => {
|
||||||
|
|
||||||
|
let file = files.getFilePointer(req.params.fileId)
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||||
|
res.setHeader("Content-Security-Policy","sandbox allow-scripts")
|
||||||
|
if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
|
||||||
|
if (file.visibility == "private" && Accounts.getFromToken(req.cookies.auth)?.id != file.owner) {
|
||||||
|
ServeError(res,403,"you do not own this file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let range: Range | undefined
|
||||||
|
|
||||||
|
res.setHeader("Content-Type",file.mime)
|
||||||
|
if (file.sizeInBytes) {
|
||||||
|
res.setHeader("Content-Length",file.sizeInBytes)
|
||||||
|
|
||||||
|
if (file.chunkSize) {
|
||||||
|
let rng = req.range(file.sizeInBytes)
|
||||||
|
if (rng) {
|
||||||
|
|
||||||
|
// error handling
|
||||||
|
if (typeof rng == "number") {
|
||||||
|
res.status(rng == -1 ? 416 : 400).send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rng.type != "bytes") {
|
||||||
|
res.status(400).send();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set ranges var
|
||||||
|
let rngs = Array.from(rng)
|
||||||
|
if (rngs.length != 1) { res.status(400).send(); return }
|
||||||
|
range = rngs[0]
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// supports ranges
|
||||||
|
|
||||||
|
|
||||||
|
files.readFileStream(req.params.fileId, range).then(async stream => {
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
res.status(206)
|
||||||
|
res.header("Content-Length", (range.end-range.start + 1).toString())
|
||||||
|
res.header("Content-Range", `bytes ${range.start}-${range.end}/${file.sizeInBytes}`)
|
||||||
|
}
|
||||||
|
stream.pipe(res)
|
||||||
|
|
||||||
|
}).catch((err) => {
|
||||||
|
ServeError(res,err.status,err.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
ServeError(res, 404, "file not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
primaryApi.head(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], (req: express.Request, res:express.Response) => {
|
||||||
|
let file = files.getFilePointer(req.params.fileId)
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||||
|
res.setHeader("Content-Security-Policy","sandbox allow-scripts")
|
||||||
|
if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment")
|
||||||
|
if (!file) {
|
||||||
|
res.status(404)
|
||||||
|
res.send()
|
||||||
|
} else {
|
||||||
|
res.setHeader("Content-Type",file.mime)
|
||||||
|
if (file.sizeInBytes) {
|
||||||
|
res.setHeader("Content-Length",file.sizeInBytes)
|
||||||
|
}
|
||||||
|
if (file.chunkSize) {
|
||||||
|
res.setHeader("Accept-Ranges", "bytes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// upload handlers
|
||||||
|
|
||||||
|
primaryApi.post("/upload",multerSetup.single('file'),async (req,res) => {
|
||||||
|
if (req.file) {
|
||||||
|
try {
|
||||||
|
let prm = req.header("monofile-params")
|
||||||
|
let params:{[key:string]:any} = {}
|
||||||
|
if (prm) {
|
||||||
|
params = JSON.parse(prm)
|
||||||
|
}
|
||||||
|
|
||||||
|
files.uploadFile({
|
||||||
|
owner: auth.validate(req.cookies.auth),
|
||||||
|
|
||||||
|
uploadId:params.uploadId,
|
||||||
|
name:req.file.originalname,
|
||||||
|
mime:req.file.mimetype
|
||||||
|
},req.file.buffer)
|
||||||
|
.then((uID) => res.send(uID))
|
||||||
|
.catch((stat) => {
|
||||||
|
res.status(stat.status);
|
||||||
|
res.send(`[err] ${stat.message}`)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
res.status(400)
|
||||||
|
res.send("[err] bad request")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(400)
|
||||||
|
res.send("[err] bad request")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
primaryApi.post("/clone", bodyParser.json({type: ["text/plain","application/json"]}) ,(req,res) => {
|
||||||
|
try {
|
||||||
|
axios.get(req.body.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => {
|
||||||
|
|
||||||
|
files.uploadFile({
|
||||||
|
owner: auth.validate(req.cookies.auth),
|
||||||
|
|
||||||
|
name:req.body.url.split("/")[req.body.url.split("/").length-1] || "generic",
|
||||||
|
mime:data.headers["content-type"],
|
||||||
|
uploadId:req.body.uploadId
|
||||||
|
},Buffer.from(data.data))
|
||||||
|
.then((uID) => res.send(uID))
|
||||||
|
.catch((stat) => {
|
||||||
|
res.status(stat.status);
|
||||||
|
res.send(`[err] ${stat.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
res.status(400)
|
||||||
|
res.send(`[err] failed to fetch data`)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
res.status(500)
|
||||||
|
res.send("[err] an error occured")
|
||||||
|
}
|
||||||
|
})
|
85
src/style/_base.scss
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
could probably replace this with fonts served directly
|
||||||
|
from the server but it's fine for now
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import url("/static/assets/fonts/inconsolata.css");
|
||||||
|
@import url("/static/assets/fonts/source_sans.css");
|
||||||
|
@import url("/static/assets/fonts/fira_code.css");
|
||||||
|
|
||||||
|
$FallbackFonts:
|
||||||
|
-apple-system,
|
||||||
|
system-ui,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
|
%normal {
|
||||||
|
font-family: "Source Sans Pro", $FallbackFonts
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
everything that's not a span
|
||||||
|
and/or has the normal class
|
||||||
|
(it's just in case)
|
||||||
|
*/
|
||||||
|
|
||||||
|
*:not(span), .normal { @extend %normal; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
for code blocks / terminal
|
||||||
|
*/
|
||||||
|
|
||||||
|
.monospace {
|
||||||
|
font-family: "Fira Code", monospace
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
colors
|
||||||
|
*/
|
||||||
|
|
||||||
|
$Background: #252525;
|
||||||
|
/* hsl(210,12.9,24.3) */
|
||||||
|
$darkish: rgb(54, 62, 70);
|
||||||
|
|
||||||
|
/*
|
||||||
|
then other stuff
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: rgb(30, 33, 36); // this is here so that
|
||||||
|
// pulling down to refresh
|
||||||
|
// on mobile looks good
|
||||||
|
}
|
||||||
|
|
||||||
|
#appContent {
|
||||||
|
background-color: $Background
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
scrollbars
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
/* nice scrollbars aren't needed on mobile so */
|
||||||
|
@media screen and (min-width:500px) {
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color:#191919;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color:#333;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color:#373737;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
41
src/style/app.scss
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
@use "base";
|
||||||
|
@use "app/topbar";
|
||||||
|
@use "app/pulldown";
|
||||||
|
@use "app/uploads";
|
||||||
|
|
||||||
|
.menuBtn {
|
||||||
|
text-decoration:none;
|
||||||
|
font-size:16px;
|
||||||
|
transition-duration: 100ms;
|
||||||
|
|
||||||
|
color:#555555;
|
||||||
|
background-color: #00000000;
|
||||||
|
border:none;
|
||||||
|
margin:0 0 0 0;
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
position:relative;
|
||||||
|
top:-1px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color:slategray;
|
||||||
|
transition-duration: 100ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#appContent {
|
||||||
|
position:absolute;
|
||||||
|
left:0px;
|
||||||
|
top:40px;
|
||||||
|
width:100%;
|
||||||
|
height: calc( 100% - 40px );
|
||||||
|
background-image: linear-gradient(#333,base.$Background);
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
background-image: linear-gradient(#303030,base.$Background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
font-family: "Inconsolata", monospace;
|
||||||
|
}
|
49
src/style/app/pulldown.scss
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
@use "../base";
|
||||||
|
@use "pulldown/help";
|
||||||
|
@use "pulldown/accounts";
|
||||||
|
@use "pulldown/files";
|
||||||
|
@use "pulldown/modals";
|
||||||
|
|
||||||
|
#overlay, .modalContainer {
|
||||||
|
position:absolute;
|
||||||
|
left:0px;
|
||||||
|
height: 100%;
|
||||||
|
width:100%;
|
||||||
|
top:0px;
|
||||||
|
border:none;
|
||||||
|
outline:none;
|
||||||
|
background-color:rgba(170, 170, 170, 0.25);
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulldown {
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
height: 400px;
|
||||||
|
background-color: #191919;
|
||||||
|
color: #dddddd;
|
||||||
|
|
||||||
|
top:0px;
|
||||||
|
left:50%;
|
||||||
|
transform:translateX(-50%);
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, h1, h2 {
|
||||||
|
margin:0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulldown_display {
|
||||||
|
position:absolute;
|
||||||
|
left:0px;
|
||||||
|
top:0px;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
}
|
187
src/style/app/pulldown/accounts.scss
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
.pulldown_display[name=accounts] {
|
||||||
|
.notLoggedIn {
|
||||||
|
.container_div {
|
||||||
|
position:absolute;
|
||||||
|
top:50%;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
width:100%;
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight:600;
|
||||||
|
font-size:24px;
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size:30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flavor {
|
||||||
|
font-size:14px;
|
||||||
|
|
||||||
|
/* good enoough */
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
color:#999999;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor:pointer;
|
||||||
|
background-color:#393939;
|
||||||
|
color:#DDDDDD;
|
||||||
|
border:none;
|
||||||
|
outline:none;
|
||||||
|
padding:5px;
|
||||||
|
transition-duration: 250ms;
|
||||||
|
/*overflow:clip;*/
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition-duration: 250ms;
|
||||||
|
background-color:#434343;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
flex-basis:50%;
|
||||||
|
flex-grow:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.flavor {
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text],input[type=password] {
|
||||||
|
border:none;
|
||||||
|
border-radius:0;
|
||||||
|
width:100%;
|
||||||
|
padding:5px;
|
||||||
|
background-color:#333333;
|
||||||
|
color:#dddddd;
|
||||||
|
outline:none;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwError {
|
||||||
|
div {
|
||||||
|
border:none;
|
||||||
|
border-radius:0;
|
||||||
|
width:100%;
|
||||||
|
padding:5px;
|
||||||
|
background-color:#663333;
|
||||||
|
color:#dddddd;
|
||||||
|
outline:none;
|
||||||
|
font-size:14px;
|
||||||
|
text-align:left;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lgBtnContainer {
|
||||||
|
display:flex;
|
||||||
|
position:relative;
|
||||||
|
left:20px;
|
||||||
|
width:calc( 100% - 40px );
|
||||||
|
gap:10px;
|
||||||
|
overflow:clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
position:relative;
|
||||||
|
left:20px;
|
||||||
|
width:calc( 100% - 40px );
|
||||||
|
gap:5px;
|
||||||
|
overflow:clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color:#999999;
|
||||||
|
font-size:14px;
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content:" ➜";
|
||||||
|
font-size:0px;
|
||||||
|
opacity: 0;
|
||||||
|
transition-duration:250ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
font-size:13px;
|
||||||
|
opacity: 1;
|
||||||
|
transition-duration:250ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.loggedIn {
|
||||||
|
position:absolute;
|
||||||
|
|
||||||
|
/*
|
||||||
|
left:10px;
|
||||||
|
top:10px;
|
||||||
|
*/
|
||||||
|
|
||||||
|
left:0px;
|
||||||
|
top:0px;
|
||||||
|
width:calc( 100% - 20px );
|
||||||
|
height:calc( 100% - 20px );
|
||||||
|
padding:10px;
|
||||||
|
|
||||||
|
overflow-y:auto;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight:600;
|
||||||
|
font-size:20px;
|
||||||
|
color: #AAAAAA;
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size:24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monospace {
|
||||||
|
font-size:18px;
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size:22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
|
||||||
|
p {
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
160
src/style/app/pulldown/files.scss
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
.pulldown_display[name=files] {
|
||||||
|
.notLoggedIn {
|
||||||
|
position:absolute;
|
||||||
|
top:50%;
|
||||||
|
left:0px;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
width:100%;
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
.flavor {
|
||||||
|
font-size:16px;
|
||||||
|
color:#999999;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
--col: #999999;
|
||||||
|
|
||||||
|
background-color: #232323;
|
||||||
|
color:var(--col);
|
||||||
|
font-size:14px;
|
||||||
|
border:1px solid var(--col);
|
||||||
|
padding:2px 20px 2px 20px;
|
||||||
|
cursor:pointer;
|
||||||
|
transition-duration:250ms;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color:#333333;
|
||||||
|
transition-duration:250ms;
|
||||||
|
--col:#BBBBBB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loggedIn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height:100%;
|
||||||
|
overflow:hidden;
|
||||||
|
|
||||||
|
.searchBar {
|
||||||
|
transition-duration:150ms;
|
||||||
|
background-color:#171717;
|
||||||
|
width:100%;
|
||||||
|
padding:8px;
|
||||||
|
color:#dddddd;
|
||||||
|
border:none;
|
||||||
|
border-bottom: 1px solid #aaaaaa;
|
||||||
|
outline: none;
|
||||||
|
border-radius:0px;
|
||||||
|
font-size:14px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
transition-duration:150ms;
|
||||||
|
border-bottom: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
padding:12px;
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileList {
|
||||||
|
overflow-y:auto;
|
||||||
|
overflow-x:hidden;
|
||||||
|
padding:5px 0;
|
||||||
|
|
||||||
|
.flFile {
|
||||||
|
padding: 3px 8px;
|
||||||
|
position:relative;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
padding:7px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail {
|
||||||
|
color:#777777;
|
||||||
|
font-size:14px;
|
||||||
|
position:relative;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
/* this is shit but it's the best way i can think of to do this */
|
||||||
|
/* other than flexbox but i don't feel like doing that rn */
|
||||||
|
|
||||||
|
position:relative;
|
||||||
|
top:2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size:18px;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
overflow:hidden;
|
||||||
|
font-weight:600;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p, h2 {
|
||||||
|
margin:0 0 0 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color:#00000000;
|
||||||
|
border:none;
|
||||||
|
outline:none;
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
&.hitbox {
|
||||||
|
position:absolute;
|
||||||
|
left:0px;
|
||||||
|
top:0px;
|
||||||
|
height:100%;
|
||||||
|
width:100%;
|
||||||
|
z-index:10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.more {
|
||||||
|
min-height:100%;
|
||||||
|
width:auto;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
z-index:11;
|
||||||
|
position:relative;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin:auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexCont {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.fileInfo {
|
||||||
|
width:100%;
|
||||||
|
min-width:0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width:500px) {
|
||||||
|
&:hover {
|
||||||
|
background-color: #252525;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
src/style/app/pulldown/help.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
.pulldown_display[name=help] {
|
||||||
|
|
||||||
|
overflow-y:auto;
|
||||||
|
|
||||||
|
.faqGroup {
|
||||||
|
padding:6px 10px 4px 10px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 400;
|
||||||
|
color:#DDDDDD;
|
||||||
|
font-size:16px;
|
||||||
|
margin:0 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color:#999999;
|
||||||
|
font-size:16px;
|
||||||
|
margin:0 0 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
115
src/style/app/pulldown/modals.scss
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
.optPicker {
|
||||||
|
|
||||||
|
button, .inp {
|
||||||
|
position:relative;
|
||||||
|
width:100%;
|
||||||
|
height:50px;
|
||||||
|
background-color: #191919;
|
||||||
|
border:none;
|
||||||
|
border-bottom:1px solid #AAAAAA;
|
||||||
|
transition-duration:150ms;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position:absolute;
|
||||||
|
left:13px;
|
||||||
|
top:50%;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
p,input {
|
||||||
|
text-align:left;
|
||||||
|
position:absolute;
|
||||||
|
top:50%;
|
||||||
|
left:50px;
|
||||||
|
color:#DDDDDD;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
font-size:14px;
|
||||||
|
background-color:#00000000;
|
||||||
|
border:none;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color:#777777;
|
||||||
|
font-size:12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
height:100%;
|
||||||
|
width:calc(100% - 50px);
|
||||||
|
outline:none; /* bad idea but i don't even care anymore */
|
||||||
|
margin:0px;
|
||||||
|
padding:0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
height:70px;
|
||||||
|
p,input {
|
||||||
|
font-size:16px;
|
||||||
|
left:70px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size:14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width:calc( 100% - 70px );
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width:30px;
|
||||||
|
height:30px;
|
||||||
|
left:20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition-duration:150ms;
|
||||||
|
background-color: #252525;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
border-bottom: 1px solid #AAAAAA;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #AAAAAA;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 10px 0px 3px 0px;
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdHitbox {
|
||||||
|
position:absolute;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
top:0%;
|
||||||
|
left:0%;
|
||||||
|
cursor:pointer;
|
||||||
|
z-index: 0;
|
||||||
|
border:none;
|
||||||
|
background-color: #00000000;
|
||||||
|
outline:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position:absolute;
|
||||||
|
background-color:#191919;
|
||||||
|
width:100%;
|
||||||
|
transform:translateY(-100%);
|
||||||
|
top:100%;
|
||||||
|
left:0%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
21
src/style/app/topbar.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
@use "../base";
|
||||||
|
|
||||||
|
#topbar {
|
||||||
|
position:absolute;
|
||||||
|
left:0px;
|
||||||
|
top:0px;
|
||||||
|
|
||||||
|
width:100%;
|
||||||
|
height:40px;
|
||||||
|
|
||||||
|
/* hsl(210,9.1,12.9) */
|
||||||
|
background-color: rgb(30, 33, 36);
|
||||||
|
|
||||||
|
display:flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
column-gap:5px;
|
||||||
|
|
||||||
|
}
|
115
src/style/app/uploader/add_new_files.scss
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
#uploadWindow {
|
||||||
|
#add_new_files {
|
||||||
|
background-color:#191919;
|
||||||
|
border: 1px solid gray;
|
||||||
|
padding: 0px 0px 10px 0px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: "Fira Code", monospace;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0px 0px 0px 10px;
|
||||||
|
font-size: 30px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
position:relative;
|
||||||
|
|
||||||
|
&._add_files_txt {
|
||||||
|
font-size:16px;
|
||||||
|
top:-4px;
|
||||||
|
left:10px;
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size:20px;
|
||||||
|
top:-6px;
|
||||||
|
left:10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size: 40px;
|
||||||
|
|
||||||
|
span._add_files_txt {
|
||||||
|
font-size:20px;
|
||||||
|
top:-6px;
|
||||||
|
left:10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#file_add_btns {
|
||||||
|
width:calc( 100% - 20px );
|
||||||
|
margin:auto;
|
||||||
|
position:relative;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:row;
|
||||||
|
column-gap:10px;
|
||||||
|
|
||||||
|
button, input[type=text] {
|
||||||
|
background-color:#333333;
|
||||||
|
color:#DDDDDD;
|
||||||
|
border:none;
|
||||||
|
border-radius: 0px;
|
||||||
|
outline:none;
|
||||||
|
padding:5px;
|
||||||
|
|
||||||
|
flex-basis:50%;
|
||||||
|
flex-grow:1;
|
||||||
|
transition-duration:250ms;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor:pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@media screen and (min-width: 500px) {
|
||||||
|
transition-duration:250ms;
|
||||||
|
flex-basis: 60%;
|
||||||
|
}
|
||||||
|
background-color:#393939;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileUpload {
|
||||||
|
width:100%;
|
||||||
|
height:100px;
|
||||||
|
position:relative;
|
||||||
|
|
||||||
|
background-color:#262626;
|
||||||
|
transition-duration:250ms;
|
||||||
|
|
||||||
|
input[type=file] {
|
||||||
|
opacity: 0;
|
||||||
|
position:absolute;
|
||||||
|
left:0px;
|
||||||
|
top:0px;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
position:absolute;
|
||||||
|
top:50%;
|
||||||
|
transform:translateY(-50%);
|
||||||
|
font-size:12px;
|
||||||
|
width:100%;
|
||||||
|
text-align:center;
|
||||||
|
padding:0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition-duration:250ms;
|
||||||
|
background-color:#292929;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
src/style/app/uploader/file.scss
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// should probably start using mixins for thingss like this
|
||||||
|
|
||||||
|
#uploadWindow {
|
||||||
|
.file {
|
||||||
|
background-color:#191919;
|
||||||
|
border: 1px solid gray;
|
||||||
|
padding: 10px;
|
||||||
|
overflow:clip;
|
||||||
|
position:relative;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0px;
|
||||||
|
font-weight:600;
|
||||||
|
width:calc( 100% - 20px );
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
background-color:#333333;
|
||||||
|
color:#DDDDDD;
|
||||||
|
border:none;
|
||||||
|
outline:none;
|
||||||
|
padding:5px;
|
||||||
|
position:relative;
|
||||||
|
|
||||||
|
width:100%;
|
||||||
|
transition-duration:250ms;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display:flex;
|
||||||
|
column-gap:10px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex-basis: 50%;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadingContainer {
|
||||||
|
color: #AAAAAA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hitbox {
|
||||||
|
opacity:0;
|
||||||
|
position:absolute;
|
||||||
|
left:0px;
|
||||||
|
top:0px;
|
||||||
|
height:100%;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
src/style/app/uploads.scss
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
@use "uploader/add_new_files";
|
||||||
|
@use "uploader/file";
|
||||||
|
|
||||||
|
#uploadWindow {
|
||||||
|
position:absolute;
|
||||||
|
left:50%;
|
||||||
|
top:50%;
|
||||||
|
transform:translate(-50%,-50%);
|
||||||
|
padding:10px 15px 10px 15px;
|
||||||
|
display:flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width:350px;
|
||||||
|
@media screen and (min-width:500px) {
|
||||||
|
max-height: calc( 100% - 80px );
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color:#222222;
|
||||||
|
color:#ddd;
|
||||||
|
|
||||||
|
h1, p, a {
|
||||||
|
margin: 0px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color:#999;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight:600;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadContainer {
|
||||||
|
overflow:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor:pointer;
|
||||||
|
background-color:#393939;
|
||||||
|
color:#DDDDDD;
|
||||||
|
border:none;
|
||||||
|
outline:none;
|
||||||
|
padding:5px;
|
||||||
|
transition-duration: 250ms;
|
||||||
|
/*overflow:clip;*/
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition-duration: 250ms;
|
||||||
|
background-color:#434343;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
width: calc( 100% - 20px );
|
||||||
|
height: calc( 100% - 20px );
|
||||||
|
border-radius:0px;
|
||||||
|
background-color:#00000000;
|
||||||
|
|
||||||
|
transform:none;
|
||||||
|
left:10px;
|
||||||
|
top:10px;
|
||||||
|
padding:0px;
|
||||||
|
}
|
||||||
|
}
|
26
src/style/downloads.scss
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// probably dont need to import the entire
|
||||||
|
// uploads css file
|
||||||
|
// so i might just make a separate file with mixins
|
||||||
|
// and import them
|
||||||
|
|
||||||
|
@use "app/uploads";
|
||||||
|
@use "base";
|
||||||
|
|
||||||
|
#appContent {
|
||||||
|
position:absolute;
|
||||||
|
left:0px;
|
||||||
|
top:0px;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
background-image: linear-gradient(#333,base.$Background);
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
background-image: linear-gradient(#303030,base.$Background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadWindow {
|
||||||
|
img, video, audio {
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
}
|
20
src/style/error.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@use "_base";
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size:20px;
|
||||||
|
color: lightslategray;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
.code {
|
||||||
|
font-size:25px;
|
||||||
|
font-family: "Inconsolata", monospace;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
168
src/style/themes/classy.scss
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
#uploadWindow {
|
||||||
|
color: #FFFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color:#DDDDDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
#appContent {
|
||||||
|
background: darkgray;
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
background:white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadWindow {
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
color:black;
|
||||||
|
|
||||||
|
h1, p, a {
|
||||||
|
margin: 0px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color:rgb(153, 153, 153);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight:600;
|
||||||
|
font-size: 25px;
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > p:nth-of-type(1) {
|
||||||
|
text-align:center;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight:600;
|
||||||
|
font-size: 16px;
|
||||||
|
color:black !important;
|
||||||
|
@media screen and (max-width:500px) {
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor:pointer;
|
||||||
|
color:black;
|
||||||
|
border:none;
|
||||||
|
outline:none;
|
||||||
|
padding:5px;
|
||||||
|
background: #AAAAAA;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
outline: 1px solid #333333;
|
||||||
|
color: black;
|
||||||
|
background-color:#AAAAAA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > button:nth-last-of-type(1) {
|
||||||
|
background-color:#66AAFF;
|
||||||
|
&:hover {
|
||||||
|
background-color:#66AAFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#add_new_files {
|
||||||
|
background-color: #AAAAAA66;
|
||||||
|
border:1px solid #AAAAAA;
|
||||||
|
|
||||||
|
#file_add_btns {
|
||||||
|
button, input[type=text] {
|
||||||
|
transition-duration:0s;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
font-size:16px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
font-family: "Fira Code", monospace;
|
||||||
|
background-color:#AAAAAA;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor:pointer;
|
||||||
|
background-color:#AAAAAA;
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
flex-basis: 50%;
|
||||||
|
transition-duration:0s;
|
||||||
|
background-color:#AAAAAA;
|
||||||
|
color: black;
|
||||||
|
outline: 1px solid #333333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fileUpload {
|
||||||
|
background-color:#AAAAAA;
|
||||||
|
transition-duration:250ms;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transition-duration:0s;
|
||||||
|
background-color:#AAAAAA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file {
|
||||||
|
background-color: #AAAAAA66;
|
||||||
|
border: 1px solid #AAAAAA;
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
font-family: "Fira Code", monospace;
|
||||||
|
background-color:#AAAAAA;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
/* nice scrollbars aren't needed on mobile so */
|
||||||
|
@media screen and (min-width:500px) {
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background-color:#AAAAAA;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color:#DDDDDD;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color:#FFFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar {
|
||||||
|
background-color: #DDDDDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
.code {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
28
src/svelte/App.svelte
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import Topbar from "./elem/Topbar.svelte";
|
||||||
|
import PulldownManager from "./elem/PulldownManager.svelte";
|
||||||
|
import UploadWindow from "./elem/UploadWindow.svelte";
|
||||||
|
import { pulldownManager } from "./elem/stores.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type Topbar
|
||||||
|
*/
|
||||||
|
let topbar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type PulldownManager
|
||||||
|
*/
|
||||||
|
let pulldown;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
pulldownManager.set(pulldown)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Topbar bind:this={topbar} pulldown={pulldown} />
|
||||||
|
<div id="appContent">
|
||||||
|
<PulldownManager bind:this={pulldown} />
|
||||||
|
|
||||||
|
<UploadWindow/>
|
||||||
|
</div>
|
49
src/svelte/elem/PulldownManager.svelte
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<script context="module">
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
// can't find a better way to do this
|
||||||
|
import Files from "./pulldowns/Files.svelte";
|
||||||
|
import Accounts from "./pulldowns/Accounts.svelte";
|
||||||
|
import Help from "./pulldowns/Help.svelte";
|
||||||
|
|
||||||
|
export let allPulldowns = new Map()
|
||||||
|
|
||||||
|
allPulldowns
|
||||||
|
.set("account",Accounts)
|
||||||
|
.set("help",Help)
|
||||||
|
.set("files",Files)
|
||||||
|
|
||||||
|
export const pulldownOpen = writable(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { fade, scale } from "svelte/transition";
|
||||||
|
|
||||||
|
export function isOpen() {
|
||||||
|
return $pulldownOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openPulldown(name) {
|
||||||
|
pulldownOpen.set(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closePulldown() {
|
||||||
|
pulldownOpen.set(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{#if $pulldownOpen}
|
||||||
|
<div class="pulldown" transition:fade={{duration:200}}>
|
||||||
|
<svelte:component this={allPulldowns.get($pulldownOpen)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="overlay"
|
||||||
|
on:click={closePulldown}
|
||||||
|
transition:fade={{duration:200}}
|
||||||
|
/>
|
||||||
|
{/if}
|
31
src/svelte/elem/Topbar.svelte
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import { circOut } from "svelte/easing";
|
||||||
|
import { scale } from "svelte/transition";
|
||||||
|
import PulldownManager, {pulldownOpen} from "./PulldownManager.svelte";
|
||||||
|
import { account } from "./stores.mjs";
|
||||||
|
import { _void } from "./transition/_void";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type PulldownManager
|
||||||
|
*/
|
||||||
|
export let pulldown;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="topbar">
|
||||||
|
{#if $pulldownOpen}
|
||||||
|
<button
|
||||||
|
class="menuBtn"
|
||||||
|
on:click={pulldown.closePulldown}
|
||||||
|
transition:_void={{duration:200,prop:"width",easingFunc:circOut}}
|
||||||
|
>close</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- too lazy to make this better -->
|
||||||
|
|
||||||
|
<button class="menuBtn" on:click={() => pulldown.openPulldown("files")}>files</button>
|
||||||
|
<button class="menuBtn" on:click={() => pulldown.openPulldown("account")}>{$account.username ? `@${$account.username}` : "account"}</button>
|
||||||
|
<button class="menuBtn" on:click={() => pulldown.openPulldown("help")}>help</button>
|
||||||
|
|
||||||
|
<div /> <!-- not sure what's offcenter but something is
|
||||||
|
so this div is here to ""fix"" that -->
|
||||||
|
</div>
|
231
src/svelte/elem/UploadWindow.svelte
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
<script>
|
||||||
|
import { _void } from "./transition/_void.js";
|
||||||
|
import { padding_scaleY } from "./transition/padding_scaleY.js"
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
import { circIn, circOut } from "svelte/easing";
|
||||||
|
import { serverStats, refresh_stats, account } from "./stores.mjs";
|
||||||
|
|
||||||
|
import AttachmentZone from "./uploader/AttachmentZone.svelte";
|
||||||
|
|
||||||
|
// stats
|
||||||
|
|
||||||
|
refresh_stats()
|
||||||
|
|
||||||
|
// uploads
|
||||||
|
|
||||||
|
let attachmentZone;
|
||||||
|
let uploads = {};
|
||||||
|
let uploadInProgress = false;
|
||||||
|
|
||||||
|
let handle_file_upload = (ev) => {
|
||||||
|
if (ev.detail.type == "clone") {
|
||||||
|
uploads[Math.random().toString().slice(2)] = {
|
||||||
|
type: "clone",
|
||||||
|
name: ev.detail.url,
|
||||||
|
url: ev.detail.url,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
uploadId: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadStatus:{
|
||||||
|
fileId: null,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploads = uploads
|
||||||
|
} else if (ev.detail.type == "upload") {
|
||||||
|
ev.detail.files.forEach((v,x) => {
|
||||||
|
uploads[Math.random().toString().slice(2)] = {
|
||||||
|
type: "upload",
|
||||||
|
name: v.name,
|
||||||
|
file: v,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
uploadId: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadStatus:{
|
||||||
|
fileId: null,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploads = uploads
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let handle_fetch_promise = (x,prom) => {
|
||||||
|
return prom.then(async (res) => {
|
||||||
|
let txt = await res.text()
|
||||||
|
if (txt.startsWith("[err]")) uploads[x].uploadStatus.error = txt;
|
||||||
|
else {
|
||||||
|
uploads[x].uploadStatus.fileId = txt;
|
||||||
|
|
||||||
|
refresh_stats();
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
uploads[x].uploadStatus.error = err.toString();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let upload_files = async () => {
|
||||||
|
uploadInProgress = true
|
||||||
|
|
||||||
|
let sequential = localStorage.getItem("sequentialMode") == "true"
|
||||||
|
|
||||||
|
// go through all files
|
||||||
|
for (let [x,v] of Object.entries(uploads)) {
|
||||||
|
// quick patch-in to allow for a switch to have everything upload sequentially
|
||||||
|
// switch will have a proper menu option later, for now i'm lazy so it's just gonna be a Secret
|
||||||
|
let hdl = () => {
|
||||||
|
switch(v.type) {
|
||||||
|
case "upload":
|
||||||
|
let fd = new FormData()
|
||||||
|
fd.append("file",v.file)
|
||||||
|
|
||||||
|
return handle_fetch_promise(x,fetch("/upload",{
|
||||||
|
headers: {
|
||||||
|
"monofile-params": JSON.stringify(v.params)
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: fd
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
case "clone":
|
||||||
|
return handle_fetch_promise(x,fetch("/clone",{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: v.url,
|
||||||
|
...v.params
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sequential) await hdl();
|
||||||
|
else hdl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// animation
|
||||||
|
|
||||||
|
function fileTransition(node) {
|
||||||
|
return {
|
||||||
|
duration: 300,
|
||||||
|
css: t => {
|
||||||
|
let eased = circOut(t)
|
||||||
|
|
||||||
|
return `
|
||||||
|
height: ${eased*(node.offsetHeight-22)}px;
|
||||||
|
padding: ${eased*10}px 10px;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="uploadWindow">
|
||||||
|
<h1>monofile</h1>
|
||||||
|
<p style:color="#999999">
|
||||||
|
<span class="number">{$serverStats.version ? `v${$serverStats.version}` : "•••"}</span> — Discord based file sharing
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style:min-height="10px" />
|
||||||
|
|
||||||
|
<!-- consider splitting the file thing into a separate element maybe -->
|
||||||
|
|
||||||
|
<div class="uploadContainer">
|
||||||
|
{#each Object.entries(uploads) as upload (upload[0])}
|
||||||
|
<!-- container to allow for animate directive -->
|
||||||
|
<div>
|
||||||
|
<div class="file" transition:fileTransition style:border={upload[1].uploadStatus.error ? "1px solid #BB7070" : ""}>
|
||||||
|
<h2>{upload[1].name} <span style:color="#999999" style:font-weight="400">{upload[1].type}{@html upload[1].type == "upload" ? ` (${Math.round(upload[1].file.size/1048576)}MiB)` : ""}</span></h2>
|
||||||
|
|
||||||
|
{#if upload[1].maximized && !uploadInProgress}
|
||||||
|
<div transition:padding_scaleY|local>
|
||||||
|
<div style:height="10px" />
|
||||||
|
<input placeholder="custom id" type="text" bind:value={ uploads[upload[0]].params.uploadId }>
|
||||||
|
<div style:height="10px" />
|
||||||
|
<div class="buttonContainer">
|
||||||
|
<button on:click={() => {delete uploads[upload[0]];uploads=uploads;}}>
|
||||||
|
delete
|
||||||
|
</button>
|
||||||
|
<button on:click={() => uploads[upload[0]].maximized = false}>
|
||||||
|
minimize
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if !uploadInProgress}
|
||||||
|
<button on:click={() => uploads[upload[0]].maximized = true} class="hitbox"></button>
|
||||||
|
{:else}
|
||||||
|
<div transition:padding_scaleY|local class="uploadingContainer">
|
||||||
|
{#if !upload[1].uploadStatus.fileId}
|
||||||
|
<p in:fade={{duration:300, delay:400, easingFunc:circOut}} out:padding_scaleY={{easingFunc:circIn,op:true}}>{upload[1].uploadStatus.error ?? "Uploading..."}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if upload[1].uploadStatus.fileId}
|
||||||
|
<div style:height="10px" transition:padding_scaleY />
|
||||||
|
{#if !upload[1].viewingUrl}
|
||||||
|
<div class="buttonContainer" out:_void in:_void={{easingFunc:circOut}}>
|
||||||
|
<button on:click={() => uploads[upload[0]].viewingUrl = true}>
|
||||||
|
view url
|
||||||
|
</button>
|
||||||
|
<button on:click={() => navigator.clipboard.writeText(`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`)}>
|
||||||
|
copy url
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="buttonContainer" out:_void in:_void={{easingFunc:circOut}}>
|
||||||
|
<input type="text" readonly value={`https://${window.location.host}/download/${upload[1].uploadStatus.fileId}`} style:flex-basis="80%">
|
||||||
|
<button on:click={() => uploads[upload[0]].viewingUrl = false} style:flex-basis="20%">
|
||||||
|
ok
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div style:height="10px" transition:padding_scaleY />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if uploadInProgress == false}
|
||||||
|
|
||||||
|
<!-- if required for upload, check if logged in -->
|
||||||
|
{#if ($serverStats.accounts||{}).requiredForUpload ? !!$account.username : true}
|
||||||
|
|
||||||
|
<AttachmentZone bind:this={attachmentZone} on:addFiles={handle_file_upload}/>
|
||||||
|
<div style:min-height="10px" transition:_void={{rTarg:"height",prop:"min-height"}} />
|
||||||
|
{#if Object.keys(uploads).length > 0}
|
||||||
|
<button in:padding_scaleY={{easingFunc:circOut}} out:_void on:click={upload_files}>upload</button>
|
||||||
|
<div transition:_void={{rTarg:"height",prop:"min-height"}} style:min-height="10px" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
|
||||||
|
<p transition:_void style:color="#999999" style:text-align="center">Please log in to upload files.</p>
|
||||||
|
<div transition:_void={{rTarg:"height",prop:"min-height"}} style:min-height="10px" />
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<p style:color="#999999" style:text-align="center">
|
||||||
|
Hosting <span class="number" style:font-weight="600">{$serverStats.files || "•••"}</span> files
|
||||||
|
—
|
||||||
|
Maximum filesize is <span class="number" style:font-weight="600">{(($serverStats.maxDiscordFileSize || 0)*($serverStats.maxDiscordFiles || 0))/1048576 || "•••"}MiB</span>
|
||||||
|
<br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style:color="#999999" style:text-align="center" style:font-size="12px">
|
||||||
|
Made with {Math.floor(Math.random()*10)==0 ? "🐟" : "❤"} by <a href="https://github.com/nbitzz" style:font-size="12px">@nbitzz</a> — <a href="https://github.com/nbitzz/monofile" style:font-size="12px">source</a>
|
||||||
|
</p>
|
||||||
|
<div style:height="10px" />
|
||||||
|
</div>
|
78
src/svelte/elem/prompts/OptionPicker.svelte
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import { fade, slide } from "svelte/transition";
|
||||||
|
|
||||||
|
|
||||||
|
let activeModal;
|
||||||
|
let modalResults;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param mdl {name:string,icon:string,description:string,id:string}[]
|
||||||
|
* @returns Promise
|
||||||
|
*/
|
||||||
|
export function picker(title,mdl) {
|
||||||
|
if (activeModal) forceCancel()
|
||||||
|
|
||||||
|
return new Promise((resolve,reject) => {
|
||||||
|
activeModal = {
|
||||||
|
resolve,
|
||||||
|
title,
|
||||||
|
modal:mdl
|
||||||
|
}
|
||||||
|
|
||||||
|
modalResults = {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forceCancel() {
|
||||||
|
if (activeModal && activeModal.resolve) {
|
||||||
|
activeModal.resolve(null)
|
||||||
|
}
|
||||||
|
activeModal = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if activeModal}
|
||||||
|
<div class="modalContainer" transition:fade={{duration:200}}>
|
||||||
|
<button class="mdHitbox" on:click|self={forceCancel}></button>
|
||||||
|
<div class="modal" transition:slide={{duration:200}}>
|
||||||
|
|
||||||
|
<div class="optPicker">
|
||||||
|
|
||||||
|
<div class="category">
|
||||||
|
<p style:margin-bottom="10px">{activeModal.title}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each activeModal.modal as option (option.id)}
|
||||||
|
{#if option.inputSettings}
|
||||||
|
<div class="inp">
|
||||||
|
<img src={option.icon} alt={option.id}>
|
||||||
|
|
||||||
|
<!-- i have to do this stupidness because of svelte but -->
|
||||||
|
<!-- its reason for blocking this is pretty good sooooo -->
|
||||||
|
|
||||||
|
{#if option.inputSettings.password}
|
||||||
|
<input placeholder={option.name} type="password" bind:value={modalResults[option.id]}>
|
||||||
|
{:else}
|
||||||
|
<input placeholder={option.name} bind:value={modalResults[option.id]}>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button on:click={() => {activeModal.resolve({...modalResults,selected:option.id});activeModal=null;modalResults=null;}}>
|
||||||
|
<img src={option.icon} alt={option.id}>
|
||||||
|
<p>{option.name}<span><br />{option.description}</span></p>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<button on:click={forceCancel}>
|
||||||
|
<img src="/static/assets/icons/delete.svg" alt="cancel">
|
||||||
|
<p>Cancel</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|